import { redis } from '../redis.js'; export interface CacheOptions { ttlSeconds: number; graceSeconds?: number; } export async function cached(key: string, ttlSeconds: number | CacheOptions, fetcher: () => Promise): Promise { const opts: CacheOptions = typeof ttlSeconds === 'number' ? { ttlSeconds } : ttlSeconds; const graceSeconds = opts.graceSeconds ?? Math.max(Math.floor(opts.ttlSeconds * 0.25), 30); const totalTtl = opts.ttlSeconds + graceSeconds; const hit = await redis.get(key); if (hit !== null) { try { const parsed = JSON.parse(hit) as T; const ttlRemaining = await redis.ttl(key); if (ttlRemaining >= 0 && ttlRemaining <= graceSeconds) { fetcher() .then((fresh) => { if (fresh !== undefined) { redis.set(key, JSON.stringify(fresh), 'EX', totalTtl).catch(() => {}); } }) .catch(() => {}); } return parsed; } catch { await redis.del(key); } } const fresh = await fetcher(); if (fresh !== undefined) { await redis.set(key, JSON.stringify(fresh), 'EX', totalTtl); } return fresh; } export async function bust(pattern: string): Promise { let cursor = '0'; let removed = 0; do { const [next, batch] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100); cursor = next; if (batch.length > 0) { removed += await redis.del(...batch); } } while (cursor !== '0'); return removed; }