Commit 74564d94df5 for nodejs

commit 74564d94df5b6c0879efc5f6d1cdec23f39b1942
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Sun Sep 27 19:02:12 2026 +0200

    worker: serialize messages after exit

    The outside port must serialize and transfer messages even when it has
    no peer. Retain that port after the backing thread exits and create a
    closed port when the entry script fetch fails. This preserves clone
    errors and transfer side effects in both cases.

    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    Assisted-by: Codex
    PR-URL: https://github.com/nodejs/node/pull/66354
    Reviewed-By: Anna Henningsen <anna@addaleax.net>
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Aviv Keller <me@aviv.sh>

diff --git a/lib/internal/webworker.js b/lib/internal/webworker.js
index cf5f1df6372..744fe4de068 100644
--- a/lib/internal/webworker.js
+++ b/lib/internal/webworker.js
@@ -68,6 +68,7 @@ const {
 } = require('internal/worker');

 const {
+  MessageChannel,
   lazyMessageEvent,
 } = require('internal/worker/io');

@@ -107,6 +108,7 @@ const kLocation = Symbol('kLocation');
 const kName = Symbol('kName');
 const kNavigator = Symbol('kNavigator');
 const kNavigatorBrand = Symbol('kNavigatorBrand');
+const kOutsidePort = Symbol('kOutsidePort');
 const kType = Symbol('kType');
 const kURL = Symbol('kURL');
 const kWorker = Symbol('kWorker');
@@ -808,6 +810,12 @@ class Worker extends EventTarget {
     this[kWorker] = null;
     const entry = resolveWorkerEntry(workerURL);
     if (entry === null) {
+      // Even a failed worker has an outside port. Keep a closed port so
+      // postMessage() still serializes its message and transfers objects.
+      const { port1, port2 } = new MessageChannel();
+      port1.close();
+      port2.close();
+      this[kOutsidePort] = port1;
       // "If the algorithm asynchronously completes with null or with a
       // script whose error to rethrow is non-null, then: Queue a global
       // task on the DOM manipulation task source given worker's relevant
@@ -837,7 +845,8 @@ class Worker extends EventTarget {
       },
     });

-    forwardMessageEvents(this[kWorker][kPublicPort], this);
+    this[kOutsidePort] = this[kWorker][kPublicPort];
+    forwardMessageEvents(this[kOutsidePort], this);

     // "Set notHandled to the result of firing an event named error at
     // workerObject, using ErrorEvent, with the cancelable attribute
@@ -872,7 +881,7 @@ class Worker extends EventTarget {
     // invoked the respective postMessage(message, transfer) and
     // postMessage(message, options) on this's outside port, with the same
     // arguments, and returned the same return value."
-    this[kWorker]?.postMessage(message, transfer);
+    this[kOutsidePort].postMessage(message, transfer);
   }

   // The following properties are non-standard, Node.js extensions
diff --git a/test/parallel/test-webworker-postmessage-lifecycle.js b/test/parallel/test-webworker-postmessage-lifecycle.js
new file mode 100644
index 00000000000..51acd57fa62
--- /dev/null
+++ b/test/parallel/test-webworker-postmessage-lifecycle.js
@@ -0,0 +1,30 @@
+// Flags: --experimental-web-worker
+'use strict';
+
+const common = require('../common');
+const assert = require('node:assert');
+
+function checkSerialization(worker) {
+  assert.throws(() => worker.postMessage(() => {}), { name: 'DataCloneError' });
+  const buffer = new ArrayBuffer(8);
+  assert.throws(() => worker.postMessage(null, [buffer, buffer]), { name: 'DataCloneError' });
+  assert.strictEqual(buffer.byteLength, 8);
+  worker.postMessage(null, [buffer]);
+  assert.strictEqual(buffer.byteLength, 0);
+}
+
+// A failed script fetch still leaves an outside port that serializes messages.
+{
+  const worker = new Worker('data:text/plain,');
+  worker.onerror = common.mustCall(() => checkSerialization(worker));
+  checkSerialization(worker);
+}
+
+// Serialization is required even after the backing thread has exited.
+{
+  const worker = new Worker('data:text/javascript,close()');
+  worker.onerror = common.mustNotCall('worker failed');
+  process.once('worker', common.mustCall((thread) => {
+    thread.once('exit', common.mustCall(() => checkSerialization(worker)));
+  }));
+}