valkey-redis-eventbus
Version:
A lightweight event bus implementation using Redis (Valkey) for scalable inter-process communication in Node.js applications.
162 lines (161 loc) • 5.75 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.EventBus = void 0;
const redis_1 = require("redis");
class EventBus {
/**
* Private constructor for EventBus. Use EventBus.create() to instantiate.
* @param name Name of the EventBus instance
* @param prefix Prefix for Redis channels
* @param pub Redis client for publishing
* @param sub Redis client for subscribing
*/
constructor(name, prefix, pub, sub) {
this._name = name;
this._prefix = prefix;
this._pub = pub;
this._sub = sub;
}
/**
* Initializes the EventBus by subscribing to internal 'ping' events.
*/
async init() {
await this._on("ping", () => {
this._emit("pong", "", true);
}, true);
}
/**
* Registers a callback for a specific event.
* @param event Event name to listen for
* @param callback Function to call when the event is received
*/
async on(event, callback) {
return this._on(event, callback, false);
}
/**
* Internal method to subscribe to an event channel.
* @param event Event name
* @param callback Callback to execute on event
* @param internalCall Whether this is an internal event
*/
async _on(event, callback, internalCall = true) {
if (!internalCall && this.isReservedEventName(event)) {
throw new Error(`Reserved event name ${event} cannot be registered`);
}
const fullChannel = this.getPrefixedChannelName(event);
// Subscribe to the event channel
await this._sub.subscribe(fullChannel, (message) => {
// Already an object
callback(message);
});
}
/**
* Emits an event with the given payload.
* @param event Event name
* @param payload Data to send
*/
emit(event, payload) {
this._emit(event, payload, false);
}
/**
* Internal method to publish an event.
* @param event Event name
* @param payload Data to send
* @param internalCall Whether this is an internal event
*/
_emit(event, payload, internalCall = true) {
if (!internalCall && this.isReservedEventName(event)) {
throw new Error(`Reserved event name ${event} cannot be emitted`);
}
const message = typeof payload === "string" ? payload : JSON.stringify(payload);
this._pub.publish(this.getPrefixedChannelName(event), message);
}
/**
* Pings all EventBus instances and waits for pong responses.
* @param timeout Timeout in ms to wait for responses
* @param minResponseCount Minimum number of responses required
* @returns Promise resolving to true if enough responses are received
*/
ping(timeout = 3000, minResponseCount = 1) {
return new Promise(async (resolve) => {
let responseCount = 0;
const timeoutRef = setTimeout(() => {
//+1 because our instance itself will respond
resolve(responseCount >= minResponseCount + 1);
}, timeout);
await this._on("pong", () => {
responseCount++;
//+1 because our instance itself will respond
if (responseCount >= minResponseCount + 1) {
//Cleat timeout
clearTimeout(timeoutRef);
resolve(true);
}
}, true);
this._emit("ping", "", true);
});
}
/**
* Cleans up the EventBus instance and closes Redis connections.
*/
async destroy() {
await this._sub.unsubscribe();
await this._sub.quit();
await this._pub.quit();
EventBus._eventBusInstances.delete(this._name);
}
/**
* Returns true if both Redis clients are connected.
*/
get connected() {
return this._pub.isOpen && this._sub.isOpen;
}
/**
* Returns the channel name with prefix applied.
* @param channel Channel name
* @returns Prefixed channel name
*/
getPrefixedChannelName(channel) {
return `${this._prefix.length > 0 ? `${this._prefix}:` : ""}${channel}`;
}
/**
* Checks if the event name is reserved (internal use).
* @param event Event name
* @returns True if reserved
*/
isReservedEventName(event) {
return event === "ping" || event === "pong";
}
/**
* Creates a new EventBus instance or returns an existing one.
* @param name Name of the EventBus instance
* @param clientOpts Redis client options
* @returns EventBus instance
*/
static async create(name, clientOpts = {}, prefix = "") {
const existing = EventBus._eventBusInstances.get(name);
if (existing)
return existing;
const channelPrefix = `${prefix !== undefined ? prefix : ""}node-redis-eventbus:${name}`;
const pub = (0, redis_1.createClient)(clientOpts);
const sub = (0, redis_1.createClient)(clientOpts);
await Promise.all([pub.connect(), sub.connect()]);
const instance = new EventBus(name, channelPrefix, pub, sub);
await instance.init();
EventBus._eventBusInstances.set(name, instance);
return instance;
}
/**
* Retrieves an EventBus instance by name.
* @param name Name of the EventBus instance
* @returns EventBus instance
*/
static getByName(name) {
const instance = EventBus._eventBusInstances.get(name);
if (!instance)
throw new Error(`EventBus ${name} not found.`);
return instance;
}
}
exports.EventBus = EventBus;
EventBus._eventBusInstances = new Map();