import { NextResponse } from "next/server"; type CacheStatus = "HIT" | "MISS" | "STALE" | "FALLBACK"; type PublicCacheEntry = { data: T; expiresAt: number; staleUntil: number; }; const PUBLIC_API_CACHE_KEY = Symbol.for("ai-chatbot.public-api-cache"); function getStore() { const globalCache = globalThis as typeof globalThis & { [PUBLIC_API_CACHE_KEY]?: Map>; }; if (!globalCache[PUBLIC_API_CACHE_KEY]) { globalCache[PUBLIC_API_CACHE_KEY] = new Map>(); } return globalCache[PUBLIC_API_CACHE_KEY]!; } export function getEnvInt( name: string, fallback: number, min: number, max: number ) { const value = Number.parseInt(process.env[name] || "", 10); if (!Number.isFinite(value)) { return fallback; } return Math.min(max, Math.max(min, value)); } export function withTimeout( promise: Promise, timeoutMs: number, label = "operation" ) { let timer: ReturnType | undefined; const timeout = new Promise((_, reject) => { timer = setTimeout(() => { reject(new Error(`${label} timed out after ${timeoutMs}ms`)); }, timeoutMs); }); return Promise.race([promise, timeout]).finally(() => { if (timer) { clearTimeout(timer); } }); } export function publicJson( data: unknown, { cacheStatus, cacheControl = "private, max-age=30, stale-while-revalidate=120", fallback = false, startedAt, status, }: { cacheStatus: CacheStatus; cacheControl?: string; fallback?: boolean; startedAt: number; status?: number; } ) { const durationMs = Math.max(0, Date.now() - startedAt); const response = NextResponse.json(data, status ? { status } : undefined); response.headers.set("Cache-Control", cacheControl); response.headers.set("X-Cache", cacheStatus); response.headers.set("X-Fallback", fallback ? "1" : "0"); response.headers.set("Server-Timing", `app;dur=${durationMs}`); return response; } export async function cachedPublicJson>({ cacheKey, fallbackData, load, timeoutMs = 1800, ttlMs = 30_000, staleMs = 5 * 60_000, cacheControl, }: { cacheKey: string; fallbackData: T; load: () => Promise; timeoutMs?: number; ttlMs?: number; staleMs?: number; cacheControl?: string; }) { const startedAt = Date.now(); const store = getStore(); const cached = store.get(cacheKey); if (cached && cached.expiresAt > startedAt) { return publicJson(cached.data, { cacheStatus: "HIT", cacheControl, startedAt, }); } try { const data = await withTimeout(load(), timeoutMs, cacheKey); store.set(cacheKey, { data, expiresAt: Date.now() + ttlMs, staleUntil: Date.now() + ttlMs + staleMs, }); return publicJson(data, { cacheStatus: "MISS", cacheControl, startedAt, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.warn(`[Public API] ${cacheKey} fallback: ${message}`); if (cached && cached.staleUntil > Date.now()) { return publicJson(cached.data, { cacheStatus: "STALE", cacheControl, fallback: true, startedAt, }); } return publicJson(fallbackData, { cacheStatus: "FALLBACK", cacheControl, fallback: true, startedAt, }); } }