UNPKG

squee

Version:
310 lines (269 loc) 10.8 kB
/** * Removes any instances of an item from an array. * * @template TItem Types of items in the array. * @param array Array of items. * @param item Item to remove. * @returns Whether any number of that item was removed. */ const removeFromArray = <TItem>(array: TItem[], item: TItem): boolean => { let removed = false; for (let i = 0; i < array.length; i += 1) { if (array[i] === item) { removed = true; array.splice(i, 1); i -= 1; } } return removed; }; /** * Creates a new, blank registration. * * @returns A new, blank registration. */ const createNewRegistration = () => ({ firstOnlyListeners: [], listeners: [], }); /** * Listeners and first args registered to events, keyed by event name. * * @template TEvents Types of events supported for registration. */ type IEventRegistrations<TEvents extends string> = { [EventName in TEvents]?: IRegistration | undefined; }; /** * Listeners and first args registered to an event. */ interface IRegistration { /** * Args sent by the first emission, if available yet. */ firstArgs?: any[]; /** * Registered listeners for first emissions */ firstOnlyListeners: IListenerRegistrations; /** * Registered listeners for all emissions. */ listeners: IListenerRegistrations; } /** * Listeners registered to an event. */ type IListenerRegistrations = IListener[]; /** * Listener for an event. * * @param args Any args for the event. */ type IListener = (...args: any[]) => void; /** * Submits application events. * * @template TTypes Event names linked to their arg types. */ export interface IEventSubmitter<TTypes = any> { /** * Emits an event, along with any amount of additional information. * * @param eventName Name of an event. * @param args Any additional information for the event. */ emit<TEventName extends keyof TTypes>(eventName: TEventName, ...args: TTypes[TEventName][]): void; } /** * Receives application events. * * @template TTypes Event names linked to their arg types. */ export interface IEventReceiver<TTypes = any> { /** * Binds an event listener to an event name. * * @param eventName Name of an event. * @param listener Listener for the event. */ on<TEventName extends keyof TTypes>(eventName: TEventName, listener: (...args: TTypes[TEventName][]) => void): void; /** * Binds an event listener to the first time an event name. * * @param eventName Name of an event. * @param listener Listener for the event. * @remarks If the event name was already fired, it's immediately called with the args from the first event. */ onFirst<TEventName extends keyof TTypes>(eventName: TEventName, listener: (...args: TTypes[TEventName][]) => void): void; /** * Binds an event listener to all events. * * @param listener Called on any event with the event name and args. */ onAny(listener: <TEventName extends keyof TTypes>(eventName: TEventName, ...args: TTypes[TEventName][]) => void): void; /** * Removes an event listener from an event name. * If no listener is provided, it removes all listeners for that event name. * If no event name is provided, it removes all listeners for all event names. * * @param eventName Name of an event, if not all events. * @param listener Listener for the event, if not all listeners for the event(s). * @remarks Throws an error if the listener wasn't added for that event name. */ off<TEventName extends keyof TTypes>(eventName?: TEventName, listener?: (...args: TTypes[TEventName][]) => void): void; /** * Creates a Promise to be resolved the next time an event is fired. * * @param eventName Name of an event. * @returns A Promise to be resolved with the first object passed with the event. */ waitFor<TEventName extends keyof TTypes>(eventName: TEventName): Promise<TTypes[TEventName]>; /** * Creates a Promise to be resolved the first time an event is fired. * * @param eventName Name of an event. * @returns A Promise to be resolve with the first object passed with the first event. * @remarks If the event name was already fired, it's immediately resolved with the args from the first event. */ waitForFirst<TEventName extends keyof TTypes>(eventName: TEventName): Promise<TTypes[TEventName]>; } /** * Hub for triggerable application events. * * @template TTypes Event names linked to their arg types. */ export class EventEmitter<TTypes = any> implements IEventSubmitter<TTypes> { /** * Listeners to fire on all events. */ private readonly anyRegistrations: IListener[] = []; /** * Listeners and first args registered to events. */ private registrations: IEventRegistrations<keyof TTypes> = {}; /** * Binds an event listener to an event name. * * @param eventName Name of an event. * @param listener Listener for the event. */ public on<TEventName extends keyof TTypes>(eventName: TEventName, listener: (...args: TTypes[TEventName][]) => void): void { this.safelyGetRegistration(eventName) .listeners.push(listener); } /** * Binds an event listener to the first time an event name. * * @param eventName Name of an event. * @param listener Listener for the event. * @remarks If the event name was already fired, it's immediately called with the args from the first event. */ public onFirst<TEventName extends keyof TTypes>(eventName: TEventName, listener: (...args: TTypes[TEventName][]) => void): void { const registration = this.safelyGetRegistration(eventName); if (registration.firstArgs !== undefined) { listener(...registration.firstArgs); } registration.firstOnlyListeners.push(listener); } /** * Binds an event listener to all events. * * @param listener Called on any event with the event name and args. */ public onAny(listener: <TEventName extends keyof TTypes>(eventName: TEventName, ...args: TTypes[TEventName][]) => void): void { this.anyRegistrations.push(listener); } /** * Removes an event listener from an event name. * If no listener is provided, it removes all listeners for that event name. * If no event name is provided, it removes all listeners for all event names. * * @param eventName Name of an event, if not all events. * @param listener Listener for the event, if not all listeners for the event(s). * @remarks Throws an error if the listener wasn't added for that event name. */ public off<TEventName extends keyof TTypes>(eventName?: TEventName, listener?: (...args: TTypes[TEventName][]) => void): void { if (eventName === undefined) { this.registrations = {}; return; } if (listener === undefined) { this.registrations[eventName] = createNewRegistration(); return; } const registration = this.safelyGetRegistration(eventName); const wasInListeners = removeFromArray(registration.listeners, listener); const wasInFirstOnlyListeners = removeFromArray(registration.firstOnlyListeners, listener); if (!wasInListeners && !wasInFirstOnlyListeners) { throw new Error(`Tried to remove a non-existent listener for event name '${eventName}'.`); } } /** * Emits an event, along with any amount of additional information. * * @param eventName Name of an event. * @param args Any additional information for the event. */ public emit<TEventName extends keyof TTypes>(eventName: TEventName, ...args: TTypes[TEventName][]): void { const registration = this.safelyGetRegistration(eventName); if (registration.firstArgs === undefined) { for (const firstOnlyListener of registration.firstOnlyListeners) { firstOnlyListener(...args); } registration.firstArgs = args; registration.firstOnlyListeners = []; } for (const listener of registration.listeners) { listener(...args); } for (const listener of this.anyRegistrations) { listener(eventName, ...args); } } /** * Creates a Promise to be resolved the next time an event is fired. * * @param eventName Name of an event. * @returns A Promise to be resolved with the first object passed with the event. */ public waitFor<TEventName extends keyof TTypes>(eventName: TEventName): Promise<TTypes[TEventName]> { return new Promise((resolve) => { const listener = (arg: TTypes[TEventName]) => { resolve(arg); this.off(eventName, listener); }; this.on(eventName, listener); }); } /** * Creates a Promise to be resolved the first time an event is fired. * * @param eventName Name of an event. * @returns A Promise to be resolve with the first object passed with the first event. * @remarks If the event name was already fired, it's immediately resolved with the args from the first event. */ public waitForFirst<TEventName extends keyof TTypes>(eventName: TEventName): Promise<TTypes[TEventName]> { return new Promise((resolve) => { const listener = (arg: TTypes[TEventName]) => { resolve(arg); this.off(eventName, listener); }; this.onFirst(eventName, listener); }); } /** * Ensures the registrations object for an event exists. * * @param eventName Name of an event. * @returns Registrations for the event. */ private safelyGetRegistration<TEventName extends keyof TTypes>(eventName: TEventName): IRegistration { if (this.registrations[eventName] === undefined) { this.registrations[eventName] = createNewRegistration(); } // See https://github.com/Microsoft/TypeScript/issues/10530 return this.registrations[eventName] as IRegistration; } } export const Squee = EventEmitter;