// Cache abstraction with two implementations: // - memory: per-process Map with TTL expiry (single instance) // - redis: shared GET / SETEX / DEL with JSON values (all instances) // // Values are JSON-serialized. Selection happens once based on REDIS_URL. On any // Redis error the cache behaves as a miss so callers fall back to their source. import { getRedis, isRedisEnabled } from '../redis.js'; export interface Cache { readonly backend: 'memory' | 'redis'; get(key: string): Promise; set(key: string, value: T, ttlSeconds: number): Promise; del(key: string): Promise; } // ==================== Memory implementation ==================== interface Entry { value: unknown; expiresAt: number; } class MemoryCache implements Cache { readonly backend = 'memory' as const; private store = new Map(); constructor() { const cleanup = setInterval(() => { const now = Date.now(); for (const [key, entry] of this.store) { if (now > entry.expiresAt) this.store.delete(key); } }, 60_000); (cleanup as any).unref?.(); } async get(key: string): Promise { const entry = this.store.get(key); if (!entry) return null; if (Date.now() > entry.expiresAt) { this.store.delete(key); return null; } return entry.value as T; } async set(key: string, value: T, ttlSeconds: number): Promise { this.store.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 }); } async del(key: string): Promise { this.store.delete(key); } } // ==================== Redis implementation ==================== class RedisCache implements Cache { readonly backend = 'redis' as const; async get(key: string): Promise { const redis = getRedis(); if (!redis) return null; try { const raw = await redis.get(`cache:${key}`); if (raw === null) return null; return JSON.parse(raw) as T; } catch (err: any) { console.error('[cache] redis get error:', err?.message || err); return null; } } async set(key: string, value: T, ttlSeconds: number): Promise { const redis = getRedis(); if (!redis) return; try { await redis.set(`cache:${key}`, JSON.stringify(value), 'EX', ttlSeconds); } catch (err: any) { console.error('[cache] redis set error:', err?.message || err); } } async del(key: string): Promise { const redis = getRedis(); if (!redis) return; try { await redis.del(`cache:${key}`); } catch (err: any) { console.error('[cache] redis del error:', err?.message || err); } } } // ==================== Selection ==================== let instance: Cache | null = null; export function getCache(): Cache { if (!instance) { instance = isRedisEnabled() ? new RedisCache() : new MemoryCache(); } return instance; }