mirror of
https://github.com/denoland/deno.git
synced 2024-12-23 07:44:48 -05:00
90125566bb
Fixes #3324 Co-authored-by: Kitson Kelly <me@kitsonkelly.com>
524 lines
17 KiB
TypeScript
524 lines
17 KiB
TypeScript
// 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";
|
|
import { assert } from "../testing/asserts.ts";
|
|
|
|
export interface WrappedFunction extends Function {
|
|
listener: Function;
|
|
}
|
|
|
|
/**
|
|
* See also https://nodejs.org/api/events.html
|
|
*/
|
|
export default class EventEmitter {
|
|
public static defaultMaxListeners = 10;
|
|
public static errorMonitor = Symbol("events.errorMonitor");
|
|
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)) {
|
|
if (
|
|
eventName === "error" &&
|
|
this._events.get(EventEmitter.errorMonitor)
|
|
) {
|
|
this.emit(EventEmitter.errorMonitor, ...args);
|
|
}
|
|
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") {
|
|
if (this._events.get(EventEmitter.errorMonitor)) {
|
|
this.emit(EventEmitter.errorMonitor, ...args);
|
|
}
|
|
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++) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
unwrappedListeners[i] = (arr[i] as any)["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 as unknown) as WrappedFunction,
|
|
context: this
|
|
};
|
|
const wrapped = (wrapper.bind(
|
|
wrapperContext
|
|
) as unknown) as WrappedFunction;
|
|
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 (eventName && 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>
|
|
| undefined = this._events.get(eventName);
|
|
|
|
assert(arr);
|
|
|
|
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] && (arr[i] as WrappedFunction)["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;
|
|
}
|
|
}
|
|
|
|
export { EventEmitter };
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
});
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function createIterResult(value: any, done: boolean): IteratorResult<any> {
|
|
return { value, done };
|
|
}
|
|
|
|
interface AsyncInterable {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
next(): Promise<IteratorResult<any, any>>;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return(): Promise<IteratorResult<any, any>>;
|
|
throw(err: Error): void;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
[Symbol.asyncIterator](): any;
|
|
}
|
|
|
|
/**
|
|
* Returns an AsyncIterator that iterates eventName events. It will throw if
|
|
* the EventEmitter emits 'error'. It removes all listeners when exiting the
|
|
* loop. The value returned by each iteration is an array composed of the
|
|
* emitted event arguments.
|
|
*/
|
|
export function on(
|
|
emitter: EventEmitter,
|
|
event: string | symbol
|
|
): AsyncInterable {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const unconsumedEventValues: any[] = [];
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const unconsumedPromises: any[] = [];
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let error: Error | null = null;
|
|
let finished = false;
|
|
|
|
const iterator = {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
next(): Promise<IteratorResult<any>> {
|
|
// First, we consume all unread events
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const value: any = unconsumedEventValues.shift();
|
|
if (value) {
|
|
return Promise.resolve(createIterResult(value, false));
|
|
}
|
|
|
|
// Then we error, if an error happened
|
|
// This happens one time if at all, because after 'error'
|
|
// we stop listening
|
|
if (error) {
|
|
const p: Promise<never> = Promise.reject(error);
|
|
// Only the first element errors
|
|
error = null;
|
|
return p;
|
|
}
|
|
|
|
// If the iterator is finished, resolve to done
|
|
if (finished) {
|
|
return Promise.resolve(createIterResult(undefined, true));
|
|
}
|
|
|
|
// Wait until an event happens
|
|
return new Promise(function(resolve, reject) {
|
|
unconsumedPromises.push({ resolve, reject });
|
|
});
|
|
},
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return(): Promise<IteratorResult<any>> {
|
|
emitter.removeListener(event, eventHandler);
|
|
emitter.removeListener("error", errorHandler);
|
|
finished = true;
|
|
|
|
for (const promise of unconsumedPromises) {
|
|
promise.resolve(createIterResult(undefined, true));
|
|
}
|
|
|
|
return Promise.resolve(createIterResult(undefined, true));
|
|
},
|
|
|
|
throw(err: Error): void {
|
|
error = err;
|
|
emitter.removeListener(event, eventHandler);
|
|
emitter.removeListener("error", errorHandler);
|
|
},
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
[Symbol.asyncIterator](): any {
|
|
return this;
|
|
}
|
|
};
|
|
|
|
emitter.on(event, eventHandler);
|
|
emitter.on("error", errorHandler);
|
|
|
|
return iterator;
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function eventHandler(...args: any[]): void {
|
|
const promise = unconsumedPromises.shift();
|
|
if (promise) {
|
|
promise.resolve(createIterResult(args, false));
|
|
} else {
|
|
unconsumedEventValues.push(args);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
function errorHandler(err: any): void {
|
|
finished = true;
|
|
|
|
const toError = unconsumedPromises.shift();
|
|
if (toError) {
|
|
toError.reject(err);
|
|
} else {
|
|
// The next time we call next()
|
|
error = err;
|
|
}
|
|
|
|
iterator.return();
|
|
}
|
|
}
|