Skip to content

Commit

Permalink
chore: use soft event emitter (#31868)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman authored Jul 30, 2024
1 parent 7e7319d commit 58b0c76
Show file tree
Hide file tree
Showing 21 changed files with 1,775 additions and 1 deletion.
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/channelOwner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { EventEmitter } from 'events';
import { EventEmitter } from './eventEmitter';
import type * as channels from '@protocol/channels';
import { maybeFindValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
import { debugLogger } from '../utils/debugLogger';
Expand Down
345 changes: 345 additions & 0 deletions packages/playwright-core/src/client/eventEmitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
/**
* 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[]) => void;
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;

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') {
Reflect.apply(handler, this, args);
} else {
const len = handler.length;
const listeners = handler.slice();
for (let i = 0; i < len; ++i)
Reflect.apply(listeners[i], this, args);
}

return true;
}

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?: string): this {
const events = this._events;
if (!events)
return this;

// 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 this;
}

// 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 this;
}

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

return this;
}

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 _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[]) => 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;
}
Loading

0 comments on commit 58b0c76

Please sign in to comment.