@homer0/events-hub
Version:
A simple implementation of a pubsub service for handling events
309 lines (308 loc) • 10.5 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
EventsHub: () => EventsHub,
eventsHub: () => eventsHub
});
module.exports = __toCommonJS(index_exports);
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();
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
EventsHub,
eventsHub
});
//# sourceMappingURL=index.js.map