import Redis from 'ioredis'; import { RedisConnectionManager } from './connection-manager'; import { CACHE_DEFAULTS } from './constants'; import type { CacheOptions, CacheProvider, CacheStats } from './types'; /** * Simplified Redis-based cache provider */ export class RedisCache implements CacheProvider { private redis: Redis; private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } = console; private defaultTTL: number; private keyPrefix: string; private stats: CacheStats = { hits: 0, misses: 0, errors: 0, hitRate: 0, total: 0, uptime: 0, }; private startTime = Date.now(); constructor(options: CacheOptions) { this.defaultTTL = options.ttl ?? CACHE_DEFAULTS.TTL; this.keyPrefix = options.keyPrefix ?? CACHE_DEFAULTS.KEY_PREFIX; this.logger = options.logger || console; const manager = RedisConnectionManager.getInstance(); const name = options.name || 'CACHE'; this.redis = manager.getConnection({ name: `${name}-SERVICE`, singleton: options.shared ?? true, redisConfig: options.redisConfig, logger: this.logger, }); } private getKey(key: string): string { return `${this.keyPrefix}${key}`; } private updateStats(hit: boolean, error = false): void { if (error) { this.stats.errors++; } else if (hit) { this.stats.hits++; } else { this.stats.misses++; } this.stats.total = this.stats.hits + this.stats.misses; this.stats.hitRate = this.stats.total > 0 ? this.stats.hits / this.stats.total : 0; this.stats.uptime = Date.now() - this.startTime; } async get(key: string): Promise { try { const value = await this.redis.get(this.getKey(key)); if (value === null) { this.updateStats(false); return null; } this.updateStats(true); return JSON.parse(value); } catch { this.updateStats(false, true); return null; } } async set( key: string, value: T, options?: | number | { ttl?: number; preserveTTL?: boolean; onlyIfExists?: boolean; onlyIfNotExists?: boolean; getOldValue?: boolean; } ): Promise { try { const fullKey = this.getKey(key); const serialized = JSON.stringify(value); const opts = typeof options === 'number' ? { ttl: options } : options || {}; let oldValue: T | null = null; if (opts.getOldValue) { const existing = await this.redis.get(fullKey); if (existing) { oldValue = JSON.parse(existing); } } const ttl = opts.ttl ?? this.defaultTTL; if (opts.onlyIfExists) { const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'XX'); if (!result) { return oldValue; } } else if (opts.onlyIfNotExists) { const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'NX'); if (!result) { return oldValue; } } else if (opts.preserveTTL) { const currentTTL = await this.redis.ttl(fullKey); if (currentTTL > 0) { await this.redis.setex(fullKey, currentTTL, serialized); } else { await this.redis.setex(fullKey, ttl, serialized); } } else { await this.redis.setex(fullKey, ttl, serialized); } return oldValue; } catch (error) { this.updateStats(false, true); throw error; } } async del(key: string): Promise { try { await this.redis.del(this.getKey(key)); } catch (error) { this.updateStats(false, true); throw error; } } async exists(key: string): Promise { try { return (await this.redis.exists(this.getKey(key))) === 1; } catch { this.updateStats(false, true); return false; } } async clear(): Promise { try { const stream = this.redis.scanStream({ match: `${this.keyPrefix}*`, count: CACHE_DEFAULTS.SCAN_COUNT, }); const pipeline = this.redis.pipeline(); stream.on('data', (keys: string[]) => { if (keys.length) { keys.forEach(key => pipeline.del(key)); } }); await new Promise((resolve, reject) => { stream.on('end', resolve); stream.on('error', reject); }); await pipeline.exec(); } catch (error) { this.updateStats(false, true); throw error; } } async keys(pattern: string): Promise { try { const keys: string[] = []; const stream = this.redis.scanStream({ match: `${this.keyPrefix}${pattern}`, count: CACHE_DEFAULTS.SCAN_COUNT, }); await new Promise((resolve, reject) => { stream.on('data', (resultKeys: string[]) => { keys.push(...resultKeys.map(k => k.replace(this.keyPrefix, ''))); }); stream.on('end', resolve); stream.on('error', reject); }); return keys; } catch { this.updateStats(false, true); return []; } } async health(): Promise { try { return (await this.redis.ping()) === 'PONG'; } catch { return false; } } getStats(): CacheStats { return { ...this.stats, uptime: Date.now() - this.startTime, }; } async waitForReady(timeout = 5000): Promise { if (this.redis.status === 'ready') { return; } return new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error(`Redis connection timeout after ${timeout}ms`)); }, timeout); this.redis.once('ready', () => { clearTimeout(timer); resolve(); }); }); } isReady(): boolean { return this.redis.status === 'ready'; } }