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" | "pages-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;
}
function repoCountStatement(repoId: string, row: Row): ReturnType<typeof stmt> {
if (row.key === REPO_STARS) return stmt("UPDATE repos SET stars_count = ? WHERE id = ?", row.a, 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";
return stmt(`UPDATE ${table} SET upvotes = ? WHERE id = ?`, row.a, row.key.slice(2));
}
type Kind = {
/** Current D1 totals for a key the object has not seen yet, so taking over a live counter loses nothing. */
seed(scope: string, key: string): Promise<Totals>;
/** Writes absolute totals, so a retried flush is idempotent. */