// Auteur : Simon-Pierre Boucher — contact@spboucher.ai /** * Cache mémoire simple (par processus) pour les lectures coûteuses de la base * de coûts : aperçu, assemblages, indices. Invalidé par TTL et par la clé * `last_ingest` (cost_meta) mise à jour à chaque ingestion. */ import { metaGet } from "./db"; interface Entry { value: T; expires: number; ingest: string | null } const store = new Map>(); export function cached(key: string, ttlMs: number, compute: () => T): T { let ingest: string | null = null; try { ingest = metaGet("last_ingest"); } catch { /* base indisponible */ } const e = store.get(key) as Entry | undefined; if (e && e.expires > Date.now() && e.ingest === ingest) return e.value; const value = compute(); store.set(key, { value, expires: Date.now() + ttlMs, ingest }); return value; } export function invalidateCache(): void { store.clear(); } /** Limiteur de débit en mémoire (par clé, fenêtre glissante). */ const hits = new Map(); export function rateLimit(key: string, max: number, windowMs: number): boolean { const now = Date.now(); const arr = (hits.get(key) ?? []).filter((t) => now - t < windowMs); if (arr.length >= max) { hits.set(key, arr); return false; } arr.push(now); hits.set(key, arr); return true; } export function clientKey(req: Request): string { const xff = req.headers.get("x-forwarded-for"); return (xff ? xff.split(",").pop()!.trim() : req.headers.get("x-real-ip")) ?? "local"; }