squee
Version:
Super quick event emitter!
310 lines (269 loc) • 10.8 kB
text/typescript
/**
* 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;