UNPKG

@homer0/events-hub

Version:

A simple implementation of a pubsub service for handling events

283 lines 9.5 kB
// src/index.ts var EventsHub = class { /** * A dictionary of the events and their listeners. */ events = {}; /** * A dictionary of wrappers that were created for "one time subscriptions". This is * used by the {@link EventsHub.off}: if it doesn't find the subscriber as it is, it * will look for a wrapper and remove it. */ onceWrappers = {}; /** * Gets all the listeners for a specific event. * The list is returned by reference, so it can be modified once obtained. * * @param event The name of the event. */ getSubscribers(event) { if (!this.events[event]) { this.events[event] = []; } return this.events[event]; } /** * Adds a new event listener. * * @param event An event name or a list of them. * @param listener The listener function. * @returns An unsubscribe function to remove the listene(s). * @template ListenerFn The type of the listener function. * @example * * const events = new EventsHub(); * type Listener = (arg0: string) => void; * const unsubscribe = events.on<Listener>('event', (arg0) => { * console.log(`Event received: ${arg0}`); * }); * */ on(event, listener) { const events = Array.isArray(event) ? event : [event]; events.forEach((name) => { const subscribers = this.getSubscribers(name); if (!subscribers.includes(listener)) { subscribers.push(listener); } }); return () => this.off(event, listener); } /** * Adds an event listener that will only be executed once. * * @param event An event name or a list of them. * @param listener The listener function. * @returns An unsubscribe function to remove the listener(s). * @template ListenerFn The type of the listener function. * @example * * const events = new EventsHub(); * type Listener = (arg0: string) => void; * const unsubscribe = events.once<Listener>('event', (arg0) => { * console.log(`Event received: ${arg0}`); * }); * */ once(event, listener) { const events = Array.isArray(event) ? event : [event]; let wrapper = events.reduce((acc, name) => { if (acc) return acc; const onceWrapper = this.onceWrappers[name]; if (Array.isArray(onceWrapper)) { const existing = onceWrapper.find((item) => item.original === listener); if (existing) { return existing.wrapper; } return null; } this.onceWrappers[name] = []; return null; }, null); if (!wrapper) { const newWrapper = (...args) => listener(...args); newWrapper.once = true; wrapper = newWrapper; events.forEach((name) => { this.onceWrappers[name].push({ wrapper, original: listener }); }); } return this.on(event, wrapper); } /** * Removes an event listener. * * @param event An event name or a list of them. * @param listener The listener function. * @returns If `event` was a `string`, it will return whether or not the listener was * found and removed; but if `event` * was an `Array`, it will return a list of boolean values. * @template ListenerFn The type of the listener function. * @example * * const events = new EventsHub(); * const listener = (arg0) => { * console.log(`Event received: ${arg0}`); * }; * events.on('event', listener); // subscribe. * events.off('event', listener); // manually unsubscribe. * */ off(event, listener) { const isArray = Array.isArray(event); const events = isArray ? event : [event]; const result = events.map((name) => { const subscribers = this.getSubscribers(name); const onceSubscribers = this.onceWrappers[name]; let found = false; let index = subscribers.indexOf(listener); if (index > -1) { found = true; if ("once" in listener && onceSubscribers) { const wrapperIndex = onceSubscribers.findIndex( (item) => item.wrapper === listener ); onceSubscribers.splice(wrapperIndex, 1); } subscribers.splice(index, 1); } else if (onceSubscribers) { index = onceSubscribers.findIndex((item) => item.original === listener); if (index > -1) { found = true; const originalIndex = subscribers.indexOf(onceSubscribers[index].original); subscribers.splice(originalIndex, 1); onceSubscribers.splice(index, 1); } } return found; }); return isArray ? result : result[0]; } /** * Emits an event and call all its listeners. * * @param event An event name or a list of them. * @param args A list of parameters to send to the listeners. * @template Args The type of the parameters to send to the listeners. * @example * * const events = new EventsHub(); * events.on('event', (arg0) => { * console.log(`Event received: ${arg0}`); * }); * events.emit('event', 'Hello'); // prints "Event received: Hello" * */ emit(event, ...args) { const toClean = []; const events = Array.isArray(event) ? event : [event]; events.forEach((name) => { this.getSubscribers(name).forEach((subscriber) => { subscriber(...args); if ("once" in subscriber) { toClean.push({ event: name, listener: subscriber }); } }); }); toClean.forEach((info) => this.off(info.event, info.listener)); } /** * Asynchronously reduces a target using an event. It's like emit, but the events * listener return a modified (or not) version of the `target`. * * @param event An event name or a list of them. * @param target The variable to reduce with the reducers/listeners. * @param args A list of parameters to send to the reducers/listeners. * @returns A version of the `target` processed by the listeners. * @template Target The type of the target. * @template Args The type of the parameters to send to the reducers/listeners. * @example * * const events = new EventsHub(); * events.on('event', async (target, arg0) => { * const data = await fetch(`https://api.example.com/${arg0}`); * target.push(data); * return target; * }); * const result = await events.reduce('event', [], 'Hello'); * // result would be a list of data fetched from the API. * */ async reduce(event, target, ...args) { const events = Array.isArray(event) ? event : [event]; const toClean = []; const result = await events.reduce( (eventAcc, name) => eventAcc.then((eventCurrent) => { const subscribers = this.getSubscribers(name); return subscribers.reduce( (subAcc, subscriber) => subAcc.then((subCurrent) => { let useCurrent; if (Array.isArray(subCurrent)) { useCurrent = subCurrent.slice(); } else if (typeof subCurrent === "object") { useCurrent = { ...subCurrent }; } else { useCurrent = subCurrent; } const nextStep = subscriber(...[useCurrent, ...args]); if ("once" in subscriber) { toClean.push({ event: name, listener: subscriber }); } return nextStep; }), Promise.resolve(eventCurrent) ); }), Promise.resolve(target) ); toClean.forEach((info) => this.off(info.event, info.listener)); return result; } /** * Synchronously reduces a target using an event. It's like emit, but the events * listener return a modified (or not) version of the `target`. * * @param event An event name or a list of them. * @param target The variable to reduce with the reducers/listeners. * @param args A list of parameters to send to the reducers/listeners. * @returns A version of the `target` processed by the listeners. * @template Target The type of the target. * @template Args The type of the parameters to send to the reducers/listeners. * @example * * const events = new EventsHub(); * events.on('event', (target, arg0) => { * target.push(arg0); * return target; * }); * events.reduce('event', [], 'Hello'); // returns ['Hello'] * */ reduceSync(event, target, ...args) { const events = Array.isArray(event) ? event : [event]; const toClean = []; const result = events.reduce((eventAcc, name) => { const subscribers = this.getSubscribers(name); return subscribers.reduce((subAcc, subscriber) => { let useCurrent; if (Array.isArray(subAcc)) { useCurrent = subAcc.slice(); } else if (typeof subAcc === "object") { useCurrent = { ...subAcc }; } else { useCurrent = subAcc; } const nextStep = subscriber(...[useCurrent, ...args]); if ("once" in subscriber) { toClean.push({ event: name, listener: subscriber }); } return nextStep; }, eventAcc); }, target); toClean.forEach((info) => this.off(info.event, info.listener)); return result; } }; var eventsHub = () => new EventsHub(); export { EventsHub, eventsHub }; //# sourceMappingURL=index.js.map