import { logger } from '@goodgo/logger'; import NodeCache from 'node-cache'; import { getRedisClient } from '../../config/redis.config'; /** * EN: Multi-layer cache implementation (L1: Memory, L2: Redis) * VI: Triển khai cache đa tầng (L1: Bộ nhớ, L2: Redis) */ export class MultiLayerCache { private l1Cache: NodeCache; // In-memory cache private redis: ReturnType | null = null; // Disabled for testing constructor() { // L1: In-memory cache (10MB default, 60s default TTL) this.l1Cache = new NodeCache({ stdTTL: 60, // Default TTL 60 seconds maxKeys: 10000, // Max 10k keys useClones: false, // Better performance }); // EN: Temporarily disable L2 Redis cache to test performance // VI: Tạm thời tắt L2 Redis cache để test hiệu suất // TODO: Re-enable after fixing Redis EPIPE errors // this.redis = getRedisClient(); } /** * EN: Get value from cache (L1 -> L2) * VI: Lấy giá trị từ cache (L1 -> L2) */ async get(key: string): Promise { try { // L1: Check in-memory cache first (< 1ms) const l1Value = this.l1Cache.get(key); if (l1Value !== undefined) { return l1Value; } // L2: Check Redis cache (< 5ms) - only if Redis is enabled if (this.redis) { const l2Value = await this.redis.get(key); if (l2Value) { const parsed = JSON.parse(l2Value) as T; // Warm up L1 cache this.l1Cache.set(key, parsed, 60); // Cache 1 minute in L1 return parsed; } } return null; } catch (error) { logger.error('Multi-layer cache get error', { key, error }); return null; } } /** * EN: Set value in cache (both L1 and L2) * VI: Lưu giá trị vào cache (cả L1 và L2) */ async set(key: string, value: any, ttlSeconds?: number): Promise { try { // L1: Set in-memory cache const l1Ttl = ttlSeconds ? Math.min(ttlSeconds, 300) : 60; // Max 5 min in L1 this.l1Cache.set(key, value, l1Ttl); // EN: Temporarily disable L2 Redis cache to test performance // VI: Tạm thời tắt L2 Redis cache để test hiệu suất // TODO: Re-enable after fixing Redis EPIPE errors /* // L2: Set Redis cache if (this.redis) { const stringValue = JSON.stringify(value); if (ttlSeconds) { await this.redis.setex(key, ttlSeconds, stringValue); } else { await this.redis.set(key, stringValue); } } */ } catch (error) { logger.error('Multi-layer cache set error', { key, error }); // Continue even if Redis fails (L1 still works) } } /** * EN: Get from cache or fetch from source if missing * VI: Lấy từ cache hoặc lấy từ nguồn nếu không có */ async getOrSet( key: string, fetchFn: () => Promise, ttlSeconds: number = 300 ): Promise { const cached = await this.get(key); if (cached !== null) { return cached; } const data = await fetchFn(); await this.set(key, data, ttlSeconds); return data; } /** * EN: Delete from cache (both L1 and L2) * VI: Xóa khỏi cache (cả L1 và L2) */ async del(key: string): Promise { try { // L1: Delete from memory this.l1Cache.del(key); // L2: Delete from Redis if (this.redis) { await this.redis.del(key); } } catch (error) { logger.error('Multi-layer cache del error', { key, error }); } } /** * EN: Delete multiple keys from cache * VI: Xóa nhiều keys khỏi cache */ async delMany(keys: string[]): Promise { try { // L1: Delete from memory keys.forEach(key => this.l1Cache.del(key)); // L2: Delete from Redis if (this.redis && keys.length > 0) { await this.redis.del(...keys); } } catch (error) { logger.error('Multi-layer cache delMany error', { keys, error }); } } /** * EN: Invalidate cache by pattern (for cache invalidation) * VI: Làm mất hiệu lực cache theo pattern */ async invalidatePattern(pattern: string): Promise { try { // L1: Clear all (pattern matching is expensive, so clear all) // In production, you might want to implement pattern matching this.l1Cache.flushAll(); // L2: Scan and delete matching keys in Redis if (this.redis) { const stream = this.redis.scanStream({ match: pattern, count: 100, }); const keys: string[] = []; stream.on('data', (resultKeys: string[]) => { keys.push(...resultKeys); }); const redis = this.redis; // Capture for closure stream.on('end', async () => { if (keys.length > 0) { await redis.del(...keys); } }); } } catch (error) { logger.error('Multi-layer cache invalidatePattern error', { pattern, error }); } } /** * EN: Get cache statistics * VI: Lấy thống kê cache */ getStats() { const l1Stats = this.l1Cache.getStats(); return { l1: { keys: l1Stats.keys, hits: l1Stats.hits, misses: l1Stats.misses, hitRate: l1Stats.hits / (l1Stats.hits + l1Stats.misses) || 0, }, }; } } export const multiLayerCache = new MultiLayerCache();