399 lines
12 KiB
TypeScript

/**
* Copyright Joyent, Inc. and other Node contributors.
* Modifications copyright (c) Microsoft Corporation.
*
* 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.
*/
type EventType = string | symbol;
type Listener = (...args: any[]) => any;
type EventMap = Record<EventType, Listener | Listener[]>;
import { EventEmitter as OriginalEventEmitter } from 'events';
import type { EventEmitter as EventEmitterType } from 'events';
import { isUnderTest } from '../utils';
export class EventEmitter implements EventEmitterType {
private _events: EventMap | undefined = undefined;
private _eventsCount = 0;
private _maxListeners: number | undefined = undefined;
readonly _pendingHandlers = new Map<EventType, Set<Promise<void>>>();
private _rejectionHandler: ((error: Error) => void) | undefined;
constructor() {
if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) {
this._events = Object.create(null);
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined;
this.on = this.addListener;
this.off = this.removeListener;
}
setMaxListeners(n: number): this {
if (typeof n !== 'number' || n < 0 || Number.isNaN(n))
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
this._maxListeners = n;
return this;
}
getMaxListeners(): number {
return this._maxListeners === undefined ? OriginalEventEmitter.defaultMaxListeners : this._maxListeners;
}
emit(type: EventType, ...args: any[]): boolean {
const events = this._events;
if (events === undefined)
return false;
const handler = events?.[type];
if (handler === undefined)
return false;
if (typeof handler === 'function') {
this._callHandler(type, handler, args);
} else {
const len = handler.length;
const listeners = handler.slice();
for (let i = 0; i < len; ++i)
this._callHandler(type, listeners[i], args);
}
return true;
}
private _callHandler(type: EventType, handler: Listener, args: any[]): void {
const promise = Reflect.apply(handler, this, args);
if (!(promise instanceof Promise))
return;
let set = this._pendingHandlers.get(type);
if (!set) {
set = new Set();
this._pendingHandlers.set(type, set);
}
set.add(promise);
promise.catch(e => {
if (this._rejectionHandler)
this._rejectionHandler(e);
else
throw e;
}).finally(() => set.delete(promise));
}
addListener(type: EventType, listener: Listener): this {
return this._addListener(type, listener, false);
}
on(type: EventType, listener: Listener): this {
return this._addListener(type, listener, false);
}
private _addListener(type: EventType, listener: Listener, prepend: boolean): this {
checkListener(listener);
let events = this._events;
let existing;
if (events === undefined) {
events = this._events = Object.create(null);
this._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener !== undefined) {
this.emit('newListener', type, unwrapListener(listener));
// Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = this._events!;
}
existing = events[type];
}
if (existing === undefined) {
// Optimize the case of one listener. Don't need the extra array object.
existing = events![type] = listener;
++this._eventsCount;
} else {
if (typeof existing === 'function') {
// Adding the second element, need to change to array.
existing = events![type] =
prepend ? [listener, existing] : [existing, listener];
// If we've already got an array, just append.
} else if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
// Check for listener leak
const m = this.getMaxListeners();
if (m > 0 && existing.length > m && !(existing as any).warned) {
(existing as any).warned = true;
// No error code for this since it is a Warning
const w = new Error('Possible EventEmitter memory leak detected. ' +
existing.length + ' ' + String(type) + ' listeners ' +
'added. Use emitter.setMaxListeners() to ' +
'increase limit') as any;
w.name = 'MaxListenersExceededWarning';
w.emitter = this;
w.type = type;
w.count = existing.length;
if (!isUnderTest()) {
// eslint-disable-next-line no-console
console.warn(w);
}
}
}
return this;
}
prependListener(type: EventType, listener: Listener): this {
return this._addListener(type, listener, true);
}
once(type: EventType, listener: Listener): this {
checkListener(listener);
this.on(type, new OnceWrapper(this, type, listener).wrapperFunction);
return this;
}
prependOnceListener(type: EventType, listener: Listener): this {
checkListener(listener);
this.prependListener(type, new OnceWrapper(this, type, listener).wrapperFunction);
return this;
}
removeListener(type: EventType, listener: Listener): this {
checkListener(listener);
const events = this._events;
if (events === undefined)
return this;
const list = events[type];
if (list === undefined)
return this;
if (list === listener || (list as any).listener === listener) {
if (--this._eventsCount === 0) {
this._events = Object.create(null);
} else {
delete events[type];
if (events.removeListener)
this.emit('removeListener', type, (list as any).listener ?? listener);
}
} else if (typeof list !== 'function') {
let position = -1;
let originalListener;
for (let i = list.length - 1; i >= 0; i--) {
if (list[i] === listener || wrappedListener(list[i]) === listener) {
originalListener = wrappedListener(list[i]);
position = i;
break;
}
}
if (position < 0)
return this;
if (position === 0)
list.shift();
else
list.splice(position, 1);
if (list.length === 1)
events[type] = list[0];
if (events.removeListener !== undefined)
this.emit('removeListener', type, originalListener || listener);
}
return this;
}
off(type: EventType, listener: Listener): this {
return this.removeListener(type, listener);
}
removeAllListeners(type?: EventType): this;
removeAllListeners(type: EventType | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>;
removeAllListeners(type?: string, options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): this | Promise<void> {
this._removeAllListeners(type);
if (!options)
return this;
if (options.behavior === 'wait') {
const errors: Error[] = [];
this._rejectionHandler = error => errors.push(error);
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
return this._waitFor(type).then(() => {
if (errors.length)
throw errors[0];
});
}
if (options.behavior === 'ignoreErrors')
this._rejectionHandler = () => {};
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
return Promise.resolve();
}
private _removeAllListeners(type?: string) {
const events = this._events;
if (!events)
return;
// not listening for removeListener, no need to emit
if (!events.removeListener) {
if (type === undefined) {
this._events = Object.create(null);
this._eventsCount = 0;
} else if (events[type] !== undefined) {
if (--this._eventsCount === 0)
this._events = Object.create(null);
else
delete events[type];
}
return;
}
// emit removeListener for all listeners on all events
if (type === undefined) {
const keys = Object.keys(events);
let key;
for (let i = 0; i < keys.length; ++i) {
key = keys[i];
if (key === 'removeListener')
continue;
this._removeAllListeners(key);
}
this._removeAllListeners('removeListener');
this._events = Object.create(null);
this._eventsCount = 0;
return;
}
const listeners = events[type];
if (typeof listeners === 'function') {
this.removeListener(type, listeners);
} else if (listeners !== undefined) {
// LIFO order
for (let i = listeners.length - 1; i >= 0; i--)
this.removeListener(type, listeners[i]);
}
}
listeners(type: EventType): Listener[] {
return this._listeners(this, type, true);
}
rawListeners(type: EventType): Listener[] {
return this._listeners(this, type, false);
}
listenerCount(type: EventType): number {
const events = this._events;
if (events !== undefined) {
const listener = events[type];
if (typeof listener === 'function')
return 1;
if (listener !== undefined)
return listener.length;
}
return 0;
}
eventNames(): Array<string | symbol> {
return this._eventsCount > 0 && this._events ? Reflect.ownKeys(this._events) : [];
}
private async _waitFor(type?: EventType) {
let promises: Promise<void>[] = [];
if (type) {
promises = [...(this._pendingHandlers.get(type) || [])];
} else {
promises = [];
for (const [, pending] of this._pendingHandlers)
promises.push(...pending);
}
await Promise.all(promises);
}
private _listeners(target: EventEmitter, type: EventType, unwrap: boolean): Listener[] {
const events = target._events;
if (events === undefined)
return [];
const listener = events[type];
if (listener === undefined)
return [];
if (typeof listener === 'function')
return unwrap ? [unwrapListener(listener)] : [listener];
return unwrap ? unwrapListeners(listener) : listener.slice();
}
}
function checkListener(listener: any) {
if (typeof listener !== 'function')
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
}
class OnceWrapper {
private _fired = false;
readonly wrapperFunction: (...args: any[]) => Promise<void> | void;
readonly _listener: Listener;
private _eventEmitter: EventEmitter;
private _eventType: EventType;
constructor(eventEmitter: EventEmitter, eventType: EventType, listener: Listener) {
this._eventEmitter = eventEmitter;
this._eventType = eventType;
this._listener = listener;
this.wrapperFunction = this._handle.bind(this);
(this.wrapperFunction as any).listener = listener;
}
private _handle(...args: any[]) {
if (this._fired)
return;
this._fired = true;
this._eventEmitter.removeListener(this._eventType, this.wrapperFunction);
return this._listener.apply(this._eventEmitter, args);
}
}
function unwrapListener(l: Listener): Listener {
return wrappedListener(l) ?? l;
}
function unwrapListeners(arr: Listener[]): Listener[] {
return arr.map(l => wrappedListener(l) ?? l);
}
function wrappedListener(l: Listener): Listener {
return (l as any).listener;
}