108 lines · 3.5 KB
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<string, Counts>();
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,