import Redis from 'ioredis'; import type { RedisConfig } from './types'; interface ConnectionConfig { name: string; singleton?: boolean; db?: number; redisConfig: RedisConfig; logger?: any; } /** * Redis Connection Manager for managing shared and unique connections */ export class RedisConnectionManager { private connections = new Map(); private static sharedConnections = new Map(); private static instance: RedisConnectionManager; private logger: any = console; private static readyConnections = new Set(); // Singleton pattern for the manager itself static getInstance(): RedisConnectionManager { if (!this.instance) { this.instance = new RedisConnectionManager(); } return this.instance; } /** * Get or create a Redis connection * @param config Connection configuration * @returns Redis connection instance */ getConnection(config: ConnectionConfig): Redis { const { name, singleton = false, db, redisConfig, logger } = config; if (logger) { this.logger = logger; } if (singleton) { // Use shared connection across all instances if (!RedisConnectionManager.sharedConnections.has(name)) { const connection = this.createConnection(name, redisConfig, db, logger); RedisConnectionManager.sharedConnections.set(name, connection); this.logger.info(`Created shared Redis connection: ${name}`); } const connection = RedisConnectionManager.sharedConnections.get(name); if (!connection) { throw new Error(`Expected connection ${name} to exist in shared connections`); } return connection; } else { // Create unique connection per instance const uniqueName = `${name}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const connection = this.createConnection(uniqueName, redisConfig, db, logger); this.connections.set(uniqueName, connection); this.logger.debug(`Created unique Redis connection: ${uniqueName}`); return connection; } } /** * Create a new Redis connection with configuration */ private createConnection(name: string, config: RedisConfig, db?: number, logger?: any): Redis { const redisOptions = { host: config.host, port: config.port, password: config.password || undefined, username: config.username || undefined, db: db ?? config.db ?? 0, maxRetriesPerRequest: config.maxRetriesPerRequest ?? 3, retryDelayOnFailover: config.retryDelayOnFailover ?? 100, connectTimeout: config.connectTimeout ?? 10000, commandTimeout: config.commandTimeout ?? 5000, keepAlive: config.keepAlive ?? 0, connectionName: name, lazyConnect: false, // Connect immediately instead of waiting for first command ...(config.tls && { tls: { cert: config.tls.cert || undefined, key: config.tls.key || undefined, ca: config.tls.ca || undefined, rejectUnauthorized: config.tls.rejectUnauthorized ?? true, }, }), }; const redis = new Redis(redisOptions); // Use the provided logger or fall back to instance logger const log = logger || this.logger; // Setup event handlers redis.on('connect', () => { log.info(`Redis connection established: ${name}`); }); redis.on('ready', () => { log.info(`Redis connection ready: ${name}`); }); redis.on('error', err => { log.error(`Redis connection error for ${name}:`, err); }); redis.on('close', () => { log.warn(`Redis connection closed: ${name}`); }); redis.on('reconnecting', () => { log.warn(`Redis reconnecting: ${name}`); }); return redis; } /** * Close a specific connection */ async closeConnection(connection: Redis): Promise { try { await connection.quit(); } catch (error) { this.logger.warn('Error closing Redis connection:', error as Error); } } /** * Close all connections managed by this instance */ async closeAllConnections(): Promise { // Close instance-specific connections const instancePromises = Array.from(this.connections.values()).map(conn => this.closeConnection(conn) ); await Promise.all(instancePromises); this.connections.clear(); // Close shared connections (only if this is the last instance) if (RedisConnectionManager.instance === this) { const sharedPromises = Array.from(RedisConnectionManager.sharedConnections.values()).map( conn => this.closeConnection(conn) ); await Promise.all(sharedPromises); RedisConnectionManager.sharedConnections.clear(); } this.logger.info('All Redis connections closed'); } /** * Get connection statistics */ getConnectionCount(): { shared: number; unique: number } { return { shared: RedisConnectionManager.sharedConnections.size, unique: this.connections.size, }; } /** * Get all connection names for monitoring */ getConnectionNames(): { shared: string[]; unique: string[] } { return { shared: Array.from(RedisConnectionManager.sharedConnections.keys()), unique: Array.from(this.connections.keys()), }; } /** * Health check for all connections */ async healthCheck(): Promise<{ healthy: boolean; details: Record }> { const details: Record = {}; let allHealthy = true; // Check shared connections for (const [name, connection] of RedisConnectionManager.sharedConnections) { try { await connection.ping(); details[`shared:${name}`] = true; } catch { details[`shared:${name}`] = false; allHealthy = false; } } // Check instance connections for (const [name, connection] of this.connections) { try { await connection.ping(); details[`unique:${name}`] = true; } catch { details[`unique:${name}`] = false; allHealthy = false; } } return { healthy: allHealthy, details }; } /** * Wait for all created connections to be ready * @param timeout Maximum time to wait in milliseconds * @returns Promise that resolves when all connections are ready */ static async waitForAllConnections(timeout: number = 30000): Promise { const instance = this.getInstance(); const allConnections = new Map([...instance.connections, ...this.sharedConnections]); if (allConnections.size === 0) { instance.logger.debug('No Redis connections to wait for'); return; } instance.logger.info(`Waiting for ${allConnections.size} Redis connections to be ready...`); const connectionPromises = Array.from(allConnections.entries()).map(([name, redis]) => instance.waitForConnection(redis, name, timeout) ); try { await Promise.all(connectionPromises); instance.logger.info('All Redis connections are ready'); } catch (error) { instance.logger.error('Failed to establish all Redis connections:', error); throw error; } } /** * Wait for a specific connection to be ready */ private async waitForConnection(redis: Redis, name: string, timeout: number): Promise { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { reject(new Error(`Redis connection ${name} failed to be ready within ${timeout}ms`)); }, timeout); const onReady = () => { clearTimeout(timeoutId); RedisConnectionManager.readyConnections.add(name); this.logger.debug(`Redis connection ready: ${name}`); resolve(); }; const onError = (err: Error) => { clearTimeout(timeoutId); this.logger.error(`Redis connection failed for ${name}:`, err); reject(err); }; if (redis.status === 'ready') { onReady(); } else { redis.once('ready', onReady); redis.once('error', onError); } }); } /** * Check if all connections are ready */ static areAllConnectionsReady(): boolean { const instance = this.getInstance(); const allConnections = new Map([...instance.connections, ...this.sharedConnections]); return ( allConnections.size > 0 && Array.from(allConnections.keys()).every(name => this.readyConnections.has(name)) ); } } export default RedisConnectionManager;