Revert "implement node:events in javascript (#2604)"

This reverts commit 96a2ed1040.
This commit is contained in:
Jarred Sumner
2023-04-21 07:16:23 -07:00
parent 143ccdbeb6
commit a4d0a1961a
15 changed files with 218 additions and 1169 deletions

Binary file not shown.

View File

@@ -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);
}
});
}
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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",

BIN
bench/snippets/bun.lockb Executable file

Binary file not shown.

101
bench/snippets/emitter.mjs Normal file
View File

@@ -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();

View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"eventemitter3": "^5.0.0",
"tiny-typed-emitter": "latest"
},
"prettier": "../../.prettierrc.cjs"
}

View File

@@ -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`.
---

View File

@@ -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;

View File

@@ -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"),

Binary file not shown.

View File

@@ -1,474 +1,78 @@
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;
var ran = false;
emitter.on("hey", () => {
ran = true;
});
emitter.on("hey", () => {
ran = true;
});
emitter.on("exit", () => {
ran = true;
});
const { _events } = emitter;
emitter.removeAllListeners();
expect(emitter.listenerCount("hey")).toBe(0);
expect(emitter.listenerCount("exit")).toBe(0);
emitter.emit("hey");
emitter.emit("exit");
expect(ran).toBe(false);
expect(_events).not.toBe(emitter._events); // This looks wrong but node.js replaces it too
emitter.on("hey", () => {
ran = true;
});
emitter.emit("hey");
expect(ran).toBe(true);
expect(emitter.listenerCount("hey")).toBe(1);
});
test("removeAllListeners(type)", () => {
test("EventEmitter.removeAllListeners()", () => {
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");
emitter.removeAllListeners();
expect(emitter.listenerCount("hey")).toBe(0);
expect(emitter.listenerCount("exit")).toBe(1);
emitter.emit("hey");
expect(ran).toBe(false);
emitter.emit("exit");
emitter.on("hey", () => {
ran = true;
});
emitter.emit("hey");
expect(ran).toBe(true);
expect(emitter.listenerCount("hey")).toBe(1);
});
// 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);
// 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 validates function", () => {
var myEmitter = new EventEmitter();
expect(() => myEmitter.addListener("foo", {} as any)).toThrow();
test("async EventEmitter emit (microtask) after", async done => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
await 1;
emitter.emit("wow");
});
test("removeListener return type", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.removeListener("foo", () => {})).toBe(myEmitter);
test("EventEmitter emit (same tick)", done => {
var emitter = new EventEmitter();
emitter.on("wow", () => done());
emitter.emit("wow");
});
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);
});
});
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);
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);
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);
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(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);
});

View File

@@ -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);
}
});
});