457 lines · 17.7 KB
import { DurableObject } from "cloudflare:workers";
import { batch, first, stmt as d1Stmt } from "./db";
import { backoffMs, ISOLATE_AFTER } from "./do-flush";
import { accountDb, repoDb, stmt, type ScopedDb, type ScopedStatement } from "./scoped-db";
import { locate } from "./scoped-db/locators";
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 };

/**
 * Pending writes of one flush, grouped by store: D1 directory statements (star counts) and statements for one
 * repository's or account's object. Each group is its own transaction.
 */
type Writes = { d1: D1PreparedStatement[]; scoped: Array<{ db: ScopedDb; statements: ScopedStatement[] }> };

async function count(read: Promise<{ value: number } | null>): Promise<number> {
  return (await read)?.value ?? 0;
}

async function repoExists(repoId: string): Promise<boolean> {
  return (await first<{ ok: number }>("SELECT 1 AS ok FROM repos WHERE id = ?", repoId)) !== null;
}

/**
 * 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`.
 */
async function repoCountWrites(repoId: string, rows: Row[]): Promise<Writes> {