aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar dave caruso <me@paperdave.net> 2023-04-18 17:59:51 -0400
committerGravatar GitHub <noreply@github.com> 2023-04-18 14:59:51 -0700
commit96a2ed1040d5a0ca51ae41267cba4f8e5d0a6142 (patch)
tree5af89ada3aff9044bcdca75dbf0101bc4515f964 /src
parent76deb51c294126406c8f0be5f13bb184c1ddb454 (diff)
downloadbun-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
Diffstat (limited to 'src')
-rw-r--r--src/bun.js/events.exports.js464
-rw-r--r--src/bun.js/module_loader.zig10
2 files changed, 473 insertions, 1 deletions
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"),