/** * Represents an arbitrary event-listener signature (e.g. `(e: KeyboardEvent) => * void`, `(e: MouseEvent) => void`, or a zero-arg handler). The * bivariance-hack method-shorthand form is used instead of a plain function * type so that concretely-typed listeners remain assignable here under * `strictFunctionTypes` — a plain `(...args: unknown[]) => void` alias would * reject any listener with a more specific parameter type. */ export type EventListenerCallback = { bivarianceHack(...args: unknown[]): void; }["bivarianceHack"]; export class EventDispatcher { private _listeners: Record> = {}; constructor(validListeners: string[]) { this._initListeners(validListeners); } public on(event: string, callback: EventListenerCallback, context: object) { const isRegistered = this._isRegisteredEvent(event); const isNotDuplicate = !this._isDuplicate(event, callback); if (isRegistered && isNotDuplicate) { this._listeners[event].push([callback, context]); } else { if (!isRegistered) { this._throwEventError(event); } } } public dispatch(event: string, ...args: unknown[]) { if (this._listeners[event]) { this._executeListeners(event, ...args); } else { this._throwEventError(event); } } public disconnect(event: string, callback: EventListenerCallback): void { const name1 = callback.name; this._listeners[event] = this._listeners[event].filter((listener) => { const [cb] = listener; const name2 = cb.name; if (name2 && name1 !== name2) { return listener; } }); } public disconnectAll(): void { Object.keys(this._listeners).forEach((key) => { this._listeners[key] = []; }); } private _initListeners(listeners: string[]) { listeners.forEach((listener) => { if (!this._listeners[listener]) { this._listeners[listener] = []; } }); } private _executeListeners(event: string, ...args: unknown[]) { this._listeners[event].forEach((listener) => { const [callback, context] = listener; callback.call(context, ...args); }); } private _throwEventError(evt: string): TypeError { const msg = `No ${evt} event registered on this object`; throw new TypeError(msg); } private _isRegisteredEvent(event: string): boolean { return !!this._listeners[event]; } private _isDuplicate( event: string, callback: EventListenerCallback ): boolean { const name1 = callback.name; return this._listeners[event].reduce((bool, ls) => { const [cb] = ls; const name2 = cb.name; if (name1 && name2 && name1 === name2) { return true; } else { return bool; } }, false); } }