fix(worker_threads): use MessagePort for parentPort instead of dispatching to self

Node.js worker_threads delivers messages only to parentPort, not to
self.onmessage. Libraries like fflate set both handlers expecting only
parentPort.on('message') to fire.

Previously, Bun dispatched messages to both self.onmessage AND
parentPort event listeners, causing handlers to run twice.

This fix creates a real MessagePort pair for Node workers:
- Parent keeps port1 (m_parentPort) for worker.postMessage()
- Worker gets port2 as parentPort via entangle()
- WorkerMessageForwarder forwards port1 messages to Worker object

This matches Node.js architecture where worker.postMessage() goes
through a MessagePort channel, not the global scope.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 4
Claude-Escapes: 0
This commit is contained in:
Dylan Conway
2026-01-19 21:18:51 -08:00
parent 62834e1bfe
commit 94d609cd69
5 changed files with 145 additions and 74 deletions

View File

@@ -32,6 +32,34 @@ test("support eval in worker", async () => {
await worker.terminate();
});
// In Node.js worker_threads, messages go to parentPort only, not self.onmessage.
// Libraries like fflate set both handlers, expecting only parentPort to fire.
test("worker_threads messages should not trigger self.onmessage", async () => {
const workerCode = `
const { parentPort } = require('worker_threads');
let selfOnMessageCount = 0;
let parentPortOnMessageCount = 0;
self.onmessage = () => { selfOnMessageCount++; };
parentPort.on('message', () => {
parentPortOnMessageCount++;
parentPort.postMessage({ selfOnMessageCount, parentPortOnMessageCount });
});
`;
const worker = new Worker(workerCode, { eval: true });
const result = await new Promise<{ selfOnMessageCount: number; parentPortOnMessageCount: number }>(
(resolve, reject) => {
worker.on("message", resolve);
worker.on("error", reject);
worker.postMessage({ test: 1 });
},
);
await worker.terminate();
expect(result.parentPortOnMessageCount).toBe(1);
expect(result.selfOnMessageCount).toBe(0);
});
test("all worker_threads module properties are present", () => {
expect(wt).toHaveProperty("getEnvironmentData");
expect(wt).toHaveProperty("isMainThread");