/** * Generic Valkey (Redis-compatible) cache. * * Reuses the same ioredis singleton from presignCache.ts but exposes a * simple typed get/set/invalidate API for arbitrary JSON values. * * Usage: * const books = await cache.get('books:all'); * await cache.set('books:all', books, 5 * 60); * await cache.invalidate('books:all'); */ import Redis from 'ioredis'; let _client: Redis | null = null; function client(): Redis { if (!_client) { const url = process.env.VALKEY_URL ?? 'redis://valkey:6379'; _client = new Redis(url, { lazyConnect: false, enableOfflineQueue: true, maxRetriesPerRequest: 1, connectTimeout: 1500 }); _client.on('error', (err: Error) => { console.error('[cache] Valkey error:', err.message); }); } return _client; } /** Return the cached value for key, or null if absent / expired / error. */ export async function get(key: string): Promise { try { const raw = await client().get(key); if (!raw) return null; return JSON.parse(raw) as T; } catch { return null; } } /** * Store a value under key for ttlSeconds seconds. * Silently no-ops on Valkey errors so callers never crash. */ export async function set(key: string, value: T, ttlSeconds: number): Promise { try { await client().set(key, JSON.stringify(value), 'EX', ttlSeconds); } catch { // non-fatal } } /** Delete a key immediately (e.g. after a write that invalidates it). */ export async function invalidate(key: string): Promise { try { await client().del(key); } catch { // non-fatal } } /** Invalidate all keys matching a glob pattern (e.g. 'books:*'). */ export async function invalidatePattern(pattern: string): Promise { try { const keys = await client().keys(pattern); if (keys.length > 0) await client().del(...keys); } catch { // non-fatal } }