360 lines · 14.5 KB
import { DurableObject } from "cloudflare:workers";
import { first, stmt } from "./db";
import { backoffMs, flushIsolating, ISOLATE_AFTER } from "./do-flush";
import { afterHostingFlush } from "./sites/caps";
import { REPO_PACK_BYTES, REPO_STARS } from "./counter-keys";

/** How long increments sit in the object before D1 sees them; reads that must be exact go to the object. */
const FLUSH_DELAY_MS = 10_000;
const RETRY_DELAY_MS = 60_000;
const FLUSH_BATCH = 200;
/** Rows untouched this long are dropped once flushed, so an object only holds its working set. */
const IDLE_ROW_MS = 3 * 24 * 60 * 60 * 1000;

export type CounterKind = "site-usage" | "release-downloads" | "mcp-usage" | "repo-counts";
export type Totals = [number, number, number];
type Row = { key: string; a: number; b: number; c: number; meta: string | null };

async function count(sql: string, ...params: string[]): Promise<number> {
  return (await first<{ value: number }>(sql, ...params))?.value ?? 0;
}

/**
 * Stars and upvotes flush as a recount of their source rows, not the object's total: rows also disappear
 * without passing through the counter (account deletion cascades, access-revocation purges, discussion
 * deletes), so writing the object's total would put a stale number back. Pack bytes have no writer other
 * than this counter, so its absolute total is authoritative; a future recompute must call the object's `reset`.
 */
function repoCountStatement(repoId: string, row: Row): ReturnType<typeof stmt> {
  if (row.key === REPO_STARS)
    return stmt("UPDATE repos SET stars_count = (SELECT COUNT(*) FROM stars WHERE repo_id = ?1) WHERE id = ?1", repoId);
  if (row.key === REPO_PACK_BYTES)
    return stmt(
      `INSERT INTO repo_storage_estimates (repo_id, pack_bytes, updated_at)
       SELECT ?1, ?2, ?3 WHERE EXISTS (SELECT 1 FROM repos WHERE id = ?1)
       ON CONFLICT (repo_id) DO UPDATE SET pack_bytes = excluded.pack_bytes, updated_at = excluded.updated_at`,
      repoId,
      row.a,
      Date.now(),
    );
  const table = row.key.startsWith("d:") ? "discussions" : "discussion_comments";