Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
b6afa8dfae [autofix.ci] apply automated fixes 2025-11-29 01:43:35 +00:00
Claude
5151ee71c9 fix(runtime): prevent globalThis.onmessage from hanging process (#24256)
When globalThis.onmessage was set in the main thread, Bun would keep
the event loop alive indefinitely waiting for messages, even though
there's no actual message passing mechanism in the main thread context.

This fix adds a check to only ref the event loop when in a Worker
context (where message passing is actually expected), matching Node.js
behavior where setting globalThis.onmessage doesn't prevent exit.

Fixes #24256

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 17:41:43 -08:00
2 changed files with 102 additions and 3 deletions

View File

@@ -17,24 +17,33 @@ void WorkerGlobalScope::onDidChangeListenerImpl(EventTarget& self, const AtomStr
{
if (eventType == eventNames().messageEvent) {
auto& global = static_cast<WorkerGlobalScope&>(self);
auto* context = global.scriptExecutionContext();
// Only keep the event loop alive if we're in a Worker context.
// In the main thread, setting globalThis.onmessage should not prevent exit
// since there's no actual message passing mechanism.
if (!context || !context->isWorker) {
return;
}
switch (kind) {
case Add:
if (global.m_messageEventCount == 0) {
global.scriptExecutionContext()->refEventLoop();
context->refEventLoop();
}
global.m_messageEventCount++;
break;
case Remove:
global.m_messageEventCount--;
if (global.m_messageEventCount == 0) {
global.scriptExecutionContext()->unrefEventLoop();
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();
context->unrefEventLoop();
}
global.m_messageEventCount = 0;
break;

View File

@@ -0,0 +1,90 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { Worker } from "node:worker_threads";
test("globalThis.onmessage should not prevent process exit", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", 'console.log("hello world"); globalThis.onmessage = () => {}'],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("hello world\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("globalThis.onmessage can be set to a function", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
globalThis.onmessage = () => {
console.log("message handler set");
};
console.log(typeof globalThis.onmessage);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("function\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("globalThis.onmessage can be set to null", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
globalThis.onmessage = () => {};
globalThis.onmessage = null;
console.log(globalThis.onmessage);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("null\n");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("Workers with onmessage should still work properly", async () => {
using dir = tempDir("worker-onmessage-test", {
"worker.js": `
const { parentPort } = require("node:worker_threads");
parentPort.onmessage = (event) => {
parentPort.postMessage({ received: event.data });
};
`,
});
const worker = new Worker(String(dir) + "/worker.js");
const { promise, resolve } = Promise.withResolvers();
worker.on("message", msg => {
resolve(msg);
});
worker.postMessage("hello");
const result = await promise;
expect(result).toEqual({ received: "hello" });
await worker.terminate();
});