import type { DisposerFunction } from '../dispose-types' import { getGlobalContext } from '../global' import { DefaultLogger } from '../log/log' import { safeTimeout } from '../timeout' export type EmitterHandler = (...objs: any[]) => void export type EmitterAllHandler = (key: T, ...objs: any[]) => void // For magic see https://www.npmjs.com/package/tiny-typed-emitter / License MIT // https://stackoverflow.com/a/61609010/140927 // https://basarat.gitbook.io/typescript/main-1/typed-event // https://github.com/andywer/typed-emitter#extending-an-emitter // TODO: Allow symbols? https://github.com/sindresorhus/emittery export declare type ListenerSignature = { [E in keyof L]: (...args: any[]) => any } export declare interface DefaultListener { [k: string]: (...args: any[]) => any } interface EmitterSubscriber { fn: EmitterHandler // (...args: any[]) => any priority: number } export interface EmitterSubscriberOptions { priority?: number } export class Emitter< RemoteListener extends ListenerSignature = DefaultListener, LocalListener extends ListenerSignature = DefaultListener, > { private subscribers: Record = {} private subscribersOnAny: any[] = [] _logEmitter = DefaultLogger('zeed:emitter', 'warn') /** RPC like emitting of events. */ call: RemoteListener = new Proxy({} as any, { get: (target: any, name: any) => async (...args: any): Promise => await this.emit(name, ...args), }) /** * Emits an event to all subscribers and executes their corresponding event handlers. * * @param event - The event to emit. * @param args - The arguments to pass to the event handlers. * @returns A promise that resolves to an array of results from all subscribers, or undefined if no subscribers are present. */ public async getAll(event: U, ...args: Parameters): Promise[] | undefined> { try { const subscribers = (this.subscribers[event] || []) as EmitterSubscriber[] this._logEmitter.debug('emit', this?.constructor?.name, event, ...args, subscribers) this.subscribersOnAny.forEach(fn => fn(event, ...args)) if (subscribers.length > 0) { const all = subscribers.map(({ fn }) => { try { return Promise.resolve(fn(...args)) } catch (err) { this._logEmitter.warn('emit warning:', err) } return null }).filter(fn => fn != null) return (await Promise.all(all)) as any[] } } catch (err) { this._logEmitter.error('emit exception', err) } return undefined } /** * Emits an event to all subscribers and executes their corresponding event handlers. * * @param event - The event to emit. * @param args - The arguments to pass to the event handlers. * @returns A promise that resolves to the result of the first subscriber's handler, or undefined if no subscribers are present. */ public async get(event: U, ...args: Parameters): Promise | undefined> { const results = await this.getAll(event, ...args) if (results && results.length > 0) { return results[0] // Return first result } return undefined } /** * Emits an event to all subscribers and executes their corresponding event handlers. * * @param event - The event to emit. * @param args - The arguments to pass to the event handlers. * @returns A promise that resolves to a boolean indicating whether the event was successfully emitted. */ public async emit(event: U, ...args: Parameters): Promise { const result = await this.getAll(event, ...args) return result != null } public onAny(fn: EmitterHandler) { this.subscribersOnAny.push(fn) } public on( event: U, fn: LocalListener[U], opt: EmitterSubscriberOptions = {}, ): DisposerFunction { const { priority = 0 } = opt const subscribers = (this.subscribers[event] || []) const slen = subscribers.length const sobj = { fn, priority } if (slen <= 0) { this.subscribers[event] = [sobj] } else { let pos = slen for (let i = subscribers.length - 1; i >= 0; i--) { const s = subscribers[i] // Insert after last entry of same priority if (priority <= s.priority) break pos -= 1 } subscribers.splice(pos, 0, sobj) // in place } return () => { this.off(event, fn) } } public onCall(handlers: Partial) { for (const [name, handler] of Object.entries(handlers)) this.on(name as any, handler as any) } public once( event: U, listener: LocalListener[U], ): DisposerFunction { const onceListener = async (...args: any[]) => { this.off(event, onceListener as any) return await Promise.resolve(listener(...args)) } this.on(event, onceListener as any) return () => { this.off(event, listener) } } public off( event: U, listener: LocalListener[U], ): this { // log("off", key) this.subscribers[event] = (this.subscribers[event] || []).filter(f => listener && f.fn !== listener) return this } public removeAllListeners(): this { this.subscribers = {} return this } public hasListeners(event: keyof LocalListener): boolean { return !!this.subscribers[event] && this.subscribers[event].length > 0 } /// waitOn[0]>( event: U, timeoutMS = 1000, ): Promise { return new Promise((resolve, reject) => { let disposeTimer: any const disposeListener = this.once(event, ((value): void => { disposeTimer?.() resolve(value) }) as LocalListener[U]) disposeTimer = safeTimeout(() => { disposeListener() reject(new Error('Did not respond in time')) }, timeoutMS) }) } // For compatibility reasons: provide explicitly typed wrapper methods so // documentation generator (TypeDoc) can resolve signatures/URLs correctly. public addEventListener( event: U, fn: LocalListener[U], opt: EmitterSubscriberOptions = {}, ): DisposerFunction { return this.on(event, fn, opt) } public removeEventListener( event: U, listener: LocalListener[U], ): this { return this.off(event, listener) } } declare global { interface ZeedGlobalContext { emitter?: Emitter } /** * Global emitter interface for Zeed, used for cross-module event emission. * @category Global */ interface ZeedGlobalEmitter {} } /** Global emitter that will listen even across modules */ export function getGlobalEmitter = ZeedGlobalEmitter>(): Emitter { let emitter = getGlobalContext().emitter if (!emitter) { emitter = new Emitter() getGlobalContext().emitter = emitter } return emitter as any } // Public alias so typedoc lists the global emitter under an Emitter-prefixed name export interface ZeedGlobalEmitter extends DefaultListener {}