1import type { Database } from "bun:sqlite";2import { inflateSync } from "node:zlib";3import type { ReleaseRecord } from "../bucket/repo.ts";4import { mirrorState } from "./store.ts";56export type Save = {7 id: string;8 parent: string;9 message: string;10 created_at: number;11 machine: string;12 agent: string;13 session: string;14 is_checkpoint: boolean;15};1617export type FileEntry = { path: string; hash: string; executable: boolean; size: number };181920export type Touches = { line: Save[]; byPath: Map<string, Save[]> };2122export type RepoStats = { saves: number; lines: number; language: string; updated: number | null };2324const SAVE_COLUMNS = `id, COALESCE(parent, '') AS parent, message, created_at, machine,25 COALESCE(agent, '') AS agent, COALESCE(session, '') AS session, is_checkpoint = 1 AS is_checkpoint`;26const LINE_COLUMNS =27 "line.id, line.parent, line.message, line.created_at, line.machine, line.agent, line.session, line.is_checkpoint, line.n";28const ID_PREFIX = /^[0-9a-f]{6,12}$/;293031const NOT_CHECKPOINT = "is_checkpoint = 0";3233export function headId(db: Database): string | null {34 return mirrorState(db, "head");35}363738export function history(db: Database, limit: number, offset: number): Save[] {39 return db40 .query<Save, [number, number]>(41 `SELECT ${SAVE_COLUMNS} FROM saves WHERE ${NOT_CHECKPOINT} ORDER BY rowid DESC LIMIT ? OFFSET ?`,42 )43 .all(limit, offset)44 .map(asSave);45}464748export function timeline(db: Database): Save[] {49 return db50 .query<Save, []>(`SELECT ${SAVE_COLUMNS} FROM saves WHERE ${NOT_CHECKPOINT} ORDER BY rowid`)51 .all()52 .map(asSave);53}545556export function saveByPrefix(db: Database, prefix: string): Save | null {57 if (!ID_PREFIX.test(prefix)) return null;58 const rows = db59 .query<Save, [string]>(`SELECT ${SAVE_COLUMNS} FROM saves WHERE id LIKE ? || '%' LIMIT 2`)60 .all(prefix);61 return rows.length === 1 ? asSave(rows[0] as Save) : null;62}636465export function visibleParent(db: Database, save: Save): Save | null {66 for (let id = save.parent; id !== ""; ) {67 const parent = saveByPrefix(db, id);68 if (!parent?.is_checkpoint) return parent;69 id = parent.parent;70 }71 return null;72}737475export function flagReason(db: Database, saveId: string): string | null {76 const row = db77 .query<{ kind: string; reason: string }, [string]>(78 `SELECT kind, COALESCE(reason, '') AS reason FROM events79 WHERE save_id = ? AND kind IN ('flag', 'clear') ORDER BY created_at DESC, id DESC LIMIT 1`,80 )81 .get(saveId);82 return row?.kind === "flag" ? row.reason : null;83}848586export function stripCount(db: Database): number {87 return db.query<{ n: number }, []>("SELECT COUNT(*) AS n FROM events WHERE kind = 'strip'").get()88 ?.n as number;89}9091export function filesAt(db: Database, saveId: string): FileEntry[] {92 return db93 .query<FileEntry, [string]>(94 `SELECT f.path, f.hash, f.executable = 1 AS executable, o.size FROM save_files f95 JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? ORDER BY f.path`,96 )97 .all(saveId)98 .map((file) => ({ ...file, executable: Boolean(file.executable) }));99}100101export function fileAt(db: Database, saveId: string, path: string): FileEntry | null {102 const file = db103 .query<FileEntry, [string, string]>(104 `SELECT f.path, f.hash, f.executable = 1 AS executable, o.size FROM save_files f105 JOIN objects o ON o.hash = f.hash WHERE f.save_id = ? AND f.path = ?`,106 )107 .get(saveId, path);108 return file ? { ...file, executable: Boolean(file.executable) } : null;109}110111export function objectBytes(db: Database, hash: string): Uint8Array<ArrayBuffer> | null {112 const row = db113 .query<{ data: Uint8Array }, [string]>("SELECT data FROM objects WHERE hash = ?")114 .get(hash);115 return row ? new Uint8Array(inflateSync(row.data)) : null;116}117118export function repoStats(db: Database): RepoStats {119 const row = db120 .query<{ saves: number; updated: number | null }, []>(121 `SELECT COUNT(*) AS saves, MAX(created_at) AS updated FROM saves WHERE ${NOT_CHECKPOINT}`,122 )123 .get() ?? { saves: 0, updated: null };124 return {125 saves: row.saves,126 updated: row.updated,127 lines: Number(mirrorState(db, "lines") ?? 0),128 language: mirrorState(db, "language") ?? "",129 };130}131132133134135136137export function fileTouches(db: Database, saveId: string): Touches {138 const rows = db139 .query<Save & { path: string; n: number }, { at: string }>(140 `WITH line AS (141 SELECT ${SAVE_COLUMNS}, LAG(id) OVER (ORDER BY rowid) AS prev, rowid AS n FROM saves142 WHERE ${NOT_CHECKPOINT} AND rowid <= (SELECT rowid FROM saves WHERE id = $at)143 )144 SELECT ${LINE_COLUMNS}, f.path FROM line JOIN save_files f ON f.save_id = line.id145 LEFT JOIN save_files p ON p.save_id = line.prev AND p.path = f.path146 WHERE p.hash IS NOT f.hash OR p.executable IS NOT f.executable147 UNION ALL148 SELECT ${LINE_COLUMNS}, p.path FROM line JOIN save_files p ON p.save_id = line.prev149 WHERE NOT EXISTS (SELECT 1 FROM save_files f WHERE f.save_id = line.id AND f.path = p.path)150 ORDER BY n`,151 )152 .all({ at: saveId });153 const saves = new Map<string, Save>();154 const byPath = new Map<string, Save[]>();155 for (const { path, n: _, ...row } of rows) {156 const save = saves.get(row.id) ?? asSave(row);157 saves.set(save.id, save);158 const touched = byPath.get(path);159 if (touched) touched.push(save);160 else byPath.set(path, [save]);161 }162 return { line: [...saves.values()], byPath };163}164165166export function releaseRecords(db: Database): ReleaseRecord[] {167 return db168 .query<{ record: string }, []>("SELECT record FROM releases")169 .all()170 .map((row) => JSON.parse(row.record) as ReleaseRecord);171}172173function asSave(row: Save): Save {174 return { ...row, is_checkpoint: Boolean(row.is_checkpoint) };175}