492 lines · 18.8 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";
import { forwardFlushed } from "./counters-forward";

/** 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"
  | "platform-ai"
  | "platform-sites";
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;
}