diff options
author | 2023-04-21 07:16:23 -0700 | |
---|---|---|
committer | 2023-04-21 07:16:23 -0700 | |
commit | a4d0a1961abe0c6073e15cc6f7c0601b74f2e3f7 (patch) | |
tree | b0b2f0debda72afe5349b46872e7c508b2f81b94 | |
parent | 143ccdbeb6988bef73f0a465566c900b62e96103 (diff) | |
download | bun-a4d0a1961abe0c6073e15cc6f7c0601b74f2e3f7.tar.gz bun-a4d0a1961abe0c6073e15cc6f7c0601b74f2e3f7.tar.zst bun-a4d0a1961abe0c6073e15cc6f7c0601b74f2e3f7.zip |
Revert "implement `node:events` in javascript (#2604)"
This reverts commit 96a2ed1040d5a0ca51ae41267cba4f8e5d0a6142.
-rwxr-xr-x | bench/bun.lockb | bin | 35848 -> 34640 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 | 0 -> 1477 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 -> 0 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, 210 insertions, 1161 deletions
diff --git a/bench/bun.lockb b/bench/bun.lockb Binary files differindex 298e2a7c9..abc5a11b4 100755 --- a/bench/bun.lockb +++ b/bench/bun.lockb diff --git a/bench/emitter/implementations.mjs b/bench/emitter/implementations.mjs deleted file mode 100644 index 2050ac38e..000000000 --- a/bench/emitter/implementations.mjs +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index eae59d4c1..000000000 --- a/bench/emitter/microbench.mjs +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index b24fb2103..000000000 --- a/bench/emitter/microbench_once.mjs +++ /dev/null @@ -1,40 +0,0 @@ -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 deleted file mode 100644 index e65398b49..000000000 --- a/bench/emitter/realworld_stream.mjs +++ /dev/null @@ -1,63 +0,0 @@ -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 501dd6f51..9cd47e043 100644 --- a/bench/package.json +++ b/bench/package.json @@ -1,13 +1,11 @@ { "name": "bench", "dependencies": { - "@babel/core": "^7.16.10", - "@babel/preset-react": "^7.16.7", - "@swc/core": "^1.2.133", - "benchmark": "^2.1.4", + "mitata": "^0.1.6", "esbuild": "^0.14.12", - "eventemitter3": "^5.0.0", - "mitata": "^0.1.6" + "@swc/core": "^1.2.133", + "@babel/core": "^7.16.10", + "@babel/preset-react": "^7.16.7" }, "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 differnew file mode 100755 index 000000000..3acb6d075 --- /dev/null +++ b/bench/snippets/bun.lockb diff --git a/bench/snippets/emitter.mjs b/bench/snippets/emitter.mjs new file mode 100644 index 000000000..f708cf77a --- /dev/null +++ b/bench/snippets/emitter.mjs @@ -0,0 +1,101 @@ +// **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 new file mode 100644 index 000000000..6244001a8 --- /dev/null +++ b/bench/snippets/package.json @@ -0,0 +1,7 @@ +{ + "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 9fb6e778b..cbb264486 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` `events.on`. +- Missing `EventEmitterAsyncResource`. `EventEmitter` is missing `{get}set}MaxListeners` `usingDomains` `init`. --- diff --git a/src/bun.js/events.exports.js b/src/bun.js/events.exports.js deleted file mode 100644 index 3e4536ff2..000000000 --- a/src/bun.js/events.exports.js +++ /dev/null @@ -1,464 +0,0 @@ -// 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 127bc85e3..ede5622dc 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1712,15 +1712,7 @@ 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 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:events" => return jsSyntheticModule(.@"node:events"), .@"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 differdeleted file mode 100755 index 3b60656d2..000000000 --- a/test/bun.lockb +++ /dev/null diff --git a/test/js/node/events/event-emitter.test.ts b/test/js/node/events/event-emitter.test.ts index cef309d48..401ccf605 100644 --- a/test/js/node/events/event-emitter.test.ts +++ b/test/js/node/events/event-emitter.test.ts @@ -1,106 +1,34 @@ -import { test, describe, expect } from "bun:test"; -import { sleep } from "bun"; - +import { test, describe, expect, it } from "bun:test"; +import { heapStats } from "bun:jsc"; +import { expectMaxObjectTypeCount, gc } from "harness"; // 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("node:events", () => { - test("captureRejectionSymbol", () => { +describe("EventEmitter", () => { + it("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("constructor", () => { + test("EventEmitter constructor", () => { var emitter = new EventEmitter(); emitter.setMaxListeners(100); expect(emitter.getMaxListeners()).toBe(100); }); - test("removeAllListeners()", () => { - var emitter = new EventEmitter() as any; + test("EventEmitter.removeAllListeners()", () => { + var emitter = new EventEmitter(); 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; }); @@ -109,366 +37,42 @@ describe("EventEmitter", () => { 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()); - + test("EventEmitter emit (different tick)", done => { + var emitter = new EventEmitter(); + emitter.on("wow", () => done()); + queueMicrotask(() => { emitter.emit("wow"); }); - - test("setTimeout task", done => { - var emitter = new EventEmitter(); - emitter.on("wow", () => done()); - setTimeout(() => emitter.emit("wow"), 1); - }); - }); - - 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("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); + // 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"); }); -}); - -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("async EventEmitter emit (microtask) after", async done => { + var emitter = new EventEmitter(); + emitter.on("wow", () => done()); + await 1; + emitter.emit("wow"); }); - 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"); + test("EventEmitter emit (same tick)", done => { + var emitter = new EventEmitter(); - await sleep(5); + emitter.on("wow", () => done()); - expect(handled).toEqual(null); + emitter.emit("wow"); }); - 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); + test("EventEmitter emit (setTimeout task)", done => { + var emitter = new EventEmitter(); + emitter.on("wow", () => done()); + setTimeout(() => emitter.emit("wow"), 1); }); }); @@ -508,30 +112,53 @@ const waysOfCreating = [ }, ]; -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); +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); + 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); - }); - } + expect(firstEvents).toBe(myEmitter._events); + expect(called).toBe(true); + }); +} + +test("EventEmitter.on", () => { + var myEmitter = new EventEmitter(); + expect(myEmitter.on("foo", () => {})).toBe(myEmitter); +}); + +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); }); diff --git a/test/js/node/events/node-builtins.test.js b/test/js/node/events/node-builtins.test.js new file mode 100644 index 000000000..67050f31a --- /dev/null +++ b/test/js/node/events/node-builtins.test.js @@ -0,0 +1,18 @@ +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); + } + }); +}); |