import { z } from "zod"; import type { PerformanceMetrics } from "@/lib/performance-diagnostics"; import { redis } from "@/lib/redis"; const KEY = "cms:performance:v1"; const LIMIT = 200; const RETENTION_MS = 60 * 60 * 1000; const measurement = z.object({ count: z.number().nonnegative(), durationMs: z.number().nonnegative(), errors: z.number().nonnegative(), }); const schema = z.object({ operationId: z.string().max(100), route: z.string().max(300), method: z.string().max(10), status: z.number().int(), at: z.number(), streaming: z.boolean(), metrics: z.object({ durationMs: z.number().nonnegative(), database: measurement, external: measurement, }), }); export interface PerformanceSample { operationId: string; route: string; method: string; status: number; at: number; streaming: boolean; metrics: PerformanceMetrics; } const state = globalThis as typeof globalThis & { cmsPerformance?: { samples: PerformanceSample[]; pending: number }; }; const buffer = state.cmsPerformance ?? { samples: [], pending: 0 }; state.cmsPerformance = buffer; /** Best effort: no connection wait, no unbounded offline queue, no request failure. */ export function recordPerformance(sample: PerformanceSample): void { buffer.samples.unshift(sample); buffer.samples.length = Math.min(buffer.samples.length, LIMIT); if (redis?.status !== "ready" || buffer.pending >= 4) return; buffer.pending++; try { void redis .multi() .lpush(KEY, JSON.stringify(sample)) .ltrim(KEY, 0, LIMIT - 1) .expire(KEY, 3600) .exec() .catch(() => undefined) .finally(() => { buffer.pending--; }); } catch { buffer.pending--; } } export async function readPerformance(): Promise<{ samples: PerformanceSample[]; shared: boolean; }> { const cutoff = Date.now() - RETENTION_MS; if (redis?.status === "ready") { let timer: ReturnType | undefined; try { const rows = await Promise.race([ redis.lrange(KEY, 0, LIMIT - 1), new Promise((_, reject) => { timer = setTimeout( () => reject(new Error("Diagnostic storage timeout")), 1000, ); }), ]); const samples = rows.flatMap((row) => { try { const sample = schema.safeParse(JSON.parse(row)); return sample.success && sample.data.at >= cutoff ? [sample.data] : []; } catch { return []; } }); return { samples, shared: true }; } catch { /* Keep local diagnostics available during a Redis outage. */ } finally { clearTimeout(timer); } } return { samples: buffer.samples.filter((sample) => sample.at >= cutoff), shared: false, }; }