aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbench/bun.lockbbin34640 -> 35848 bytes
-rw-r--r--bench/emitter/implementations.mjs31
-rw-r--r--bench/emitter/microbench.mjs96
-rw-r--r--bench/emitter/microbench_once.mjs40
-rw-r--r--bench/emitter/realworld_stream.mjs63
-rw-r--r--bench/package.json10
-rwxr-xr-xbench/snippets/bun.lockbbin1477 -> 0 bytes
-rw-r--r--bench/snippets/emitter.mjs101
-rw-r--r--bench/snippets/package.json7
-rw-r--r--docs/runtime/nodejs-apis.md2
-rw-r--r--src/bun.js/events.exports.js464
-rw-r--r--src/bun.js/module_loader.zig10
-rwxr-xr-xtest/bun.lockbbin36614 -> 135217 bytes
-rw-r--r--test/js/node/events/event-emitter.test.ts529
-rw-r--r--test/js/node/events/node-builtins.test.js18
15 files changed, 1161 insertions, 210 deletions
diff --git a/bench/bun.lockb b/bench/bun.lockb
index abc5a11b4..298e2a7c9 100755
--- a/bench/bun.lockb
+++ b/bench/bun.lockb
Binary files differ
diff --git a/bench/emitter/implementations.mjs b/bench/emitter/implementations.mjs
new file mode 100644
index 000000000..2050ac38e
--- /dev/null
+++ b/bench/emitter/implementations.mjs
@@ -0,0 +1,31 @@
+import EventEmitter3 from "eventemitter3";
+import { group } from "mitata";
+import EventEmitterNative from "node:events";
+
+export const implementations = [
+ {
+ EventEmitter: EventEmitterNative,
+ name: process.isBun ? (EventEmitterNative.init ? "bun" : "C++") : "node:events",
+ monkey: true,
+ },
+ // { EventEmitter: EventEmitter3, name: "EventEmitter3" },
+].filter(Boolean);
+
+for (const impl of implementations) {
+ impl.EventEmitter?.setMaxListeners?.(Infinity);
+}
+
+export function groupForEmitter(name, cb) {
+ if (implementations.length === 1) {
+ return cb({
+ ...implementations[0],
+ name: `${name}: ${implementations[0].name}`,
+ });
+ } else {
+ return group(name, () => {
+ for (let impl of implementations) {
+ cb(impl);
+ }
+ });
+ }
+}
diff --git a/bench/emitter/microbench.mjs b/bench/emitter/microbench.mjs
new file mode 100644
index 000000000..eae59d4c1
--- /dev/null
+++ b/bench/emitter/microbench.mjs
@@ -0,0 +1,96 @@
+import { bench, run } from "mitata";
+import { groupForEmitter } from "./implementations.mjs";
+
+var id = 0;
+
+groupForEmitter("single emit", ({ EventEmitter, name }) => {
+ const emitter = new EventEmitter();
+
+ emitter.on("hello", event => {
+ event.preventDefault();
+ });
+
+ bench(name, () => {
+ emitter.emit("hello", {
+ preventDefault() {
+ id++;
+ },
+ });
+ });
+});
+
+groupForEmitter("on x 10_000 (handler)", ({ EventEmitter, name }) => {
+ const emitter = new EventEmitter();
+
+ bench(name, () => {
+ var cb = event => {
+ event.preventDefault();
+ };
+ emitter.on("hey", cb);
+ var called = false;
+ for (let i = 0; i < 10_000; i++)
+ emitter.emit("hey", {
+ preventDefault() {
+ id++;
+ called = true;
+ },
+ });
+
+ if (!called) throw new Error("not called");
+ });
+});
+
+// for (let { impl: EventEmitter, name, monkey } of []) {
+// if (monkey) {
+// var monkeyEmitter = Object.assign({}, EventEmitter.prototype);
+// monkeyEmitter.on("hello", event => {
+// event.preventDefault();
+// });
+
+// bench(`[monkey] ${className}.emit`, () => {
+// var called = false;
+// monkeyEmitter.emit("hello", {
+// preventDefault() {
+// id++;
+// called = true;
+// },
+// });
+
+// if (!called) {
+// throw new Error("monkey failed");
+// }
+// });
+
+// bench(`[monkey] ${className}.on x 10_000 (handler)`, () => {
+// var cb = () => {
+// event.preventDefault();
+// };
+// monkeyEmitter.on("hey", cb);
+// for (let i = 0; i < 10_000; i++)
+// monkey.emit("hey", {
+// preventDefault() {
+// id++;
+// },
+// });
+// monkeyEmitter.off("hey", cb);
+// });
+// }
+// }
+
+// var target = new EventTarget();
+// target.addEventListener("hello", event => {});
+// bench("EventTarget.dispatch", () => {
+// target.dispatchEvent(event);
+// });
+
+// var hey = new Event("hey");
+
+// bench("EventTarget.on x 10_000 (handler)", () => {
+// var handler = event => {};
+// target.addEventListener("hey", handler);
+
+// for (let i = 0; i < 10_000; i++) target.dispatchEvent(hey);
+// target.removeEventListener("hey", handler);
+// });
+
+await run();
diff --git a/bench/emitter/microbench_once.mjs b/bench/emitter/microbench_once.mjs
new file mode 100644
index 000000000..b24fb2103
--- /dev/null
+++ b/bench/emitter/microbench_once.mjs
@@ -0,0 +1,40 @@
+import { bench, run } from "mitata";
+import { groupForEmitter } from "./implementations.mjs";
+
+var id = 0;
+
+groupForEmitter("test 1", ({ EventEmitter, name }) => {
+ const emitter = new EventEmitter();
+
+ emitter.on("hello", event => {
+ event.preventDefault();
+ });
+
+ bench(name, () => {
+ emitter.once("hello", event => {
+ event.preventDefault();
+ });
+ emitter.emit("hello", {
+ preventDefault() {
+ id++;
+ },
+ });
+ });
+});
+
+groupForEmitter("test 2", ({ EventEmitter, name }) => {
+ const emitter = new EventEmitter();
+
+ bench(name, () => {
+ emitter.once("hello", event => {
+ event.preventDefault();
+ });
+ emitter.emit("hello", {
+ preventDefault() {
+ id++;
+ },
+ });
+ });
+});
+
+await run();
diff --git a/bench/emitter/realworld_stream.mjs b/bench/emitter/realworld_stream.mjs
new file mode 100644
index 000000000..e65398b49
--- /dev/null
+++ b/bench/emitter/realworld_stream.mjs
@@ -0,0 +1,63 @@
+import { bench, run } from "mitata";
+import { groupForEmitter } from "./implementations.mjs";
+
+// Psuedo RNG is derived from https://stackoverflow.com/a/424445
+let rngState = 123456789;
+function nextInt() {
+ const m = 0x80000000; // 2**31;
+ const a = 1103515245;
+ const c = 12345;
+ rngState = (a * rngState + c) % m;
+ return rngState;
+}
+function nextRange(start, end) {
+ // returns in range [start, end): including start, excluding end
+ // can't modulu nextInt because of weak randomness in lower bits
+ const rangeSize = end - start;
+ const randomUnder1 = nextInt() / 0x7fffffff; // 2**31 - 1
+ return start + Math.floor(randomUnder1 * rangeSize);
+}
+
+const chunks = new Array(1024).fill(null).map((_, j) => {
+ const arr = new Uint8Array(1024);
+ for (let i = 0; i < arr.length; i++) {
+ arr[i] = nextRange(0, 256);
+ }
+ return arr;
+});
+
+groupForEmitter("stream simulation", ({ EventEmitter, name }) => {
+ bench(name, () => {
+ let id = 0;
+ const stream = new EventEmitter();
+
+ stream.on("start", res => {
+ if (res.status !== 200) throw new Error("not 200");
+ });
+
+ const recived = [];
+ stream.on("data", req => {
+ recived.push(req);
+ });
+
+ stream.on("end", ev => {
+ ev.preventDefault();
+ });
+
+ // simulate a stream
+ stream.emit("start", { status: 200 });
+ for (let chunk of chunks) {
+ stream.emit("data", chunk);
+ }
+ stream.emit("end", {
+ preventDefault() {
+ id++;
+ },
+ });
+
+ if (id !== 1) throw new Error("not implemented right");
+ if (recived.length !== 1024) throw new Error("not implemented right");
+ });
+});
+
+await run();
diff --git a/bench/package.json b/bench/package.json
index 9cd47e043..501dd6f51 100644
--- a/bench/package.json
+++ b/bench/package.json
@@ -1,11 +1,13 @@
{
"name": "bench",
"dependencies": {
- "mitata": "^0.1.6",
- "esbuild": "^0.14.12",
- "@swc/core": "^1.2.133",
"@babel/core": "^7.16.10",
- "@babel/preset-react": "^7.16.7"
+ "@babel/preset-react": "^7.16.7",
+ "@swc/core": "^1.2.133",
+ "benchmark": "^2.1.4",
+ "esbuild": "^0.14.12",
+ "eventemitter3": "^5.0.0",
+ "mitata": "^0.1.6"
},
"scripts": {
"ffi": "cd ffi && bun run deps && bun run build && bun run bench",
diff --git a/bench/snippets/bun.lockb b/bench/snippets/bun.lockb
deleted file mode 100755
index 3acb6d075..000000000
--- a/bench/snippets/bun.lockb
+++ /dev/null
Binary files differ
diff --git a/bench/snippets/emitter.mjs b/bench/snippets/emitter.mjs
deleted file mode 100644
index f708cf77a..000000000
--- a/bench/snippets/emitter.mjs
+++ /dev/null
@@ -1,101 +0,0 @@
-// **so this file can run in node**
-import { createRequire } from "node:module";
-const require = createRequire(import.meta.url);
-// --
-
-const EventEmitterNative = require("node:events").EventEmitter;
-const TypedEmitter = require("tiny-typed-emitter").TypedEmitter;
-const EventEmitter3 = require("eventemitter3").EventEmitter;
-import { bench, run } from "../../node_modules/mitata/src/cli.mjs";
-const event = new Event("hello");
-var id = 0;
-for (let [EventEmitter, className] of [
- [EventEmitterNative, "EventEmitter"],
- [TypedEmitter, "TypedEmitter"],
- [EventEmitter3, "EventEmitter3"],
-]) {
- const emitter = new EventEmitter();
-
- emitter.on("hello", event => {
- event.preventDefault();
- });
-
- bench(`${className}.emit`, () => {
- emitter.emit("hello", {
- preventDefault() {
- id++;
- },
- });
- });
-
- bench(`${className}.on x 10_000 (handler)`, () => {
- var cb = event => {
- event.preventDefault();
- };
- emitter.on("hey", cb);
- var called = false;
- for (let i = 0; i < 10_000; i++)
- emitter.emit("hey", {
- preventDefault() {
- id++;
- called = true;
- },
- });
- emitter.off("hey", cb);
-
- if (!called) throw new Error("not called");
- });
-
- if (EventEmitter !== EventEmitter3) {
- var monkey = Object.assign({}, EventEmitter.prototype);
- monkey.on("hello", event => {
- event.preventDefault();
- });
-
- bench(`[monkey] ${className}.emit`, () => {
- var called = false;
- monkey.emit("hello", {
- preventDefault() {
- id++;
- called = true;
- },
- });
-
- if (!called) {
- throw new Error("monkey failed");
- }
- });
-
- bench(`[monkey] ${className}.on x 10_000 (handler)`, () => {
- var cb = () => {
- event.preventDefault();
- };
- monkey.on("hey", cb);
- for (let i = 0; i < 10_000; i++)
- monkey.emit("hey", {
- preventDefault() {
- id++;
- },
- });
- monkey.off("hey", cb);
- });
- }
-}
-
-var target = new EventTarget();
-target.addEventListener("hello", event => {});
-bench("EventTarget.dispatch", () => {
- target.dispatchEvent(event);
-});
-
-var hey = new Event("hey");
-
-bench("EventTarget.on x 10_000 (handler)", () => {
- var handler = event => {};
- target.addEventListener("hey", handler);
-
- for (let i = 0; i < 10_000; i++) target.dispatchEvent(hey);
- target.removeEventListener("hey", handler);
-});
-
-await run();
diff --git a/bench/snippets/package.json b/bench/snippets/package.json
deleted file mode 100644
index 6244001a8..000000000
--- a/bench/snippets/package.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "dependencies": {
- "eventemitter3": "^5.0.0",
- "tiny-typed-emitter": "latest"
- },
- "prettier": "../../.prettierrc.cjs"
-}
diff --git a/docs/runtime/nodejs-apis.md b/docs/runtime/nodejs-apis.md
index cbb264486..9fb6e778b 100644
--- a/docs/runtime/nodejs-apis.md
+++ b/docs/runtime/nodejs-apis.md
@@ -81,7 +81,7 @@ This page is updated regularly to reflect compatibility status of the latest ver
- {% anchor id="node_events" %} [`node:events`](https://nodejs.org/api/events.html) {% /anchor %}
- 🟡
-- Missing `EventEmitterAsyncResource`. `EventEmitter` is missing `{get}set}MaxListeners` `usingDomains` `init`.
+- Missing `EventEmitterAsyncResource` `events.on`.
---
diff --git a/src/bun.js/events.exports.js b/src/bun.js/events.exports.js
new file mode 100644
index 000000000..3e4536ff2
--- /dev/null
+++ b/src/bun.js/events.exports.js
@@ -0,0 +1,464 @@
+// Reimplementation of https://nodejs.org/api/events.html
+// Reference: https://github.com/nodejs/node/blob/main/lib/events.js
+var { isPromise, Array, Object } = import.meta.primordials;
+const SymbolFor = Symbol.for;
+const ObjectDefineProperty = Object.defineProperty;
+const kCapture = Symbol("kCapture");
+const kErrorMonitor = SymbolFor("events.errorMonitor");
+const kMaxEventTargetListeners = Symbol("events.maxEventTargetListeners");
+const kMaxEventTargetListenersWarned = Symbol("events.maxEventTargetListenersWarned");
+const kWatermarkData = SymbolFor("nodejs.watermarkData");
+const kRejection = SymbolFor("nodejs.rejection");
+const captureRejectionSymbol = SymbolFor("nodejs.rejection");
+const ArrayPrototypeSlice = Array.prototype.slice;
+
+var defaultMaxListeners = 10;
+
+// EventEmitter must be a standard function because some old code will do weird tricks like `EventEmitter.apply(this)`.
+function EventEmitter(opts) {
+ if (this._events === undefined || this._events === this.__proto__._events) {
+ this._events = { __proto__: null };
+ this._eventsCount = 0;
+ }
+
+ this._maxListeners ??= undefined;
+ if (
+ (this[kCapture] = opts?.captureRejections ? Boolean(opts?.captureRejections) : EventEmitter.prototype[kCapture])
+ ) {
+ this.emit = emitWithRejectionCapture;
+ }
+}
+
+EventEmitter.prototype._events = undefined;
+EventEmitter.prototype._eventsCount = 0;
+EventEmitter.prototype._maxListeners = undefined;
+EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
+ validateNumber(n, "setMaxListeners", 0);
+ this._maxListeners = n;
+ return this;
+};
+
+EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
+ return this._maxListeners ?? defaultMaxListeners;
+};
+
+function emitError(emitter, args) {
+ var { _events: events } = emitter;
+ args[0] ??= new Error("Unhandled error.");
+ if (!events) throw args[0];
+ var errorMonitor = events[kErrorMonitor];
+ if (errorMonitor) {
+ for (var handler of ArrayPrototypeSlice.call(errorMonitor)) {
+ handler.apply(emitter, args);
+ }
+ }
+ var handlers = events.error;
+ if (!handlers) throw args[0];
+ for (var handler of ArrayPrototypeSlice.call(handlers)) {
+ handler.apply(emitter, args);
+ }
+ return true;
+}
+
+function addCatch(emitter, promise, type, args) {
+ promise.then(undefined, function (err) {
+ // The callback is called with nextTick to avoid a follow-up rejection from this promise.
+ process.nextTick(emitUnhandledRejectionOrErr, emitter, err, type, args);
+ });
+}
+
+function emitUnhandledRejectionOrErr(emitter, err, type, args) {
+ if (typeof emitter[kRejection] === "function") {
+ emitter[kRejection](err, type, ...args);
+ } else {
+ // If the error handler throws, it is not catchable and it will end up in 'uncaughtException'.
+ // We restore the previous value of kCapture in case the uncaughtException is present
+ // and the exception is handled.
+ try {
+ emitter[kCapture] = false;
+ emitter.emit("error", err);
+ } finally {
+ emitter[kCapture] = true;
+ }
+ }
+}
+
+const emitWithoutRejectionCapture = function emit(type, ...args) {
+ if (type === "error") {
+ return emitError(this, args);
+ }
+ var { _events: events } = this;
+ if (events === undefined) return false;
+ var handlers = events[type];
+ if (handlers === undefined) return false;
+
+ for (var handler of [...handlers]) {
+ handler.apply(this, args);
+ }
+ return true;
+};
+
+const emitWithRejectionCapture = function emit(type, ...args) {
+ if (type === "error") {
+ return emitError(this, args);
+ }
+ var { _events: events } = this;
+ if (events === undefined) return false;
+ var handlers = events[type];
+ if (handlers === undefined) return false;
+ for (var handler of [...handlers]) {
+ var result = handler.apply(this, args);
+ if (result !== undefined && isPromise(result)) {
+ addCatch(this, result, type, args);
+ }
+ }
+ return true;
+};
+
+EventEmitter.prototype.emit = emitWithoutRejectionCapture;
+
+EventEmitter.prototype.addListener = function addListener(type, fn) {
+ checkListener(fn);
+ var events = this._events;
+ if (!events) {
+ events = this._events = { __proto__: null };
+ this._eventsCount = 0;
+ } else if (events.newListener) {
+ this.emit("newListener", type, fn.listener ?? fn);
+ }
+ var handlers = events[type];
+ if (!handlers) {
+ events[type] = [fn];
+ this._eventsCount++;
+ } else {
+ handlers.push(fn);
+ var m = this._maxListeners ?? defaultMaxListeners;
+ if (m > 0 && handlers.length > m && !handlers.warned) {
+ overflowWarning(this, type, handlers);
+ }
+ }
+ return this;
+};
+
+EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+
+EventEmitter.prototype.prependListener = function prependListener(type, fn) {
+ checkListener(fn);
+ var events = this._events;
+ if (!events) {
+ events = this._events = { __proto__: null };
+ this._eventsCount = 0;
+ } else if (events.newListener) {
+ this.emit("newListener", type, fn.listener ?? fn);
+ }
+ var handlers = events[type];
+ if (!handlers) {
+ events[type] = [fn];
+ this._eventsCount++;
+ } else {
+ handlers.unshift(fn);
+ var m = this._maxListeners ?? defaultMaxListeners;
+ if (m > 0 && handlers.length > m && !handlers.warned) {
+ overflowWarning(this, type, handlers);
+ }
+ }
+ return this;
+};
+
+function overflowWarning(emitter, type, handlers) {
+ handlers.warned = true;
+ const warn = new Error(
+ `Possible EventEmitter memory leak detected. ${handlers.length} ${String(type)} listeners ` +
+ `added to [${emitter.constructor.name}]. Use emitter.setMaxListeners() to increase limit`,
+ );
+ warn.name = "MaxListenersExceededWarning";
+ warn.emitter = emitter;
+ warn.type = type;
+ warn.count = handlers.length;
+ process.emitWarning(warn);
+}
+
+function onceWrapper(type, listener, ...args) {
+ this.removeListener(type, listener);
+ listener.apply(this, args);
+}
+
+EventEmitter.prototype.once = function once(type, fn) {
+ checkListener(fn);
+ const bound = onceWrapper.bind(this, type, fn);
+ bound.listener = fn;
+ this.addListener(type, bound);
+ return this;
+};
+
+EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, fn) {
+ checkListener(fn);
+ const bound = onceWrapper.bind(this, type, fn);
+ bound.listener = fn;
+ this.prependListener(type, bound);
+ return this;
+};
+
+EventEmitter.prototype.removeListener = function removeListener(type, fn) {
+ checkListener(fn);
+ var { _events: events } = this;
+ if (!events) return this;
+ var handlers = events[type];
+ if (!handlers) return this;
+ var length = handlers.length;
+ let position = -1;
+ for (let i = length - 1; i >= 0; i--) {
+ if (handlers[i] === fn || handlers[i].listener === fn) {
+ position = i;
+ break;
+ }
+ }
+ if (position < 0) return this;
+ if (position === 0) {
+ handlers.shift();
+ } else {
+ handlers.splice(position, 1);
+ }
+ if (handlers.length === 0) {
+ delete events[type];
+ this._eventsCount--;
+ }
+ return this;
+};
+
+EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
+
+EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) {
+ var { _events: events } = this;
+ if (type && events) {
+ if (events[type]) {
+ delete events[type];
+ this._eventsCount--;
+ }
+ } else {
+ this._events = { __proto__: null };
+ }
+ return this;
+};
+
+EventEmitter.prototype.listeners = function listeners(type) {
+ var { _events: events } = this;
+ if (!events) return [];
+ var handlers = events[type];
+ if (!handlers) return [];
+ return handlers.map(x => x.listener ?? x);
+};
+
+EventEmitter.prototype.rawListeners = function rawListeners(type) {
+ var { _events } = this;
+ if (!_events) return [];
+ var handlers = _events[type];
+ if (!handlers) return [];
+ return handlers.slice();
+};
+
+EventEmitter.prototype.listenerCount = function listenerCount(type) {
+ var { _events: events } = this;
+ if (!events) return 0;
+ return events[type]?.length ?? 0;
+};
+
+EventEmitter.prototype.eventNames = function eventNames() {
+ return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : [];
+};
+
+EventEmitter.prototype[kCapture] = false;
+
+function once(emitter, type, { signal } = {}) {
+ validateAbortSignal(signal, "options.signal");
+ if (signal?.aborted) {
+ throw new AbortError(undefined, { cause: signal?.reason });
+ }
+ return new Promise((resolve, reject) => {
+ const errorListener = err => {
+ emitter.removeListener(type, resolver);
+ if (signal != null) {
+ eventTargetAgnosticRemoveListener(signal, "abort", abortListener);
+ }
+ reject(err);
+ };
+ const resolver = (...args) => {
+ if (typeof emitter.removeListener === "function") {
+ emitter.removeListener("error", errorListener);
+ }
+ if (signal != null) {
+ eventTargetAgnosticRemoveListener(signal, "abort", abortListener);
+ }
+ resolve(args);
+ };
+ eventTargetAgnosticAddListener(emitter, type, resolver, { once: true });
+ if (type !== "error" && typeof emitter.once === "function") {
+ // EventTarget does not have `error` event semantics like Node
+ // EventEmitters, we listen to `error` events only on EventEmitters.
+ emitter.once("error", errorListener);
+ }
+ function abortListener() {
+ eventTargetAgnosticRemoveListener(emitter, type, resolver);
+ eventTargetAgnosticRemoveListener(emitter, "error", errorListener);
+ reject(new AbortError(undefined, { cause: signal?.reason }));
+ }
+ if (signal != null) {
+ eventTargetAgnosticAddListener(signal, "abort", abortListener, { once: true });
+ }
+ });
+}
+EventEmitter.once = once;
+
+function on(emitter, type, { signal, close, highWatermark = Number.MAX_SAFE_INTEGER, lowWatermark = 1 } = {}) {
+ throw new Error("events.on is not implemented. See https://github.com/oven-sh/bun/issues/2679");
+}
+EventEmitter.on = on;
+
+function getEventListeners(emitter, type) {
+ if (emitter instanceof EventTarget) {
+ throw new Error(
+ "getEventListeners with an EventTarget is not implemented. See https://github.com/oven-sh/bun/issues/2678",
+ );
+ }
+ return emitter.listeners(type);
+}
+EventEmitter.getEventListeners = getEventListeners;
+
+function setMaxListeners(n, ...eventTargets) {
+ validateNumber(n, "setMaxListeners", 0);
+ var length;
+ if (eventTargets && (length = eventTargets.length)) {
+ for (let i = 0; i < length; i++) {
+ eventTargets[i].setMaxListeners(n);
+ }
+ } else {
+ defaultMaxListeners = n;
+ }
+}
+EventEmitter.setMaxListeners = setMaxListeners;
+
+function listenerCount(emitter, type) {
+ return emitter.listenerCount(type);
+}
+EventEmitter.listenerCount = listenerCount;
+
+EventEmitter.EventEmitter = EventEmitter;
+EventEmitter.usingDomains = false;
+EventEmitter.captureRejectionSymbol = captureRejectionSymbol;
+ObjectDefineProperty(EventEmitter, "captureRejections", {
+ __proto__: null,
+ get() {
+ return EventEmitter.prototype[kCapture];
+ },
+ set(value) {
+ validateBoolean(value, "EventEmitter.captureRejections");
+
+ EventEmitter.prototype[kCapture] = value;
+ },
+ enumerable: true,
+});
+EventEmitter.errorMonitor = kErrorMonitor;
+Object.defineProperties(EventEmitter, {
+ defaultMaxListeners: {
+ enumerable: true,
+ get: () => {
+ return defaultMaxListeners;
+ },
+ set: arg => {
+ validateNumber(arg, "defaultMaxListeners", 0);
+ defaultMaxListeners = arg;
+ },
+ },
+ kMaxEventTargetListeners: {
+ __proto__: null,
+ value: kMaxEventTargetListeners,
+ enumerable: false,
+ configurable: false,
+ writable: false,
+ },
+ kMaxEventTargetListenersWarned: {
+ __proto__: null,
+ value: kMaxEventTargetListenersWarned,
+ enumerable: false,
+ configurable: false,
+ writable: false,
+ },
+});
+EventEmitter.init = EventEmitter;
+EventEmitter[Symbol.for("CommonJS")] = 0;
+
+export default EventEmitter;
+
+function eventTargetAgnosticRemoveListener(emitter, name, listener, flags) {
+ if (typeof emitter.removeListener === "function") {
+ emitter.removeListener(name, listener);
+ } else {
+ emitter.removeEventListener(name, listener, flags);
+ }
+}
+
+function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
+ if (typeof emitter.on === "function") {
+ emitter.on(name, listener);
+ } else {
+ emitter.addEventListener(name, listener);
+ }
+}
+
+class AbortError extends Error {
+ constructor(message = "The operation was aborted", options = undefined) {
+ if (options !== undefined && typeof options !== "object") {
+ throw new codes.ERR_INVALID_ARG_TYPE("options", "Object", options);
+ }
+ super(message, options);
+ this.code = "ABORT_ERR";
+ this.name = "AbortError";
+ }
+}
+
+function ERR_INVALID_ARG_TYPE(name, type, value) {
+ const err = new TypeError(`The "${name}" argument must be of type ${type}. Received ${value}`);
+ err.code = "ERR_INVALID_ARG_TYPE";
+ return err;
+}
+
+function ERR_OUT_OF_RANGE(name, range, value) {
+ const err = new RangeError(`The "${name}" argument is out of range. It must be ${range}. Received ${value}`);
+ err.code = "ERR_OUT_OF_RANGE";
+ return err;
+}
+
+function validateAbortSignal(signal, name) {
+ if (signal !== undefined && (signal === null || typeof signal !== "object" || !("aborted" in signal))) {
+ throw new ERR_INVALID_ARG_TYPE(name, "AbortSignal", signal);
+ }
+}
+
+function validateNumber(value, name, min = undefined, max) {
+ if (typeof value !== "number") throw new ERR_INVALID_ARG_TYPE(name, "number", value);
+ if (
+ (min != null && value < min) ||
+ (max != null && value > max) ||
+ ((min != null || max != null) && Number.isNaN(value))
+ ) {
+ throw new ERR_OUT_OF_RANGE(
+ name,
+ `${min != null ? `>= ${min}` : ""}${min != null && max != null ? " && " : ""}${max != null ? `<= ${max}` : ""}`,
+ value,
+ );
+ }
+}
+
+function checkListener(listener) {
+ if (typeof listener !== "function") {
+ throw new TypeError("The listener must be a function");
+ }
+}
+
+export class EventEmitterAsyncResource extends EventEmitter {
+ constructor(options = undefined) {
+ throw new Error("EventEmitterAsyncResource is not implemented. See https://github.com/oven-sh/bun/issues/2681");
+ }
+}
+
+EventEmitter.EventEmitterAsyncResource = EventEmitterAsyncResource;
diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig
index 4404946f4..e89cd21eb 100644
--- a/src/bun.js/module_loader.zig
+++ b/src/bun.js/module_loader.zig
@@ -1710,7 +1710,15 @@ pub const ModuleLoader = struct {
.@"node:buffer" => return jsSyntheticModule(.@"node:buffer"),
.@"node:string_decoder" => return jsSyntheticModule(.@"node:string_decoder"),
.@"node:module" => return jsSyntheticModule(.@"node:module"),
- .@"node:events" => return jsSyntheticModule(.@"node:events"),
+ .@"node:events" => {
+ return ResolvedSource{
+ .allocator = null,
+ .source_code = ZigString.init(jsModuleFromFile(jsc_vm.load_builtins_from_path, "events.exports.js")),
+ .specifier = ZigString.init("node:events"),
+ .source_url = ZigString.init("node:events"),
+ .hash = 0,
+ };
+ },
.@"node:process" => return jsSyntheticModule(.@"node:process"),
.@"node:tty" => return jsSyntheticModule(.@"node:tty"),
.@"node:util/types" => return jsSyntheticModule(.@"node:util/types"),
diff --git a/test/bun.lockb b/test/bun.lockb
index e1935e667..928fb2f33 100755
--- a/test/bun.lockb
+++ b/test/bun.lockb
Binary files differ
diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts
index 401ccf605..cef309d48 100644
--- a/test/js/node/events/event-emitter.test.ts
+++ b/test/js/node/events/event-emitter.test.ts
@@ -1,34 +1,106 @@
-import { test, describe, expect, it } from "bun:test";
-import { heapStats } from "bun:jsc";
-import { expectMaxObjectTypeCount, gc } from "harness";
+import { test, describe, expect } from "bun:test";
+import { sleep } from "bun";
+
// 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, { getEventListeners, captureRejectionSymbol } from "node:events";
-describe("EventEmitter", () => {
- it("captureRejectionSymbol", () => {
+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);
+ });
+
+ // TODO: extensive events.on tests
+ // test("on", () => {
+ // const emitter = new EventEmitter();
+ // const asyncIterator = EventEmitter.on(emitter, "hey");
+
+ // expect(asyncIterator.next).toBeDefined();
+ // expect(asyncIterator[Symbol.asyncIterator]).toBeDefined();
+
+ // const fn = async () => {
+ // const { value } = await asyncIterator.next();
+ // expect(value).toBe(1);
+ // };
+
+ // emitter.emit("hey", 1, 2, 3);
+ // });
+});
+
+describe("EventEmitter", () => {
test("getEventListeners", () => {
expect(getEventListeners(new EventEmitter(), "hey").length).toBe(0);
});
- test("EventEmitter constructor", () => {
+
+ test("constructor", () => {
var emitter = new EventEmitter();
emitter.setMaxListeners(100);
expect(emitter.getMaxListeners()).toBe(100);
});
- test("EventEmitter.removeAllListeners()", () => {
- var emitter = new EventEmitter();
+ 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;
});
@@ -37,42 +109,366 @@ describe("EventEmitter", () => {
expect(emitter.listenerCount("hey")).toBe(1);
});
- // These are also tests for the done() function in the test runner.
- test("EventEmitter emit (different tick)", done => {
+ test("removeAllListeners(type)", () => {
var emitter = new EventEmitter();
- emitter.on("wow", () => done());
- queueMicrotask(() => {
+ 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);
+ });
});
- // Unlike Jest, bun supports async and done
- test("async EventEmitter emit (microtask)", async done => {
- await 1;
- var emitter = new EventEmitter();
- emitter.on("wow", () => done());
- emitter.emit("wow");
+ test("addListener return type", () => {
+ var myEmitter = new EventEmitter();
+ expect(myEmitter.addListener("foo", () => {})).toBe(myEmitter);
});
- test("async EventEmitter emit (microtask) after", async done => {
- var emitter = new EventEmitter();
- emitter.on("wow", () => done());
- await 1;
- emitter.emit("wow");
+ test("addListener validates function", () => {
+ var myEmitter = new EventEmitter();
+ expect(() => myEmitter.addListener("foo", {} as any)).toThrow();
});
- test("EventEmitter emit (same tick)", done => {
- var emitter = new EventEmitter();
+ 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++;
+ };
- emitter.on("wow", () => done());
+ myEmitter.once("foo", fn);
- emitter.emit("wow");
+ 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("EventEmitter emit (setTimeout task)", done => {
- var emitter = new EventEmitter();
- emitter.on("wow", () => done());
- setTimeout(() => emitter.emit("wow"), 1);
+ 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("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]);
+ });
+
+ test("rawListeners", () => {
+ 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]);
+ });
+
+ 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 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);
});
});
@@ -112,53 +508,30 @@ const waysOfCreating = [
},
];
-for (let create of waysOfCreating) {
- it(`${create.toString().slice(10, 40).replaceAll("\n", "\\n").trim()} 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);
+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(firstEvents).toBe(myEmitter._events);
- expect(called).toBe(true);
- });
-}
-
-test("EventEmitter.on", () => {
- var myEmitter = new EventEmitter();
- expect(myEmitter.on("foo", () => {})).toBe(myEmitter);
-});
+ expect(myEmitter.emit("event")).toBe(true);
+ expect(myEmitter.listenerCount("event")).toBe(0);
-test("EventEmitter.off", () => {
- var myEmitter = new EventEmitter();
- expect(myEmitter.off("foo", () => {})).toBe(myEmitter);
-});
-
-// Internally, EventEmitter has a JSC::Weak with the thisValue of the listener
-test("EventEmitter GCs", async () => {
- gc();
-
- const startCount = heapStats().objectTypeCounts["EventEmitter"] ?? 0;
- (function () {
- function EventEmitterSubclass(this: any) {
- EventEmitter.call(this);
- }
-
- Object.setPrototypeOf(EventEmitterSubclass.prototype, EventEmitter.prototype);
- Object.setPrototypeOf(EventEmitterSubclass, EventEmitter);
- // @ts-ignore
- var myEmitter = new EventEmitterSubclass();
- myEmitter.on("foo", () => {});
- myEmitter.emit("foo");
- })();
-
- await expectMaxObjectTypeCount(expect, "EventEmitter", startCount);
+ expect(firstEvents).toEqual({ event: firstEvents.event }); // it shouldn't mutate
+ expect(called).toBe(true);
+ });
+ }
});
diff --git a/test/js/node/events/node-builtins.test.js b/test/js/node/events/node-builtins.test.js
deleted file mode 100644
index 67050f31a..000000000
--- a/test/js/node/events/node-builtins.test.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { describe, it, expect } from "bun:test";
-
-import { EventEmitter } from "events";
-var emitters = [EventEmitter, require("events")];
-describe("EventEmitter", () => {
- it("should emit events", () => {
- for (let Emitter of emitters) {
- const emitter = new Emitter();
- var called = false;
- const listener = () => {
- called = true;
- };
- emitter.on("test", listener);
- emitter.emit("test");
- expect(called).toBe(true);
- }
- });
-});