summaryrefslogtreecommitdiff
path: root/std/node/events.ts
diff options
context:
space:
mode:
Diffstat (limited to 'std/node/events.ts')
-rw-r--r--std/node/events.ts383
1 files changed, 383 insertions, 0 deletions
diff --git a/std/node/events.ts b/std/node/events.ts
new file mode 100644
index 000000000..f035da2fc
--- /dev/null
+++ b/std/node/events.ts
@@ -0,0 +1,383 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+// Copyright (c) 2019 Denolibs authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { validateIntegerRange } from "./util.ts";
+
+export interface WrappedFunction extends Function {
+ listener: Function;
+}
+
+/**
+ * See also https://nodejs.org/api/events.html
+ */
+export default class EventEmitter {
+ public static defaultMaxListeners = 10;
+ private maxListeners: number | undefined;
+ private _events: Map<string | symbol, Array<Function | WrappedFunction>>;
+
+ public constructor() {
+ this._events = new Map();
+ }
+
+ private _addListener(
+ eventName: string | symbol,
+ listener: Function | WrappedFunction,
+ prepend: boolean
+ ): this {
+ this.emit("newListener", eventName, listener);
+ if (this._events.has(eventName)) {
+ const listeners = this._events.get(eventName) as Array<
+ Function | WrappedFunction
+ >;
+ if (prepend) {
+ listeners.unshift(listener);
+ } else {
+ listeners.push(listener);
+ }
+ } else {
+ this._events.set(eventName, [listener]);
+ }
+ const max = this.getMaxListeners();
+ if (max > 0 && this.listenerCount(eventName) > max) {
+ const warning = new Error(
+ `Possible EventEmitter memory leak detected.
+ ${this.listenerCount(eventName)} ${eventName.toString()} listeners.
+ Use emitter.setMaxListeners() to increase limit`
+ );
+ warning.name = "MaxListenersExceededWarning";
+ console.warn(warning);
+ }
+
+ return this;
+ }
+
+ /** Alias for emitter.on(eventName, listener). */
+ public addListener(
+ eventName: string | symbol,
+ listener: Function | WrappedFunction
+ ): this {
+ return this._addListener(eventName, listener, false);
+ }
+
+ /**
+ * Synchronously calls each of the listeners registered for the event named
+ * eventName, in the order they were registered, passing the supplied
+ * arguments to each.
+ * @return true if the event had listeners, false otherwise
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public emit(eventName: string | symbol, ...args: any[]): boolean {
+ if (this._events.has(eventName)) {
+ const listeners = (this._events.get(eventName) as Function[]).slice(); // We copy with slice() so array is not mutated during emit
+ for (const listener of listeners) {
+ try {
+ listener.apply(this, args);
+ } catch (err) {
+ this.emit("error", err);
+ }
+ }
+ return true;
+ } else if (eventName === "error") {
+ const errMsg = args.length > 0 ? args[0] : Error("Unhandled error.");
+ throw errMsg;
+ }
+ return false;
+ }
+
+ /**
+ * Returns an array listing the events for which the emitter has
+ * registered listeners.
+ */
+ public eventNames(): [string | symbol] {
+ return Array.from(this._events.keys()) as [string | symbol];
+ }
+
+ /**
+ * Returns the current max listener value for the EventEmitter which is
+ * either set by emitter.setMaxListeners(n) or defaults to
+ * EventEmitter.defaultMaxListeners.
+ */
+ public getMaxListeners(): number {
+ return this.maxListeners || EventEmitter.defaultMaxListeners;
+ }
+
+ /**
+ * Returns the number of listeners listening to the event named
+ * eventName.
+ */
+ public listenerCount(eventName: string | symbol): number {
+ if (this._events.has(eventName)) {
+ return (this._events.get(eventName) as Function[]).length;
+ } else {
+ return 0;
+ }
+ }
+
+ private _listeners(
+ target: EventEmitter,
+ eventName: string | symbol,
+ unwrap: boolean
+ ): Function[] {
+ if (!target._events.has(eventName)) {
+ return [];
+ }
+ const eventListeners: Function[] = target._events.get(
+ eventName
+ ) as Function[];
+
+ return unwrap
+ ? this.unwrapListeners(eventListeners)
+ : eventListeners.slice(0);
+ }
+
+ private unwrapListeners(arr: Function[]): Function[] {
+ const unwrappedListeners: Function[] = new Array(arr.length) as Function[];
+ for (let i = 0; i < arr.length; i++) {
+ unwrappedListeners[i] = arr[i]["listener"] || arr[i];
+ }
+ return unwrappedListeners;
+ }
+
+ /** Returns a copy of the array of listeners for the event named eventName.*/
+ public listeners(eventName: string | symbol): Function[] {
+ return this._listeners(this, eventName, true);
+ }
+
+ /**
+ * Returns a copy of the array of listeners for the event named eventName,
+ * including any wrappers (such as those created by .once()).
+ */
+ public rawListeners(
+ eventName: string | symbol
+ ): Array<Function | WrappedFunction> {
+ return this._listeners(this, eventName, false);
+ }
+
+ /** Alias for emitter.removeListener(). */
+ public off(eventName: string | symbol, listener: Function): this {
+ return this.removeListener(eventName, listener);
+ }
+
+ /**
+ * Adds the listener function to the end of the listeners array for the event
+ * named eventName. No checks are made to see if the listener has already
+ * been added. Multiple calls passing the same combination of eventName and
+ * listener will result in the listener being added, and called, multiple
+ * times.
+ */
+ public on(
+ eventName: string | symbol,
+ listener: Function | WrappedFunction
+ ): this {
+ return this.addListener(eventName, listener);
+ }
+
+ /**
+ * Adds a one-time listener function for the event named eventName. The next
+ * time eventName is triggered, this listener is removed and then invoked.
+ */
+ public once(eventName: string | symbol, listener: Function): this {
+ const wrapped: WrappedFunction = this.onceWrap(eventName, listener);
+ this.on(eventName, wrapped);
+ return this;
+ }
+
+ // Wrapped function that calls EventEmitter.removeListener(eventName, self) on execution.
+ private onceWrap(
+ eventName: string | symbol,
+ listener: Function
+ ): WrappedFunction {
+ const wrapper = function(
+ this: {
+ eventName: string | symbol;
+ listener: Function;
+ rawListener: Function;
+ context: EventEmitter;
+ },
+ ...args: any[] // eslint-disable-line @typescript-eslint/no-explicit-any
+ ): void {
+ this.context.removeListener(this.eventName, this.rawListener);
+ this.listener.apply(this.context, args);
+ };
+ const wrapperContext = {
+ eventName: eventName,
+ listener: listener,
+ rawListener: wrapper,
+ context: this
+ };
+ const wrapped = wrapper.bind(wrapperContext);
+ wrapperContext.rawListener = wrapped;
+ wrapped.listener = listener;
+ return wrapped as WrappedFunction;
+ }
+
+ /**
+ * Adds the listener function to the beginning of the listeners array for the
+ * event named eventName. No checks are made to see if the listener has
+ * already been added. Multiple calls passing the same combination of
+ * eventName and listener will result in the listener being added, and
+ * called, multiple times.
+ */
+ public prependListener(
+ eventName: string | symbol,
+ listener: Function | WrappedFunction
+ ): this {
+ return this._addListener(eventName, listener, true);
+ }
+
+ /**
+ * Adds a one-time listener function for the event named eventName to the
+ * beginning of the listeners array. The next time eventName is triggered,
+ * this listener is removed, and then invoked.
+ */
+ public prependOnceListener(
+ eventName: string | symbol,
+ listener: Function
+ ): this {
+ const wrapped: WrappedFunction = this.onceWrap(eventName, listener);
+ this.prependListener(eventName, wrapped);
+ return this;
+ }
+
+ /** Removes all listeners, or those of the specified eventName. */
+ public removeAllListeners(eventName?: string | symbol): this {
+ if (this._events === undefined) {
+ return this;
+ }
+
+ if (this._events.has(eventName)) {
+ const listeners = (this._events.get(eventName) as Array<
+ Function | WrappedFunction
+ >).slice(); // Create a copy; We use it AFTER it's deleted.
+ this._events.delete(eventName);
+ for (const listener of listeners) {
+ this.emit("removeListener", eventName, listener);
+ }
+ } else {
+ const eventList: [string | symbol] = this.eventNames();
+ eventList.map((value: string | symbol) => {
+ this.removeAllListeners(value);
+ });
+ }
+
+ return this;
+ }
+
+ /**
+ * Removes the specified listener from the listener array for the event
+ * named eventName.
+ */
+ public removeListener(eventName: string | symbol, listener: Function): this {
+ if (this._events.has(eventName)) {
+ const arr: Array<Function | WrappedFunction> = this._events.get(
+ eventName
+ );
+
+ let listenerIndex = -1;
+ for (let i = arr.length - 1; i >= 0; i--) {
+ // arr[i]["listener"] is the reference to the listener inside a bound 'once' wrapper
+ if (arr[i] == listener || arr[i]["listener"] == listener) {
+ listenerIndex = i;
+ break;
+ }
+ }
+
+ if (listenerIndex >= 0) {
+ arr.splice(listenerIndex, 1);
+ this.emit("removeListener", eventName, listener);
+ if (arr.length === 0) {
+ this._events.delete(eventName);
+ }
+ }
+ }
+ return this;
+ }
+
+ /**
+ * By default EventEmitters will print a warning if more than 10 listeners
+ * are added for a particular event. This is a useful default that helps
+ * finding memory leaks. Obviously, not all events should be limited to just
+ * 10 listeners. The emitter.setMaxListeners() method allows the limit to be
+ * modified for this specific EventEmitter instance. The value can be set to
+ * Infinity (or 0) to indicate an unlimited number of listeners.
+ */
+ public setMaxListeners(n: number): this {
+ validateIntegerRange(n, "maxListeners", 0);
+ this.maxListeners = n;
+ return this;
+ }
+}
+
+/**
+ * Creates a Promise that is fulfilled when the EventEmitter emits the given
+ * event or that is rejected when the EventEmitter emits 'error'. The Promise
+ * will resolve with an array of all the arguments emitted to the given event.
+ */
+export function once(
+ emitter: EventEmitter | EventTarget,
+ name: string
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): Promise<any[]> {
+ return new Promise((resolve, reject) => {
+ if (emitter instanceof EventTarget) {
+ // EventTarget does not have `error` event semantics like Node
+ // EventEmitters, we do not listen to `error` events here.
+ emitter.addEventListener(
+ name,
+ (...args) => {
+ resolve(args);
+ },
+ { once: true, passive: false, capture: false }
+ );
+ return;
+ } else if (emitter instanceof EventEmitter) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const eventListener = (...args: any[]): void => {
+ if (errorListener !== undefined) {
+ emitter.removeListener("error", errorListener);
+ }
+ resolve(args);
+ };
+ let errorListener: Function;
+
+ // Adding an error listener is not optional because
+ // if an error is thrown on an event emitter we cannot
+ // guarantee that the actual event we are waiting will
+ // be fired. The result could be a silent way to create
+ // memory or file descriptor leaks, which is something
+ // we should avoid.
+ if (name !== "error") {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ errorListener = (err: any): void => {
+ emitter.removeListener(name, eventListener);
+ reject(err);
+ };
+
+ emitter.once("error", errorListener);
+ }
+
+ emitter.once(name, eventListener);
+ return;
+ }
+ });
+}