import EventBusRepositoryInterface, { EventHandler } from './EventBusRepositoryInterface'; export default class EventBusRedisRepository implements EventBusRepositoryInterface { private eventBusConfig; private publishClient; private subscribeClient; private handlers; private connected; private clientsNeedInitialization; private connectPromise; private subscribedPatterns; private subscriptionListeners; private pendingSubscriptions; private drainQueues; private serviceName; private loggerService; /** * Constructor */ constructor(eventBusConfig: any); /** * Initialize Redis clients for pub/sub */ private initializeRedisClients; /** * Setup event listeners for a pair of Redis clients * * The clients are passed explicitly because disconnect() replaces them, and the * listeners of a replaced client must no longer touch the repository state. */ private setupEventListeners; /** * Mark the connection as usable once both clients are ready * * Node-redis restores the subscriptions of a client that reconnected on its own, so the * repository must not subscribe again here. Only the durable cursors need to catch up on * whatever was emitted while the connection was down. Clients replaced by an explicit * disconnect carry no subscription at all, which connect() reconciles separately. */ private handleClientsReady; /** * Connect to Redis * * Resolves only once both clients are usable and every registered pattern is subscribed * again. Connecting is therefore also the point where the subscriptions of the transport * are reconciled with the handlers the application registered. */ connect(): Promise; /** * Bring both clients up */ private connectClients; /** * Subscribe every registered pattern the current subscribe client does not hold yet * * This is a no-op for a client that reconnected on its own, because node-redis restores * its subscriptions and the bookkeeping still lists them. After an explicit disconnect * the clients are replaced and nothing is subscribed, so without this the live channel * stops reaching handlers and durable drains stop being woken up. * * Every pattern is attempted even when one of them fails, and a failed pattern simply * stays unsubscribed, which makes the next connect or reconnect try it again. */ private restoreSubscriptions; /** * Open a single client, or wait for the one that is already opening */ private connectClient; /** * Wait until a client reports readiness, giving up instead of hanging forever */ private waitForClientReady; /** * Wait until Redis is usable */ private waitForConnection; /** * Check if connected to Redis */ isConnected(): boolean; /** * Emit an event with optional data * Saves to Redis List for guaranteed delivery + publishes for real-time subscribers */ emit(event: string, data?: any, ttlMinutes?: number): Promise; /** * Subscribe to an event (real-time only) */ on(eventPattern: string, handler: EventHandler): void; /** * Subscribe to an event with durable delivery (guaranteed delivery) * * Awaiting the returned promise tells the caller that past events were replayed and the * subscription is live, which is what an application wants during startup. Ignoring it * keeps the historical fire and forget behaviour. */ onDurable(eventPattern: string, handler: EventHandler): Promise; /** * Register the durable subscription and replay what the cursor has not seen * * Durable handlers are fed exclusively by the persisted cursor, never directly by * the live channel, so an event is only ever marked as processed after the handler * actually returned. Delivery is at-least-once and handlers must be idempotent. */ private subscribeDurably; /** * Add a handler for an event pattern */ private addHandler; /** * Subscribe to a pattern once Redis is usable */ private subscribeToPattern; /** * Subscribe to a pattern in Redis, joining an attempt that is already running * * Deliberately does not wait for the connection: connecting restores subscriptions * itself, and a subscription waiting for the connect while the connect waits for that * very subscription would deadlock. */ private ensureSubscribed; /** * Perform the Redis subscription */ private subscribeToPatternNow; /** * Get the listener of a pattern, creating it on first use * * The very same function object has to be handed to unsubscribe later, otherwise * node-redis keeps the old listener attached to the channel. */ private getSubscriptionListener; /** * Handle an incoming message of one subscription * * Only the handlers of the pattern whose subscription fired are dispatched. Scanning * every registered pattern here would deliver a message twice whenever two overlapping * patterns are subscribed. */ private handlePatternMessage; /** * Get the durable handlers registered for a pattern */ private getDurableHandlers; /** * Request a drain of a durable pattern * * Wake-ups are coalesced: while a drain runs, further wake-ups only ask it to make one * more pass, so an event emitted mid-drain is never missed and never starts a second * concurrent drain of the same cursor. */ private scheduleDrain; /** * Keep draining while new wake-ups arrive */ private runDrainPasses; /** * Deliver everything the cursor has not seen yet */ private drainPattern; /** * Read the events a durable pattern has not processed yet * * Events are prepended with LPUSH, so index 0 holds the newest one and the cursor counts * how many entries at the tail were already processed. Counting from the tail keeps the * cursor valid even when new events are pushed while this drain is running. */ private readPastEvents; /** * Read where this service left off in one durable key * * Deliberately read from Redis on every drain instead of caching it in the process. A * cached cursor outlives the data it points at: when Redis is restarted without * persistence, flushed, or fails over to an empty replica, the key disappears while the * process keeps counting, and every event until the list grew back would be skipped. */ private readProcessedPosition; /** * Deliver one stored event to every durable handler of the pattern * * Returns false when the cursor must not advance. An unparseable entry can never be * delivered, so it is reported and skipped instead of blocking the cursor forever. */ private deliverPastEvent; /** * Find Redis keys matching durable pattern */ private findDurableKeys; /** * Save processed position to Redis */ private saveProcessedPosition; /** * Create EventPayload from Redis message */ private createEventPayload; /** * Catch up every durable cursor after a reconnect */ private scheduleDurableCatchUp; /** * Unsubscribe from an event */ off(eventPattern: string, handler?: EventHandler): void; /** * Remove a single handler, dropping its subscription when it was the last one */ private removeHandler; /** * Unsubscribe from a pattern in Redis */ private unsubscribeFromPattern; /** * Remove all listeners for an event or all events */ removeAllListeners(eventPattern?: string): void; /** * Get the count of listeners for a specific event */ listenerCount(eventPattern: string): number; /** * Get all event patterns that have listeners */ eventNames(): string[]; /** * Disconnect from Redis */ disconnect(): Promise; /** * Close a single client */ private closeClient; /** * Check whether a whole segment of the pattern is a wildcard * * EventPatternMatcher only treats a complete "*" or "**" segment as a wildcard, so the * transport must use the same rule when choosing between subscribe and pSubscribe. */ private hasSegmentWildcard; /** * Translate a framework pattern into a Redis glob * * The glob is only a broad transport filter, it spans dots where the framework matcher * does not. EventPatternMatcher stays authoritative for both callbacks and scanned keys. */ private toRedisGlob; /** * Build the live channel or channel pattern of an event pattern */ private toLiveChannelPattern; /** * Check if an event matches a pattern */ private matchesPattern; /** * Log info */ private logInfo; /** * Log debug */ private logDebug; /** * Log error */ private logError; /** * Log warning */ private logWarning; }