import { appendFileSync, existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { DatabaseSync } from "node:sqlite"; import type { ChildProcess } from "node:child_process"; import { sleep } from "./study.ts"; // 账本里没有时间戳,墙钟时间由检验自己记:研究进程跑着的时候每隔一会儿看一眼账本, // 新出现的每条记录记下"第一次看到它的时刻",写成 timeline.jsonl(每行 {seq, kind, at},at 是毫秒)。 // 只读打开账本、只查序号和种类,不经 Ledger.records()(那会每次重读、重查全部内容)。 // 时刻比真实写入晚最多一个间隔,用来算阶段级的墙钟足够。 const POLL_MS = 1000; export interface TimelineRow { seq: number; kind: string; at: number; } /** 已经记下的最大序号;文件还没有就是 0。接着跑时往同一个文件后面追加。 */ function lastSeq(file: string): number { if (!existsSync(file)) return 0; return readTimeline(file).reduce((m, r) => Math.max(m, r.seq), 0); } /** 研究进程退出前一直记;进程退出后再看最后一眼。调用前要先等 waitForLedger(表建好了才能读)。 */ export async function watchTimeline(studyDir: string, child: ChildProcess, file: string): Promise { let seen = lastSeq(file); const look = () => { const db = new DatabaseSync(join(studyDir, "ledger.sqlite"), { readOnly: true }); try { const rows = db.prepare("SELECT seq, kind FROM records WHERE seq > ? ORDER BY seq").all(seen) as Array<{ seq: number; kind: string }>; const at = Date.now(); for (const r of rows) appendFileSync(file, `${JSON.stringify({ seq: r.seq, kind: r.kind, at })}\n`); if (rows.length > 0) seen = rows.at(-1)!.seq; } finally { db.close(); } }; while (child.exitCode === null && child.signalCode === null) { look(); await sleep(POLL_MS); } look(); } export function readTimeline(file: string): TimelineRow[] { return readFileSync(file, "utf8") .split("\n") .filter((l) => l !== "") .map((l) => JSON.parse(l) as TimelineRow); }