From c4cfe0ebcdf3dba5dd2cfa11d7238e12e524b6cc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:04:28 +0000 Subject: [PATCH 1/2] node:events: make errorMonitor an unregistered Symbol Node creates events.errorMonitor with Symbol('events.errorMonitor'). Bun created it with Symbol.for('events.errorMonitor'), so the symbol was in the global symbol registry. A listener added under Symbol.for('events.errorMonitor') then ran on every 'error' emit, and listenerCount(errorMonitor) counted it. In Node that listener is an ordinary listener. Create the symbol with Symbol() in src/js/node/events.ts and in the browser polyfill src/node-fallbacks/events.js. Symbol.keyFor(events.errorMonitor) now returns undefined, as in Node. --- src/js/node/events.ts | 2 +- src/node-fallbacks/events.js | 2 +- test/bundler/bundler_browser.test.ts | 30 ++++++++++++++++++ test/js/node/events/event-emitter.test.ts | 37 +++++++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/js/node/events.ts b/src/js/node/events.ts index 455dd4e0ac76..68febad27e7b 100644 --- a/src/js/node/events.ts +++ b/src/js/node/events.ts @@ -47,7 +47,7 @@ const kCapture: typeof shared.kCapture = shared.kCapture; // writes `undefined` instead of `delete`, keeping one shared JSC Structure // so the (StructureID, name)-keyed megamorphic cache stays hot. const kShapeMode: typeof shared.kShapeMode = shared.kShapeMode; -const kErrorMonitor = Symbol.for("events.errorMonitor"); +const kErrorMonitor = Symbol("events.errorMonitor"); const kMaxEventTargetListeners = Symbol("events.maxEventTargetListeners"); const kMaxEventTargetListenersWarned = Symbol("events.maxEventTargetListenersWarned"); const kWatermarkData = Symbol.for("nodejs.watermarkData"); diff --git a/src/node-fallbacks/events.js b/src/node-fallbacks/events.js index 60a19d471083..fa6e9bfe8873 100644 --- a/src/node-fallbacks/events.js +++ b/src/node-fallbacks/events.js @@ -9,7 +9,7 @@ const SymbolFor = Symbol.for; const kCapture = Symbol("kCapture"); -const kErrorMonitor = SymbolFor("events.errorMonitor"); +const kErrorMonitor = Symbol("events.errorMonitor"); const kMaxEventTargetListeners = Symbol("events.maxEventTargetListeners"); const kMaxEventTargetListenersWarned = Symbol("events.maxEventTargetListenersWarned"); const kRejection = SymbolFor("nodejs.rejection"); diff --git a/test/bundler/bundler_browser.test.ts b/test/bundler/bundler_browser.test.ts index 92c277d551c6..e9f4929043cd 100644 --- a/test/bundler/bundler_browser.test.ts +++ b/test/bundler/bundler_browser.test.ts @@ -166,6 +166,36 @@ describe("bundler", () => { assert(!out.includes("$newPromiseCapability"), "events polyfill must not reference a JSC builtin intrinsic"); }, }); + // The polyfill is its own copy of the emitter. As in node, its errorMonitor + // is not the symbol that Symbol.for("events.errorMonitor") returns. + itBundled("browser/NodeEventsErrorMonitor", { + files: { + "/entry.js": /* js */ ` + import { EventEmitter } from "node:events"; + const e = new EventEmitter(); + const calls = []; + e.on(EventEmitter.errorMonitor, err => calls.push("monitor:" + err.message)); + e.on(Symbol.for("events.errorMonitor"), err => calls.push("registered:" + err.message)); + try { + e.emit("error", new Error("boom")); + } catch (err) { + calls.push("threw:" + err.message); + } + console.log(JSON.stringify({ + keyFor: Symbol.keyFor(EventEmitter.errorMonitor) ?? null, + calls, + monitors: e.listenerCount(EventEmitter.errorMonitor), + })); + `, + }, + target: "browser", + run: { + stdout: '{"keyFor":null,"calls":["monitor:boom","threw:boom"],"monitors":1}', + }, + onAfterBundle(api) { + api.expectFile("out.js").not.toInclude("import "); + }, + }); itBundled("browser/NodeUrlProtocolTablesIgnorePrototype", { files: { "/entry.js": /* js */ ` diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts index a8c26f5f6f8b..eb87b9b6920f 100644 --- a/test/js/node/events/event-emitter.test.ts +++ b/test/js/node/events/event-emitter.test.ts @@ -8,6 +8,7 @@ import { join } from "path"; // our transpiler transform changes this to a var with import.meta.require import EventEmitter, { captureRejectionSymbol, + errorMonitor, getEventListeners, getMaxListeners, listenerCount, @@ -757,6 +758,42 @@ describe("EventEmitter error handling", () => { expect(handled).toBe(true); }); + + test("errorMonitor is not in the global symbol registry", () => { + expect({ + namedExport: errorMonitor === EventEmitter.errorMonitor, + sameAsRegistered: errorMonitor === Symbol.for("events.errorMonitor"), + keyFor: Symbol.keyFor(errorMonitor), + description: errorMonitor.description, + }).toEqual({ + namedExport: true, + sameAsRegistered: false, + keyFor: undefined, + description: "events.errorMonitor", + }); + }); + + test("a listener under Symbol.for('events.errorMonitor') is not an error monitor", () => { + const registered = Symbol.for("events.errorMonitor"); + const myEmitter = new EventEmitter(); + + const calls: unknown[][] = []; + myEmitter.on(registered, (...args) => calls.push(args)); + + expect(() => { + myEmitter.emit("error", new Error("boom")); + }).toThrow("boom"); + expect(calls).toEqual([]); + + expect(myEmitter.emit(registered, "direct")).toBe(true); + expect(calls).toEqual([["direct"]]); + + myEmitter.on(errorMonitor, () => {}); + expect({ + monitors: myEmitter.listenerCount(errorMonitor), + registered: myEmitter.listenerCount(registered), + }).toEqual({ monitors: 1, registered: 1 }); + }); }); describe("EventEmitter captureRejections", () => { From ad32e6810f31e0be1582e27fec742736ae00dddb Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:23:14 +0000 Subject: [PATCH 2/2] node-fallbacks/events: keep the browser polyfill's errorMonitor registered Each browser bundle has its own copy of the polyfill. The symbol registry is what lets two bundles on one page share one errorMonitor. Leave the polyfill as it was. This change is for the runtime module only. --- src/node-fallbacks/events.js | 2 +- test/bundler/bundler_browser.test.ts | 30 ---------------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/node-fallbacks/events.js b/src/node-fallbacks/events.js index fa6e9bfe8873..60a19d471083 100644 --- a/src/node-fallbacks/events.js +++ b/src/node-fallbacks/events.js @@ -9,7 +9,7 @@ const SymbolFor = Symbol.for; const kCapture = Symbol("kCapture"); -const kErrorMonitor = Symbol("events.errorMonitor"); +const kErrorMonitor = SymbolFor("events.errorMonitor"); const kMaxEventTargetListeners = Symbol("events.maxEventTargetListeners"); const kMaxEventTargetListenersWarned = Symbol("events.maxEventTargetListenersWarned"); const kRejection = SymbolFor("nodejs.rejection"); diff --git a/test/bundler/bundler_browser.test.ts b/test/bundler/bundler_browser.test.ts index e9f4929043cd..92c277d551c6 100644 --- a/test/bundler/bundler_browser.test.ts +++ b/test/bundler/bundler_browser.test.ts @@ -166,36 +166,6 @@ describe("bundler", () => { assert(!out.includes("$newPromiseCapability"), "events polyfill must not reference a JSC builtin intrinsic"); }, }); - // The polyfill is its own copy of the emitter. As in node, its errorMonitor - // is not the symbol that Symbol.for("events.errorMonitor") returns. - itBundled("browser/NodeEventsErrorMonitor", { - files: { - "/entry.js": /* js */ ` - import { EventEmitter } from "node:events"; - const e = new EventEmitter(); - const calls = []; - e.on(EventEmitter.errorMonitor, err => calls.push("monitor:" + err.message)); - e.on(Symbol.for("events.errorMonitor"), err => calls.push("registered:" + err.message)); - try { - e.emit("error", new Error("boom")); - } catch (err) { - calls.push("threw:" + err.message); - } - console.log(JSON.stringify({ - keyFor: Symbol.keyFor(EventEmitter.errorMonitor) ?? null, - calls, - monitors: e.listenerCount(EventEmitter.errorMonitor), - })); - `, - }, - target: "browser", - run: { - stdout: '{"keyFor":null,"calls":["monitor:boom","threw:boom"],"monitors":1}', - }, - onAfterBundle(api) { - api.expectFile("out.js").not.toInclude("import "); - }, - }); itBundled("browser/NodeUrlProtocolTablesIgnorePrototype", { files: { "/entry.js": /* js */ `