1import { parseManifest } from "../../shared/mod.ts";2import type { Env } from "./env.ts";34// An R2 object is live iff it is referenced by the current manifest or any5// retained history entry of any non-deleted site. The 24h age guard means an6// in-flight push (blobs uploaded, commit pending) can never lose objects to a7// concurrent sweep.8const MIN_AGE_MS = 24 * 3600 * 1000;910export async function runGc(env: Env): Promise<{ expiredSites: number; deletedObjects: number }> {11 const now = Math.floor(Date.now() / 1000);1213 // 1. Sweep expired sites (cascades to pushes history).14 const expired = await env.DB.prepare(15 "DELETE FROM sites WHERE expires_at IS NOT NULL AND expires_at < ? RETURNING address",16 ).bind(now).all<{ address: number }>();1718 // 2. Mark: every manifest referenced by any remaining site or history row.19 const liveManifests = new Set<string>();20 const current = await env.DB.prepare("SELECT current_manifest FROM sites")21 .all<{ current_manifest: string }>();22 for (const r of current.results) liveManifests.add(r.current_manifest);23 const historic = await env.DB.prepare("SELECT DISTINCT manifest_hash FROM pushes")24 .all<{ manifest_hash: string }>();25 for (const r of historic.results) liveManifests.add(r.manifest_hash);2627 // Live blobs = union of files across live manifests.28 const liveBlobs = new Set<string>();29 for (const hash of liveManifests) {30 const obj = await env.CONTENT.get(`manifest/${hash}`);31 if (!obj) continue;32 const manifest = parseManifest(new Uint8Array(await obj.arrayBuffer()));33 for (const f of Object.values(manifest.files)) liveBlobs.add(f.h);34 }3536 // 3. Sweep R2: delete unreferenced objects older than the age guard.37 let deleted = 0;38 const cutoff = Date.now() - MIN_AGE_MS;39 for (const prefix of ["manifest/", "blob/"] as const) {40 let cursor: string | undefined;41 do {42 const page: R2Objects = await env.CONTENT.list({ prefix, cursor, limit: 1000 });43 const doomed = page.objects.filter((o) => {44 if (o.uploaded.getTime() > cutoff) return false;45 const hash = o.key.slice(prefix.length);46 return prefix === "manifest/" ? !liveManifests.has(hash) : !liveBlobs.has(hash);47 }).map((o) => o.key);48 if (doomed.length > 0) {49 await env.CONTENT.delete(doomed);50 deleted += doomed.length;51 }52 cursor = page.truncated ? page.cursor : undefined;53 } while (cursor);54 }5556 return { expiredSites: expired.results.length, deletedObjects: deleted };57}