import "server-only"; import { redis } from "@/lib/redis"; type CacheEntry = { data: T; expiresAt: number }; const memory = new Map>(); /** Drop a key from the in-process cache (used when an upstream value changes). */ export function invalidateMemory(key: string): void { memory.delete(key); } /** * Redis-first cached query with an in-memory fallback. * Use for read-heavy endpoints polled by the browser (online count, etc.). */ export async function cached( key: string, ttlMs: number, fn: () => Promise, ): Promise { const ttlSec = Math.ceil(ttlMs / 1000); // In-memory fast path — served first so repeated reads within a TTL window // don't each pay a Redis round-trip (Redis is still the shared fallback). // `existing &&` short-circuits so Date.now() is never evaluated during // prerender when the map is empty (keeps `next build` prerendering clean). const existing = memory.get(key); if (existing && existing.expiresAt > Date.now()) { return existing.data as T; } // Redis path (shared across instances). if (redis && redis.status !== "end") { try { const cached = await redis.get(key); if (cached !== null && cached !== undefined) { const data = JSON.parse(cached) as T; memory.set(key, { data, expiresAt: Date.now() + ttlMs }); return data; } } catch { /* fall through to fn */ } } const data = await fn(); if (redis && redis.status !== "end") { try { await redis.setex(key, ttlSec, JSON.stringify(data)); } catch { /* non-critical: memory cache still works */ } } memory.set(key, { data, expiresAt: Date.now() + ttlMs }); return data; }