diff options
author | 2023-04-18 17:59:51 -0400 | |
---|---|---|
committer | 2023-04-18 14:59:51 -0700 | |
commit | 96a2ed1040d5a0ca51ae41267cba4f8e5d0a6142 (patch) | |
tree | 5af89ada3aff9044bcdca75dbf0101bc4515f964 | |
parent | 76deb51c294126406c8f0be5f13bb184c1ddb454 (diff) | |
download | bun-96a2ed1040d5a0ca51ae41267cba4f8e5d0a6142.tar.gz bun-96a2ed1040d5a0ca51ae41267cba4f8e5d0a6142.tar.zst bun-96a2ed1040d5a0ca51ae41267cba4f8e5d0a6142.zip |
implement `node:events` in javascript (#2604)
* initial event emitter reimplementation
* implement most of node:events. tests passing
* work on emitter
* fix importing node:events
* work on event emitter tests
* event work
* event work
* event stuff and experimenting with a lazy createHash
* cleanup crypto stuff i had on this branch
* finish event stuff up
* fix error monitor
* validate listeners are functions
* changes requested
-rwxr-xr-x | bench/bun.lockb | bin | 34640 -> 35848 bytes | |||
-rw-r--r-- | bench/emitter/implementations.mjs | 31 | ||||
-rw-r--r-- | bench/emitter/microbench.mjs | 96 | ||||
-rw-r--r-- | bench/emitter/microbench_once.mjs | 40 | ||||
-rw-r--r-- | bench/emitter/realworld_stream.mjs | 63 | ||||
-rw-r--r-- | bench/package.json | 10 | ||||
-rwxr-xr-x | bench/snippets/bun.lockb | bin | 1477 -> 0 bytes | |||
-rw-r--r-- | bench/snippets/emitter.mjs | 101 | ||||
-rw-r--r-- | bench/snippets/package.json | 7 | ||||
-rw-r--r-- | docs/runtime/nodejs-apis.md | 2 | ||||
-rw-r--r-- | src/bun.js/events.exports.js | 464 | ||||
-rw-r--r-- | src/bun.js/module_loader.zig | 10 | ||||
-rwxr-xr-x | test/bun.lockb | bin | 36614 -> 135217 bytes | |||
-rw-r--r-- | test/js/node/events/event-emitter.test.ts | 529 | ||||
-rw-r--r-- | test/js/node/events/node-builtins.test.js | 18 |
15 files changed, 1161 insertions, 210 deletions
diff --git a/bench/bun.lockb b/bench/bun.lockb Binary files differindex abc5a11b4..298e2a7c9 100755 --- a/bench/bun.lockb +++ b/bench/bun.lockb 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 Binary files differdeleted file mode 100755 index 3acb6d075..000000000 --- a/bench/snippets/bun.lockb +++ /dev/null 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 Binary files differindex e1935e667..928fb2f33 100755 --- a/test/bun.lockb +++ b/test/bun.lockb 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); - } - }); -}); |