ga-pubsub
Version:
It establishes data communication within and between the application or systems using event-driven architecture
238 lines (187 loc) • 6.41 kB
JavaScript
/**
* GA-PubSub - Multi-tenant Event System
* Supports isolated namespaces (tenant-based event buses)
*/
class EventingManagerService {
constructor(options = {}) {
this.eventRegistry = new Map();
this.history = new Map();
this.counter = 0;
this.options = {
replayLimit: options.replayLimit || 1,
historyTTL: options.historyTTL || 0,
enableWildcard: options.enableWildcard ?? true,
};
}
generate(prefix = "id") {
this.counter += 1;
return `${prefix}_${this.counter}`;
}
subscribe(eventName, callback, options = {}) {
if (typeof callback !== "function") {
throw new Error("Callback must be a function");
}
const id = this.generate();
const subscriber = {
id,
callback,
once: options.once || false,
};
if (!this.eventRegistry.has(eventName)) {
this.eventRegistry.set(eventName, new Map());
}
this.eventRegistry.get(eventName).set(id, subscriber);
// replay support
if (options.replay !== false) {
const historyData = this.getHistory(eventName);
if (historyData.length > 0) {
historyData.forEach((item) => {
try {
callback(item.data);
} catch (err) {
console.error(`[PubSub] Replay error for "${eventName}":`, err);
}
});
}
}
return { id, eventName };
}
subscribeOnce(eventName, callback, options = {}) {
return this.subscribe(eventName, callback, {
...options,
once: true,
});
}
async publish(eventName, data) {
this.storeHistory(eventName, data);
const listeners = [...this.getMatchingSubscribers(eventName)];
if (listeners.length === 0) return;
await Promise.allSettled(
listeners.map(async ({ subscriber, subscribedEvent }) => {
try {
if (subscriber.once) {
this.unsubscribe(subscribedEvent, subscriber.id);
}
await Promise.resolve(subscriber.callback(data));
} catch (err) {
console.error(`[PubSub] Error in "${eventName}" subscriber:`, err);
}
})
);
}
getMatchingSubscribers(eventName) {
const listeners = [];
for (const [registeredEvent, subscribers] of this.eventRegistry.entries()) {
const isExact = registeredEvent === eventName;
const isWildcard =
this.options.enableWildcard &&
registeredEvent.endsWith("*") &&
eventName.startsWith(registeredEvent.slice(0, -1));
if (isExact || isWildcard) {
for (const subscriber of subscribers.values()) {
listeners.push({
subscribedEvent: registeredEvent,
subscriber,
});
}
}
}
return listeners;
}
storeHistory(eventName, data) {
const now = Date.now();
if (!this.history.has(eventName)) {
this.history.set(eventName, []);
}
const historyItems = this.history.get(eventName);
historyItems.push({ data, timestamp: now });
if (historyItems.length > this.options.replayLimit) {
historyItems.shift();
}
}
getHistory(eventName) {
if (!this.history.has(eventName)) return [];
const items = this.history.get(eventName);
if (this.options.historyTTL > 0) {
const now = Date.now();
const validItems = items.filter(
(item) => now - item.timestamp <= this.options.historyTTL
);
this.history.set(eventName, validItems);
return validItems;
}
return items;
}
unsubscribe(eventName, id) {
const subscribers = this.eventRegistry.get(eventName);
if (!subscribers) return false;
const removed = subscribers.delete(id);
if (subscribers.size === 0) {
this.eventRegistry.delete(eventName);
}
return removed;
}
unsubscribeEvent(eventName) {
this.eventRegistry.delete(eventName);
this.history.delete(eventName);
}
unsubscribeAll() {
this.eventRegistry.clear();
this.history.clear();
}
getSubscriberCount(eventName) {
return this.eventRegistry.get(eventName)?.size || 0;
}
getEvents() {
return [...this.eventRegistry.keys()];
}
destroy() {
this.unsubscribeAll();
}
}
/**
* -----------------------------
* 🥉 MULTI-TENANT REGISTRY
* -----------------------------
*/
const registry = new Map();
/**
* Get isolated event bus per namespace
*/
function getEventingManagerInstance(namespace = "default", options = {}) {
if (!registry.has(namespace)) {
registry.set(namespace, new EventingManagerService(options));
}
return registry.get(namespace);
}
/**
* Reset a single namespace (useful for tests)
*/
function resetNamespace(namespace) {
registry.delete(namespace);
}
/**
* Reset everything (dev/test only)
*/
function resetAll() {
registry.clear();
}
/**
* -----------------------------
* UMD EXPORT
* -----------------------------
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(["exports"], factory);
} else if (typeof exports === "object" && typeof exports.nodeName !== "string") {
factory(exports);
} else {
factory((root.gaPubSub = {}));
}
})(typeof self !== "undefined" ? self : this, function (exports) {
exports.EventingManagerService = EventingManagerService;
exports.getEventingManagerInstance = getEventingManagerInstance;
exports.resetNamespace = resetNamespace;
exports.resetAll = resetAll;
});