// TODO: You'll probably want to use WeakRef's here. export type EventMap = Record; type Listener = (detail: T) => void; interface ListenerEntry { listener: Listener; wrappedListener: Listener; debounceTime?: number; once?: boolean; } export type OffCallback = () => void; export class EventEmitter { private listeners: Map>> = new Map(); on( type: K, listener: Listener, debounceMilliseconds?: number, ): OffCallback { const wrappedListener = debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(listener, debounceMilliseconds) : listener; if (!this.listeners.has(type)) { this.listeners.set(type, new Set()); } const listenerEntry: ListenerEntry = { listener, wrappedListener, debounceTime: debounceMilliseconds, }; this.listeners.get(type)?.add(listenerEntry as ListenerEntry); // Return an "off" callback that can be called to stop listening for events. return () => this.off(type, listener); } once( type: K, listener: Listener, debounceMilliseconds?: number, ): OffCallback { const wrappedListener: Listener = (detail: T[K]) => { this.off(type, listener); listener(detail); }; const debouncedListener = debounceMilliseconds && debounceMilliseconds > 0 ? this.debounce(wrappedListener, debounceMilliseconds) : wrappedListener; if (!this.listeners.has(type)) { this.listeners.set(type, new Set()); } const listenerEntry: ListenerEntry = { listener, wrappedListener: debouncedListener, debounceTime: debounceMilliseconds, once: true, }; this.listeners.get(type)?.add(listenerEntry as ListenerEntry); // Return an "off" callback that can be called to stop listening for events. return () => this.off(type, listener); } off(type: K, listener: Listener): void { const listeners = this.listeners.get(type); if (!listeners) return; const listenerEntry = Array.from(listeners).find( (entry) => entry.listener === listener || entry.wrappedListener === listener, ); if (listenerEntry) { listeners.delete(listenerEntry); } } emit(type: K, payload: T[K]): boolean { const listeners = this.listeners.get(type); if (!listeners) return false; listeners.forEach((entry) => { entry.wrappedListener(payload); }); return listeners.size > 0; } removeAllListeners(): void { this.listeners.clear(); } async waitFor( type: K, predicate: (payload: T[K]) => boolean, timeoutMs?: number, ): Promise { return new Promise((resolve, reject) => { let timeoutId: ReturnType | undefined; const listener = (payload: T[K]) => { if (predicate(payload)) { // Clean up this.off(type, listener); if (timeoutId !== undefined) { clearTimeout(timeoutId); } resolve(payload); } }; // Set up timeout if specified if (timeoutMs !== undefined) { timeoutId = setTimeout(() => { this.off(type, listener); reject(new Error(`Timeout waiting for event "${String(type)}"`)); }, timeoutMs); } this.on(type, listener); }); } private debounce( func: Listener, wait: number, ): Listener { let timeout: ReturnType; return (detail: T[K]) => { if (timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(() => { func(detail); }, wait); }; } }