diff --git a/src/js/node/worker_threads.ts b/src/js/node/worker_threads.ts index 921df1a621d2..012f05c7f13b 100644 --- a/src/js/node/worker_threads.ts +++ b/src/js/node/worker_threads.ts @@ -755,23 +755,26 @@ function receiveMessageOnPort(port: MessagePort) { // TODO: parent port emulation is not complete function fakeParentPort() { const fake = Object.create(MessagePort.prototype); - Object.defineProperty(fake, "onmessage", { - get() { - return self.onmessage; - }, - set(value) { - self.onmessage = value; - }, - }); - - Object.defineProperty(fake, "onmessageerror", { - get() { - return self.onmessageerror; - }, - set(value) { - self.onmessageerror = value; - }, - }); + function defineHandlerAttribute(name: string, event: string) { + let handler: any = null; + const wrapper = (e: Event) => { + if ($isCallable(handler)) handler.$call(fake, e); + else if ($isCallable(handler?.handleEvent)) handler.handleEvent.$call(handler, e); + }; + Object.defineProperty(fake, name, { + get() { + return handler; + }, + set(value) { + const next = $isCallable(value) || (value !== null && typeof value === "object") ? value : null; + if (handler === null && next !== null) self.addEventListener(event, wrapper); + else if (handler !== null && next === null) self.removeEventListener(event, wrapper); + handler = next; + }, + }); + } + defineHandlerAttribute("onmessage", "message"); + defineHandlerAttribute("onmessageerror", "messageerror"); const postMessage = $newCppFunction("ZigGlobalObject.cpp", "jsFunctionPostMessage", 1); Object.defineProperty(fake, "postMessage", { @@ -832,6 +835,10 @@ let parentPort: MessagePort | null = isMainThread ? null : fakeParentPort(); // Gate on _isNodeWorker so a raw `new globalThis.Worker` that transitively loads // this module does NOT have process.abort/chdir/setuid replaced. if (!isMainThread && _isNodeWorker) { + // Node has no global onmessage; parentPort shares this event target, so keeping it double-delivers. + try { + delete (globalThis as any).onmessage; + } catch {} applyWorkerProcessOverrides(); } function applyWorkerProcessOverrides() { diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts index 68d6c6f3f103..252f62d62989 100644 --- a/test/js/node/worker_threads/worker_threads.test.ts +++ b/test/js/node/worker_threads/worker_threads.test.ts @@ -1756,3 +1756,59 @@ test("the SHARE_ENV founding thread's process.env stays live after the swap", as expect(stdout.trim()).toBe("yes,unset"); expect(exitCode).toBe(0); }); + +// https://github.com/oven-sh/bun/issues/7816 +// Emscripten's pthread worker shim registers parentPort.on("message", ...) and +// then assigns self.onmessage. In Node the global onmessage is not an event +// handler accessor, so only the parentPort listener fires; Bun was delivering +// the message to both, so the pthread 'load' handler ran twice and aborted. +test("globalThis.onmessage is not an event handler accessor inside a node:worker_threads worker", async () => { + const worker = new Worker( + /* js */ ` + const { parentPort } = require("worker_threads"); + let count = 0; + const hadOnMessage = "onmessage" in globalThis; + Object.assign(global, { self: global }); + parentPort.on("message", data => { + globalThis.onmessage({ data }); + }); + self.onmessage = e => { + count++; + if (e.data === "ping") { + setImmediate(() => parentPort.postMessage({ count, hadOnMessage })); + } + }; + parentPort.postMessage("ready"); + `, + { eval: true }, + ); + try { + await once(worker, "message"); // "ready" + worker.postMessage("ping"); + const [result] = await once(worker, "message"); + expect(result).toEqual({ count: 1, hadOnMessage: false }); + } finally { + await worker.terminate(); + } +}); + +test("parentPort.onmessage works inside a node:worker_threads worker", async () => { + const worker = new Worker( + /* js */ ` + const { parentPort } = require("worker_threads"); + parentPort.onmessage = e => { + parentPort.postMessage({ echoed: e.data, handlerIsSet: typeof parentPort.onmessage === "function" }); + }; + parentPort.postMessage("ready"); + `, + { eval: true }, + ); + try { + await once(worker, "message"); // "ready" + worker.postMessage("hello"); + const [result] = await once(worker, "message"); + expect(result).toEqual({ echoed: "hello", handlerIsSet: true }); + } finally { + await worker.terminate(); + } +}); diff --git a/test/napi/node-napi-tests/test/common/index.js b/test/napi/node-napi-tests/test/common/index.js index 5267266ac390..e3d01cd1e9d6 100644 --- a/test/napi/node-napi-tests/test/common/index.js +++ b/test/napi/node-napi-tests/test/common/index.js @@ -356,8 +356,8 @@ if (typeof Bun === "object") { ResolveMessage, ErrorEvent, Worker, - onmessage, - onerror, + globalThis.onmessage, + globalThis.onerror, ); }