Files
bun-src/test/js/node/events/event-emitter.test.ts
2026-08-27 21:09:14 +00:00

1099 lines
30 KiB
TypeScript

import { sleep } from "bun";
import { describe, expect, mock, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
import { createRequire } from "module";
// this is also testing that imports with default and named imports in the same statement work
// our transpiler transform changes this to a var with import.meta.require
import EventEmitter, {
captureRejectionSymbol,
getEventListeners,
getMaxListeners,
listenerCount,
setMaxListeners,
} from "node:events";
describe("node:events", () => {
test("captureRejectionSymbol", () => {
expect(EventEmitter.captureRejectionSymbol).toBeDefined();
expect(captureRejectionSymbol).toBeDefined();
expect(captureRejectionSymbol).toBe(EventEmitter.captureRejectionSymbol);
});
test("once", done => {
const emitter = new EventEmitter();
EventEmitter.once(emitter, "hey").then(x => {
try {
expect(x).toEqual([1, 5]);
} catch (error) {
done(error);
}
done();
});
emitter.emit("hey", 1, 5);
});
test("once (abort)", done => {
const emitter = new EventEmitter();
const controller = new AbortController();
EventEmitter.once(emitter, "hey", { signal: controller.signal })
.then(() => done(new Error("Should not be called")))
.catch(() => done());
controller.abort();
});
test("once (two events in same tick)", done => {
const emitter = new EventEmitter();
EventEmitter.once(emitter, "hey").then(() => {
EventEmitter.once(emitter, "hey").then(data => {
try {
expect(data).toEqual([3]);
} catch (error) {
done(error);
}
done();
});
setTimeout(() => {
emitter.emit("hey", 3);
}, 10);
});
emitter.emit("hey", 1);
emitter.emit("hey", 2);
});
/// https://github.com/oven-sh/bun/issues/4518
test("once removes the listener afterwards", async () => {
const emitter = new EventEmitter();
process.nextTick(() => {
emitter.emit("hey", 1);
});
const promise = EventEmitter.once(emitter, "hey");
expect(emitter.listenerCount("hey")).toBe(1);
await promise;
expect(emitter.listenerCount("hey")).toBe(0);
});
// `events.once()` is an `async function` in Node: a bad `options`, a bad
// `options.signal`, or an already-aborted signal must produce a *rejected
// promise*, never a synchronous throw.
test("once is an async function", () => {
expect(EventEmitter.once.constructor.name).toBe("AsyncFunction");
});
test("once with already-aborted signal rejects (not a synchronous throw)", async () => {
const ee = new EventEmitter();
const p = EventEmitter.once(ee, "foo", { signal: AbortSignal.abort() });
expect(p).toBeInstanceOf(Promise);
await expect(p).rejects.toMatchObject({ name: "AbortError", code: "ABORT_ERR" });
});
test("once with invalid options.signal rejects (not a synchronous throw)", async () => {
for (const signal of [1, {}, "hi", null, false]) {
const ee = new EventEmitter();
const p = EventEmitter.once(ee, "foo", { signal } as any);
expect(p).toBeInstanceOf(Promise);
await expect(p).rejects.toMatchObject({ code: "ERR_INVALID_ARG_TYPE" });
}
});
test("once with non-object options rejects (not a synchronous throw)", async () => {
const ee = new EventEmitter();
const p = EventEmitter.once(ee, "foo", "hi" as any);
expect(p).toBeInstanceOf(Promise);
await expect(p).rejects.toMatchObject({ code: "ERR_INVALID_ARG_TYPE" });
});
});
describe("EventEmitter", () => {
test("getEventListeners", () => {
expect(getEventListeners(new EventEmitter(), "hey").length).toBe(0);
const emitter = new EventEmitter();
emitter.on("hey", () => {});
expect(getEventListeners(emitter, "hey").length).toBe(1);
});
test("constructor", () => {
var emitter = new EventEmitter();
emitter.setMaxListeners(100);
expect(emitter.getMaxListeners()).toBe(100);
});
test("removeAllListeners()", () => {
var emitter = new EventEmitter() as any;
var ran = false;
emitter.on("hey", () => {
ran = true;
});
emitter.on("hey", () => {
ran = true;
});
emitter.on("exit", () => {
ran = true;
});
const { _events } = emitter;
emitter.removeAllListeners();
expect(emitter.listenerCount("hey")).toBe(0);
expect(emitter.listenerCount("exit")).toBe(0);
emitter.emit("hey");
emitter.emit("exit");
expect(ran).toBe(false);
expect(_events).not.toBe(emitter._events); // This looks wrong but node.js replaces it too
emitter.on("hey", () => {
ran = true;
});
emitter.emit("hey");
expect(ran).toBe(true);
expect(emitter.listenerCount("hey")).toBe(1);
});
test("removeAllListeners(type)", () => {
var emitter = new EventEmitter();
var ran = false;
emitter.on("hey", () => {
ran = true;
});
emitter.on("exit", () => {
ran = true;
});
expect(emitter.listenerCount("hey")).toBe(1);
emitter.removeAllListeners("hey");
expect(emitter.listenerCount("hey")).toBe(0);
expect(emitter.listenerCount("exit")).toBe(1);
emitter.emit("hey");
expect(ran).toBe(false);
emitter.emit("exit");
expect(ran).toBe(true);
});
// These are also tests for the done() function in the test runner.
describe("emit", () => {
test("different tick", done => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
queueMicrotask(() => {
emitter.emit("wow");
});
});
// Unlike Jest, bun supports async and done
test("async microtask before", done => {
(async () => {
await 1;
var emitter = new EventEmitter();
emitter.on("wow", () => done());
emitter.emit("wow");
})();
});
test("async microtask after", done => {
(async () => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
await 1;
emitter.emit("wow");
})();
});
test("same tick", done => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
emitter.emit("wow");
});
test("setTimeout task", done => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
setTimeout(() => emitter.emit("wow"), 1);
});
test("emit multiple values", () => {
const emitter = new EventEmitter();
const receivedVals: number[] = [];
emitter.on("multiple-vals", (val1, val2, val3) => {
receivedVals[0] = val1;
receivedVals[1] = val2;
receivedVals[2] = val3;
});
emitter.emit("multiple-vals", 1, 2, 3);
expect(receivedVals).toEqual([1, 2, 3]);
});
});
test("addListener return type", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.addListener("foo", () => {})).toBe(myEmitter);
});
test("addListener validates function", () => {
var myEmitter = new EventEmitter();
expect(() => myEmitter.addListener("foo", {} as any)).toThrow();
});
test("removeListener return type", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.removeListener("foo", () => {})).toBe(myEmitter);
});
test("once", () => {
var myEmitter = new EventEmitter();
var calls = 0;
const fn = () => {
calls++;
};
myEmitter.once("foo", fn);
expect(myEmitter.listenerCount("foo")).toBe(1);
expect(myEmitter.listeners("foo")).toEqual([fn]);
myEmitter.emit("foo");
myEmitter.emit("foo");
expect(calls).toBe(1);
expect(myEmitter.listenerCount("foo")).toBe(0);
});
test("addListener/removeListener aliases", () => {
expect(EventEmitter.prototype.addListener).toBe(EventEmitter.prototype.on);
expect(EventEmitter.prototype.removeListener).toBe(EventEmitter.prototype.off);
});
test("prependListener", () => {
const myEmitter = new EventEmitter();
const order: number[] = [];
myEmitter.on("foo", () => {
order.push(1);
});
myEmitter.prependListener("foo", () => {
order.push(2);
});
myEmitter.prependListener("foo", () => {
order.push(3);
});
myEmitter.on("foo", () => {
order.push(4);
});
myEmitter.emit("foo");
expect(order).toEqual([3, 2, 1, 4]);
});
test("prependOnceListener", () => {
const myEmitter = new EventEmitter();
const order: number[] = [];
myEmitter.on("foo", () => {
order.push(1);
});
myEmitter.prependOnceListener("foo", () => {
order.push(2);
});
myEmitter.prependOnceListener("foo", () => {
order.push(3);
});
myEmitter.on("foo", () => {
order.push(4);
});
myEmitter.emit("foo");
expect(order).toEqual([3, 2, 1, 4]);
myEmitter.emit("foo");
expect(order).toEqual([3, 2, 1, 4, 1, 4]);
});
test("prependListener in callback", () => {
const myEmitter = new EventEmitter();
const order: number[] = [];
myEmitter.on("foo", () => {
order.push(1);
});
myEmitter.once("foo", () => {
myEmitter.prependListener("foo", () => {
order.push(2);
});
});
myEmitter.on("foo", () => {
order.push(3);
});
myEmitter.emit("foo");
expect(order).toEqual([1, 3]);
myEmitter.emit("foo");
expect(order).toEqual([1, 3, 2, 1, 3]);
});
test("addListener in callback", () => {
const myEmitter = new EventEmitter();
const order: number[] = [];
myEmitter.on("foo", () => {
order.push(1);
});
myEmitter.once("foo", () => {
myEmitter.addListener("foo", () => {
order.push(2);
});
});
myEmitter.on("foo", () => {
order.push(3);
});
myEmitter.emit("foo");
expect(order).toEqual([1, 3]);
myEmitter.emit("foo");
expect(order).toEqual([1, 3, 1, 3, 2]);
});
test("listeners", () => {
const myEmitter = new EventEmitter();
const fn = () => {};
myEmitter.on("foo", fn);
expect(myEmitter.listeners("foo")).toEqual([fn]);
const fn2 = () => {};
myEmitter.on("foo", fn2);
expect(myEmitter.listeners("foo")).toEqual([fn, fn2]);
myEmitter.off("foo", fn2);
expect(myEmitter.listeners("foo")).toEqual([fn]);
const fn3 = () => {};
myEmitter.once("foo", fn3);
expect(myEmitter.listeners("foo")).toEqual([fn, fn3]);
});
test("rawListeners", () => {
const myEmitter = new EventEmitter();
const fn = () => {};
myEmitter.on("foo", fn);
expect(myEmitter.rawListeners("foo")).toEqual([fn]);
const fn2 = () => {};
myEmitter.on("foo", fn2);
expect(myEmitter.rawListeners("foo")).toEqual([fn, fn2]);
myEmitter.off("foo", fn2);
expect(myEmitter.rawListeners("foo")).toEqual([fn]);
const fn3 = () => {};
myEmitter.once("foo", fn3);
const rawListeners: (Function & { listener?: Function })[] = myEmitter.rawListeners("foo");
// rawListeners() returns onceWrappers as well
expect([rawListeners[0], rawListeners[1].listener]).toEqual([fn, fn3]);
});
test("eventNames", () => {
const myEmitter = new EventEmitter();
expect(myEmitter.eventNames()).toEqual([]);
const fn = () => {};
myEmitter.on("foo", fn);
expect(myEmitter.eventNames()).toEqual(["foo"]);
myEmitter.on("bar", () => {});
expect(myEmitter.eventNames()).toEqual(["foo", "bar"]);
myEmitter.off("foo", fn);
expect(myEmitter.eventNames()).toEqual(["bar"]);
});
test("_eventsCount", () => {
const myEmitter = new EventEmitter() as EventEmitter & {
_eventsCount: number;
};
expect(myEmitter._eventsCount).toBe(0);
myEmitter.on("foo", () => {});
expect(myEmitter._eventsCount).toBe(1);
myEmitter.on("foo", () => {});
expect(myEmitter._eventsCount).toBe(1);
myEmitter.on("bar", () => {});
expect(myEmitter._eventsCount).toBe(2);
myEmitter.on("foo", () => {});
expect(myEmitter._eventsCount).toBe(2);
myEmitter.on("bar", () => {});
expect(myEmitter._eventsCount).toBe(2);
myEmitter.removeAllListeners("foo");
expect(myEmitter._eventsCount).toBe(1);
});
test("events.init", () => {
// init is a undocumented property that is identical to the constructor except it doesn't return the instance
// in node, EventEmitter just calls init()
let instance = Object.create(EventEmitter.prototype);
(EventEmitter as any).init.call(instance);
expect(instance._eventsCount).toBe(0);
expect(instance._maxListeners).toBeUndefined();
expect(instance._events).toEqual({});
expect(instance instanceof EventEmitter).toBe(true);
});
});
describe("EventEmitter.on", () => {
test("Basic test", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey");
expect(asyncIterator.next).toBeDefined();
expect(asyncIterator[Symbol.asyncIterator]).toBeDefined();
process.nextTick(() => {
emitter.emit("hey", 1);
});
const { value } = await asyncIterator.next();
expect(value).toEqual([1]);
});
test("Basic test with for await...of", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey", { close: ["close"] } as any);
process.nextTick(() => {
emitter.emit("hey", 1);
emitter.emit("hey", 2);
emitter.emit("hey", 3);
emitter.emit("hey", 4);
emitter.emit("close");
});
const result = [];
for await (const ev of asyncIterator) {
result.push(ev);
}
expect(result).toEqual([[1], [2], [3], [4]]);
});
test("Stop reading events after 'close' event is emitted", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey", { close: ["close"] } as any);
process.nextTick(() => {
emitter.emit("hey", 1);
emitter.emit("hey", 2);
emitter.emit("close");
emitter.emit("hey", 3);
});
const result = [];
for await (const ev of asyncIterator) {
result.push(ev);
}
expect(result).toEqual([[1], [2]]);
});
test("Queue events before first next() call", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey");
emitter.emit("hey", 1);
emitter.emit("hey", 2);
emitter.emit("hey", 3);
await new Promise(resolve => setTimeout(resolve, 1));
expect((await asyncIterator.next()).value).toEqual([1]);
expect((await asyncIterator.next()).value).toEqual([2]);
expect((await asyncIterator.next()).value).toEqual([3]);
});
test("Emit multiple values", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey");
emitter.emit("hey", 1, 2, 3);
const { value } = await asyncIterator.next();
expect(value).toEqual([1, 2, 3]);
});
test("kFirstEventParam", async () => {
const kFirstEventParam = Symbol.for("nodejs.kFirstEventParam");
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey", { [kFirstEventParam]: true } as any);
emitter.emit("hey", 1, 2, 3);
emitter.emit("hey", [4, 5, 6]);
expect((await asyncIterator.next()).value).toBe(1);
expect((await asyncIterator.next()).value).toEqual([4, 5, 6]);
});
test("Cancel via error event", async () => {
const { on, EventEmitter } = require("node:events");
const process = require("node:process");
const ee = new EventEmitter();
const output = [];
// Emit later on
process.nextTick(() => {
ee.emit("foo", "bar");
ee.emit("foo", 42);
ee.emit("foo", "baz");
});
setTimeout(() => {
ee.emit("error", "DONE");
}, 1);
try {
for await (const event of on(ee, "foo")) {
output.push([1, event]);
}
} catch (error) {
output.push([2, error]);
}
expect(output).toEqual([
[1, ["bar"]],
[1, [42]],
[1, ["baz"]],
[2, "DONE"],
]);
});
test("AbortController", async () => {
const { on, EventEmitter } = require("node:events");
const ac = new AbortController();
const ee = new EventEmitter();
const output = [];
process.nextTick(() => {
ee.emit("foo", "bar");
ee.emit("foo", 42);
ee.emit("foo", "baz");
});
const consumed = (async () => {
try {
for await (const event of on(ee, "foo", { signal: ac.signal })) {
output.push([1, event]);
}
output.push(["unreachable"]);
} catch (error: any) {
const { name, code, message, cause } = error;
output.push([2, { name, code, message, cause }]);
}
})();
process.nextTick(() => ac.abort());
await consumed;
expect(output).toEqual([
[1, ["bar"]],
[1, [42]],
[1, ["baz"]],
[
2,
{
name: "AbortError",
code: "ABORT_ERR",
message: "The operation was aborted",
cause: ac.signal.reason,
},
],
]);
});
// Checks for potential issues with FixedQueue size
test("Queue many events", async () => {
const emitter = new EventEmitter();
const asyncIterator = EventEmitter.on(emitter, "hey");
for (let i = 0; i < 2500; i += 1) {
emitter.emit("hey", i);
}
expect((await asyncIterator.next()).value).toEqual([0]);
});
test("readline.createInterface", async () => {
const { createInterface } = require("node:readline");
const { createReadStream } = require("node:fs");
const path = require("node:path");
const fpath = path.join(__filename, "..", "..", "child_process", "fixtures", "child-process-echo-options.js");
const text = await Bun.file(fpath).text();
const interfaced = createInterface(createReadStream(fpath));
const output = [];
try {
for await (const line of interfaced) {
output.push(line);
}
} catch (e) {}
const out = text.replaceAll("\r\n", "\n").trim().split("\n");
expect(output).toEqual(out);
});
});
describe("EventEmitter error handling", () => {
test("unhandled error event throws on emit", () => {
const myEmitter = new EventEmitter();
expect(() => {
myEmitter.emit("error", "Hello!");
}).toThrow("Hello!");
});
test("unhandled error event throws on emit with no arguments", () => {
const myEmitter = new EventEmitter();
expect(() => {
myEmitter.emit("error");
}).toThrow("Unhandled error.");
});
test("handled error event", () => {
const myEmitter = new EventEmitter();
let handled = false;
myEmitter.on("error", (...args) => {
expect(args).toEqual(["Hello", "World"]);
handled = true;
});
myEmitter.emit("error", "Hello", "World");
expect(handled).toBe(true);
});
test("errorMonitor", () => {
const myEmitter = new EventEmitter();
let handled = false;
myEmitter.on(EventEmitter.errorMonitor, (...args) => {
expect(args).toEqual(["Hello", "World"]);
handled = true;
});
myEmitter.on("error", () => {});
myEmitter.emit("error", "Hello", "World");
expect(handled).toBe(true);
});
test("errorMonitor (unhandled)", () => {
const myEmitter = new EventEmitter();
let handled = false;
myEmitter.on(EventEmitter.errorMonitor, (...args) => {
expect(args).toEqual(["Hello", "World"]);
handled = true;
});
expect(() => {
myEmitter.emit("error", "Hello", "World");
}).toThrow("Hello");
expect(handled).toBe(true);
});
});
describe("EventEmitter captureRejections", () => {
// Can't catch the unhandled rejection because we do not have process.on("unhandledRejection")
// test("captureRejections off will not capture rejections", async () => {
// const myEmitter = new EventEmitter();
// let handled = false;
// myEmitter.on("error", (...args) => {
// handled = true;
// });
// myEmitter.on("action", async () => {
// throw new Error("Hello World");
// });
// myEmitter.emit("action");
// await sleep(1);
// expect(handled).toBe(false);
// });
test("it captures rejections", async () => {
const myEmitter = new EventEmitter({ captureRejections: true });
let handled: any = null;
myEmitter.on("error", (...args) => {
handled = args;
});
myEmitter.on("action", async () => {
throw 123;
});
myEmitter.emit("action");
await sleep(5);
expect(handled).toEqual([123]);
});
test("it does not capture successful promises", async () => {
const myEmitter = new EventEmitter({ captureRejections: true });
let handled: any = null;
myEmitter.on("error", () => {
handled = true;
});
myEmitter.on("action", async () => {
return 123;
});
myEmitter.emit("action");
await sleep(5);
expect(handled).toEqual(null);
});
test("it does not capture handled rejections", async () => {
const myEmitter = new EventEmitter({ captureRejections: true });
let handled: any = null;
myEmitter.on("error", () => {
handled = true;
});
myEmitter.on("action", async () => {
return Promise.reject(123).catch(() => 234);
});
myEmitter.emit("action");
await sleep(5);
expect(handled).toEqual(null);
});
});
const waysOfCreating = [
() => Object.create(EventEmitter.prototype),
() => new EventEmitter(),
() => new (class extends EventEmitter {})(),
() => {
class MyEmitter extends EventEmitter {}
return new MyEmitter();
},
() => {
var foo = {};
Object.setPrototypeOf(foo, EventEmitter.prototype);
return foo;
},
() => {
function FakeEmitter(this: any) {
return EventEmitter.call(this);
}
Object.setPrototypeOf(FakeEmitter.prototype, EventEmitter.prototype);
Object.setPrototypeOf(FakeEmitter, EventEmitter);
return new (FakeEmitter as any)();
},
() => {
const FakeEmitter: any = function FakeEmitter(this: any) {
EventEmitter.call(this);
} as any;
Object.assign(FakeEmitter.prototype, EventEmitter.prototype);
Object.assign(FakeEmitter, EventEmitter);
return new FakeEmitter();
},
() => {
var foo = {};
Object.assign(foo, EventEmitter.prototype);
return foo;
},
];
describe("EventEmitter constructors", () => {
for (let create of waysOfCreating) {
test(`${create
.toString()
.slice(6, 52)
.replaceAll("\n", "")
.trim()
.replaceAll(/ {2,}/g, " ")
.replace(/^\{ ?/, "")} should work`, () => {
var myEmitter = create();
var called = false;
(myEmitter as EventEmitter).once("event", function () {
called = true;
// @ts-ignore
expect(this).toBe(myEmitter);
});
var firstEvents = myEmitter._events;
expect(myEmitter.listenerCount("event")).toBe(1);
expect(myEmitter.emit("event")).toBe(true);
expect(myEmitter.listenerCount("event")).toBe(0);
expect(firstEvents).toEqual({ event: firstEvents.event }); // it shouldn't mutate
expect(called).toBe(true);
});
}
test("with createRequire, events is callable", () => {
const req = createRequire(import.meta.path);
const events = req("events");
new events();
});
test("in cjs, events is callable", () => {
const EventEmitter = require("events");
new EventEmitter();
});
});
test("addAbortListener", async () => {
const emitter = new EventEmitter();
const controller = new AbortController();
const promise = EventEmitter.once(emitter, "hey", { signal: controller.signal });
const mocked = mock();
EventEmitter.addAbortListener(controller.signal, mocked);
controller.abort();
expect(promise).rejects.toThrow("aborted");
expect(mocked).toHaveBeenCalled();
});
test("using addAbortListener", async () => {
const emitter = new EventEmitter();
const controller = new AbortController();
const promise = EventEmitter.once(emitter, "hey", { signal: controller.signal });
const mocked = mock();
{
using aborty = EventEmitter.addAbortListener(controller.signal, mocked);
}
controller.abort();
expect(promise).rejects.toThrow("aborted");
expect(mocked).not.toHaveBeenCalled();
});
describe("addAbortListener resists stopImmediatePropagation", () => {
test("runs after an earlier listener stopped propagation", () => {
const controller = new AbortController();
const signal = controller.signal;
const order: string[] = [];
signal.addEventListener("abort", e => {
order.push("stopper");
e.stopImmediatePropagation();
});
EventEmitter.addAbortListener(signal, e => {
order.push(`cleanup:${(e as Event).target === signal}`);
});
signal.addEventListener("abort", () => order.push("plain-after"));
controller.abort();
expect(order).toEqual(["stopper", "cleanup:true"]);
});
test("runs when it was registered before the listener that stops propagation", () => {
const controller = new AbortController();
const signal = controller.signal;
const order: string[] = [];
EventEmitter.addAbortListener(signal, () => order.push("cleanup"));
signal.addEventListener("abort", e => {
order.push("stopper");
e.stopImmediatePropagation();
});
signal.addEventListener("abort", () => order.push("plain-after"));
controller.abort();
expect(order).toEqual(["cleanup", "stopper"]);
});
test("is not run once disposed", () => {
const controller = new AbortController();
const signal = controller.signal;
const mocked = mock();
signal.addEventListener("abort", e => e.stopImmediatePropagation());
{
using _ = EventEmitter.addAbortListener(signal, mocked);
}
controller.abort();
expect(mocked).not.toHaveBeenCalled();
});
test("once(emitter, event, { signal }) still rejects on a suppressed signal", async () => {
const emitter = new EventEmitter();
const controller = new AbortController();
controller.signal.addEventListener("abort", e => e.stopImmediatePropagation());
const promise = EventEmitter.once(emitter, "never", { signal: controller.signal });
expect(emitter.listenerCount("never")).toBe(1);
controller.abort();
// once()'s abort listener detaches the emitter listener and rejects, both synchronously.
expect(emitter.listenerCount("never")).toBe(0);
expect(await promise.catch(err => err.code)).toBe("ABORT_ERR");
});
test("stopImmediatePropagation still suppresses ordinary listeners", () => {
const target = new EventTarget();
const order: string[] = [];
target.addEventListener("x", () => order.push("a"));
target.addEventListener("x", e => {
order.push("b");
e.stopImmediatePropagation();
});
target.addEventListener("x", () => order.push("c"));
target.dispatchEvent(new Event("x"));
expect(order).toEqual(["a", "b"]);
});
});
test("getMaxListeners", () => {
const emitter = new EventEmitter();
expect(emitter.getMaxListeners()).toBe(10);
emitter.setMaxListeners(20);
expect(emitter.getMaxListeners()).toBe(20);
});
test("setMaxListeners", () => {
const emitter = new EventEmitter();
expect(emitter.getMaxListeners()).toBe(10);
emitter.setMaxListeners(20);
expect(emitter.getMaxListeners()).toBe(20);
setMaxListeners(30, emitter);
expect(emitter.getMaxListeners()).toBe(30);
const eventTarget = new EventTarget();
setMaxListeners(1, eventTarget);
expect(getMaxListeners(eventTarget)).toBe(1);
setMaxListeners(99, eventTarget);
expect(getMaxListeners(eventTarget)).toBe(99);
});
test("getEventListeners", () => {
const target = new EventTarget();
expect(getEventListeners(target, "hey").length).toBe(0);
target.addEventListener("hey", () => {}, { once: true });
expect(getEventListeners(target, "hey").length).toBe(1);
target.dispatchEvent(new Event("hey"));
expect(getEventListeners(target, "hey").length).toBe(0);
});
test("EventEmitter.prototype.listenerCount", () => {
const ee = new EventEmitter();
const a = () => {};
const b = () => {};
expect(ee.listenerCount("x")).toBe(0);
expect(ee.listenerCount("x", a)).toBe(0);
ee.on("x", a);
expect(ee.listenerCount("x")).toBe(1);
expect(ee.listenerCount("x", a)).toBe(1);
expect(ee.listenerCount("x", b)).toBe(0);
ee.on("x", b);
expect(ee.listenerCount("x")).toBe(2);
expect(ee.listenerCount("x", a)).toBe(1);
expect(ee.listenerCount("x", b)).toBe(1);
ee.once("y", a);
expect(ee.listenerCount("y")).toBe(1);
expect(ee.listenerCount("y", a)).toBe(1);
// null/undefined listener arg means "count all", same as omitting it
expect(ee.listenerCount("x", null as any)).toBe(2);
expect(ee.listenerCount("x", undefined)).toBe(2);
});
test("events.listenerCount validates emitter argument", () => {
const ee = new EventEmitter();
ee.on("y", () => {});
expect(listenerCount(ee, "y")).toBe(1);
const et = new EventTarget();
et.addEventListener("k", () => {});
et.addEventListener("k", () => {});
expect(listenerCount(et, "k")).toBe(2);
const np = Object.create(null);
EventEmitter.call(np);
EventEmitter.prototype.on.call(np, "y", () => {});
for (const bad of [{}, 42, np]) {
expect(() => listenerCount(bad as any, "y")).toThrow(
expect.objectContaining({ name: "TypeError", code: "ERR_INVALID_ARG_TYPE" }),
);
}
});
test("EventEmitter.name", () => {
expect(EventEmitter.name).toBe("EventEmitter");
});
// A fired once() wrapper must drop its closure refs so holding it (a cached
// rawListeners() result, the COW array emit() iterates) does not retain the
// emitter. wrapped.listener stays: node asserts it survives emit.
test("once() wrapper releases its target after firing", async () => {
const src = `
const { EventEmitter } = require("events");
const held = [];
const total = 8;
let collected = 0;
const registry = new FinalizationRegistry(() => collected++);
(function () {
for (let i = 0; i < total; i++) {
const ee = new EventEmitter();
ee.once("x", function () {});
held.push(ee.rawListeners("x")[0]);
ee.emit("x");
registry.register(ee);
}
})();
let iters = 0;
setImmediate(function check() {
Bun.gc(true);
if (collected === total) {
console.log("collected " + collected + "/" + total + " holding " + held.length + " wrappers");
return;
}
if (++iters > 50) {
console.log("stuck " + collected + "/" + total + " holding " + held.length + " wrappers");
process.exit(1);
}
setImmediate(check);
});
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({
stdout: "collected 8/8 holding 8 wrappers",
stderr: "",
exitCode: 0,
});
});