import { waitUntil } from "cloudflare:workers"; import { batch, stmt } from "../db"; import { randomId } from "../crypto"; const HOUR_MS = 60 * 60 * 1000; const FLUSH_INTERVAL_MS = 30_000; const MAX_BUFFERED_OPERATIONS = 50; const FAILURE_RETENTION_MS = 30 * 24 * HOUR_MS; const STATS_RETENTION_MS = 90 * 24 * HOUR_MS; const MAX_MESSAGE_CHARS = 500; const MAX_TARGET_CHARS = 200; /** Operations that embed a source URL or repo name are grouped by their verb. */ const PARAMETERISED = /^(import|fork) /; type Counts = { calls: number; failures: number }; /** Per-isolate counters, written to D1 on a later request at most every FLUSH_INTERVAL_MS (approximate by design). */ const pending = new Map(); let lastFlush = Date.now(); let lastPrune = 0; function operationKey(operation: string): string { const match = PARAMETERISED.exec(operation); return match ? match[1] : operation; } function hourOf(time: number): number { return Math.floor(time / HOUR_MS) * HOUR_MS; } function statements(now: number): D1PreparedStatement[] { const hour = hourOf(now); const out = [...pending].map(([operation, counts]) => stmt( `INSERT INTO artifacts_call_stats (hour, operation, calls, failures) VALUES (?, ?, ?, ?) ON CONFLICT (hour, operation) DO UPDATE SET calls = calls + excluded.calls, failures = failures + excluded.failures`, hour, operation, counts.calls, counts.failures, ), ); pending.clear(); lastFlush = now; return out; } function schedule(work: D1PreparedStatement[]): void { if (work.length === 0) return; try { waitUntil(batch(work).catch((error: unknown) => console.error("Artifacts health write failed", error))); } catch (error) { console.error("Artifacts health write failed", error); } } function bump(operation: string, failed: boolean): void { const key = operationKey(operation); const counts = pending.get(key) ?? { calls: 0, failures: 0 }; counts.calls++; if (failed) counts.failures++; pending.set(key, counts); } /** Counts a finished Artifacts call; failures are counted by `recordArtifactsFailure`. */ export function recordArtifactsCall(operation: string): void { bump(operation, false); const now = Date.now(); if (now - lastFlush >= FLUSH_INTERVAL_MS || pending.size >= MAX_BUFFERED_OPERATIONS) schedule(statements(now)); } /** Retention deletes, at most once per flush interval per isolate so an outage does not multiply D1 writes. */ function prune(now: number): D1PreparedStatement[] { if (now - lastPrune < FLUSH_INTERVAL_MS) return []; lastPrune = now; return [ stmt("DELETE FROM artifacts_failures WHERE created_at < ?", now - FAILURE_RETENTION_MS), stmt("DELETE FROM artifacts_call_stats WHERE hour < ?", hourOf(now - STATS_RETENTION_MS)), ]; } /** Logs an untyped Artifacts failure (with its reference id, if any) for the site admin dashboard. */ export function recordArtifactsFailure(input: { operation: string; target: string; reference: string | null; message: string; attempts: number; }): void { bump(input.operation, true); const now = Date.now(); schedule([ ...statements(now), stmt( `INSERT INTO artifacts_failures (id, operation, target, reference, message, attempts, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, randomId(), operationKey(input.operation), input.target.slice(0, MAX_TARGET_CHARS), input.reference, input.message.slice(0, MAX_MESSAGE_CHARS), input.attempts, now, ), ...prune(now), ]); }