@homer0/events-hub
Version:
A simple implementation of a pubsub service for handling events
283 lines • 9.5 kB
JavaScript
// 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