Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/bun.js/bindings/BunWorkerGlobalScope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,29 @@ void WorkerGlobalScope::onDidChangeListenerImpl(EventTarget& self, const AtomStr
{
if (eventType == eventNames().messageEvent) {
auto& global = static_cast<WorkerGlobalScope&>(self);
auto* context = global.scriptExecutionContext();
// Only ref/unref the event loop if we're in a worker thread, not the main thread.
// In the main thread, onmessage handlers shouldn't keep the process alive.
bool shouldRefEventLoop = context && !context->isMainThread();

switch (kind) {
case Add:
if (global.m_messageEventCount == 0) {
global.scriptExecutionContext()->refEventLoop();
if (global.m_messageEventCount == 0 && shouldRefEventLoop) {
context->refEventLoop();
}
global.m_messageEventCount++;
break;
case Remove:
global.m_messageEventCount--;
if (global.m_messageEventCount == 0) {
global.scriptExecutionContext()->unrefEventLoop();
if (global.m_messageEventCount == 0 && shouldRefEventLoop) {
context->unrefEventLoop();
}
break;
// I dont think clear in this context is ever called. If it is (search OnDidChangeListenerKind::Clear for the impl),
// it may actually call once per event, in a way the Remove code above would suffice.
case Clear:
if (global.m_messageEventCount > 0) {
global.scriptExecutionContext()->unrefEventLoop();
if (global.m_messageEventCount > 0 && shouldRefEventLoop) {
context->unrefEventLoop();
}
global.m_messageEventCount = 0;
break;
Expand Down
70 changes: 70 additions & 0 deletions test/js/web/workers/onmessage-main-thread.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";

test("setting global onmessage in main thread should not prevent process exit", async () => {
// This test verifies that setting a global onmessage handler in the main thread
// doesn't keep the event loop alive and prevent the process from exiting.
// This was a bug where packages like 'lzma' that detect Web Worker environments
// by checking `typeof onmessage !== 'undefined'` would inadvertently keep the
// process alive.

using dir = tempDir("onmessage-test", {
"test.js": `
// Set a global onmessage handler (simulating what the lzma package does)
onmessage = function(e) {
console.log('received message:', e);
};
console.log('OK');
// Process should exit here, not hang
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout.trim()).toBe("OK");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
}, 5000); // 5 second timeout - should exit quickly

test("setting global onmessage in worker thread should work normally", async () => {
// This test verifies that onmessage in a worker thread still works correctly
// and doesn't exit prematurely.

using dir = tempDir("onmessage-worker-test", {
"worker.js": `
onmessage = function(e) {
postMessage('received: ' + e.data);
};
`,
"main.js": `
const worker = new Worker(new URL('worker.js', import.meta.url).href);
worker.postMessage('hello');
worker.onmessage = (e) => {
console.log(e.data);
worker.terminate();
};
`,
});

await using proc = Bun.spawn({
cmd: [bunExe(), "main.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stdout.trim()).toBe("received: hello");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
}, 5000);