import { createLogger } from '@stock-bot/logger'; import { CacheProvider, CacheOptions, CacheStats } from '../types'; import { RedisCache } from './redis-cache'; import { MemoryCache } from './memory-cache'; /** * Hybrid cache provider that uses memory as L1 cache and Redis as L2 cache * Provides the best of both worlds: fast memory access and persistent Redis storage */ export class HybridCache implements CacheProvider { private memoryCache: MemoryCache; private redisCache: RedisCache; private logger = createLogger('hybrid-cache'); private enableMetrics: boolean; private startTime = Date.now(); private stats: CacheStats = { hits: 0, misses: 0, errors: 0, hitRate: 0, total: 0, uptime: 0 }; constructor(options: CacheOptions = {}) { this.enableMetrics = options.enableMetrics ?? true; // Create L1 (memory) cache with shorter TTL this.memoryCache = new MemoryCache({ ...options, ttl: options.memoryTTL ?? 300, // 5 minutes for memory maxMemoryItems: options.maxMemoryItems ?? 1000, enableMetrics: false // We'll handle metrics at hybrid level }); // Create L2 (Redis) cache with longer TTL this.redisCache = new RedisCache({ ...options, enableMetrics: false // We'll handle metrics at hybrid level }); this.logger.info('Hybrid cache initialized', { memoryTTL: options.memoryTTL ?? 300, redisTTL: options.ttl ?? 3600, maxMemoryItems: options.maxMemoryItems ?? 1000 }); } private updateStats(hit: boolean, error = false): void { if (!this.enableMetrics) return; 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 { // Try L1 cache first (memory) const memoryValue = await this.memoryCache.get(key); if (memoryValue !== null) { this.updateStats(true); this.logger.debug('L1 cache hit', { key, hitRate: this.stats.hitRate }); return memoryValue; } // Try L2 cache (Redis) const redisValue = await this.redisCache.get(key); if (redisValue !== null) { // Populate L1 cache for next access await this.memoryCache.set(key, redisValue); this.updateStats(true); this.logger.debug('L2 cache hit, populating L1', { key, hitRate: this.stats.hitRate }); return redisValue; } // Complete miss this.updateStats(false); this.logger.debug('Cache miss (both L1 and L2)', { key }); return null; } catch (error) { this.updateStats(false, true); this.logger.error('Hybrid cache get error', { key, error: error instanceof Error ? error.message : String(error) }); return null; } } async set(key: string, value: T, ttl?: number): Promise { try { // Set in both caches const memoryPromise = this.memoryCache.set(key, value, Math.min(ttl ?? 300, 300)); const redisPromise = this.redisCache.set(key, value, ttl); await Promise.allSettled([memoryPromise, redisPromise]); this.logger.debug('Cache set (both L1 and L2)', { key, ttl }); } catch (error) { this.updateStats(false, true); this.logger.error('Hybrid cache set error', { key, error: error instanceof Error ? error.message : String(error) }); } } async del(key: string): Promise { try { // Delete from both caches const memoryPromise = this.memoryCache.del(key); const redisPromise = this.redisCache.del(key); await Promise.allSettled([memoryPromise, redisPromise]); this.logger.debug('Cache delete (both L1 and L2)', { key }); } catch (error) { this.updateStats(false, true); this.logger.error('Hybrid cache delete error', { key, error: error instanceof Error ? error.message : String(error) }); } } async exists(key: string): Promise { try { // Check memory first, then Redis const memoryExists = await this.memoryCache.exists(key); if (memoryExists) return true; return await this.redisCache.exists(key); } catch (error) { this.updateStats(false, true); this.logger.error('Hybrid cache exists error', { key, error: error instanceof Error ? error.message : String(error) }); return false; } } async clear(): Promise { try { // Clear both caches const memoryPromise = this.memoryCache.clear(); const redisPromise = this.redisCache.clear(); await Promise.allSettled([memoryPromise, redisPromise]); this.logger.info('Cache cleared (both L1 and L2)'); } catch (error) { this.updateStats(false, true); this.logger.error('Hybrid cache clear error', { error: error instanceof Error ? error.message : String(error) }); } } async health(): Promise { try { const memoryHealthy = await this.memoryCache.health(); const redisHealthy = await this.redisCache.health(); // Hybrid cache is healthy if at least one cache is working const isHealthy = memoryHealthy || redisHealthy; this.logger.debug('Hybrid cache health check', { memory: memoryHealthy, redis: redisHealthy, overall: isHealthy }); return isHealthy; } catch (error) { this.logger.error('Hybrid cache health check failed', { error }); return false; } } getStats(): CacheStats { return { ...this.stats, uptime: Date.now() - this.startTime }; } /** * Get detailed stats for both cache layers */ getDetailedStats() { return { hybrid: this.getStats(), memory: this.memoryCache.getStats(), redis: this.redisCache.getStats() }; } /** * Warm up the memory cache with frequently accessed keys from Redis */ async warmupMemoryCache(keys: string[]): Promise { this.logger.info('Starting memory cache warmup', { keyCount: keys.length }); let warmed = 0; for (const key of keys) { try { const value = await this.redisCache.get(key); if (value !== null) { await this.memoryCache.set(key, value); warmed++; } } catch (error) { this.logger.warn('Failed to warm up key', { key, error }); } } this.logger.info('Memory cache warmup completed', { requested: keys.length, warmed }); } /** * Sync memory cache with Redis for specific keys */ async syncCaches(keys: string[]): Promise { for (const key of keys) { try { const redisValue = await this.redisCache.get(key); if (redisValue !== null) { await this.memoryCache.set(key, redisValue); } else { await this.memoryCache.del(key); } } catch (error) { this.logger.warn('Failed to sync key', { key, error }); } } } /** * Close connections for both caches */ async disconnect(): Promise { await this.redisCache.disconnect(); this.logger.info('Hybrid cache disconnected'); } }