UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

121 lines (120 loc) • 3.91 kB
import { acquireStoreDirLock, AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords, } from "./fs-atomic.js"; export class LedgerCore { key; paths; model; table; rows = new Map(); refs = 1; locks = new Map(); events = 0; handle; fence; released = false; constructor(key, paths, model, table) { this.key = key; this.paths = paths; this.model = model; this.table = table; } bootstrap() { this.fence = acquireStoreDirLock(this.paths.dir, { label: this.model.label }); try { const disclose = this.paths.onCorruptRead; const judge = (accepts, what) => ({ accepts, what: `a ${what} carries none of this ${this.model.label}'s own members` }); for (const row of readJsonlRecords(this.paths.snapshotPath, disclose, judge(this.model.judgeRow, "snapshot row"))) { this.rows.set(this.model.keyOf(row), row); } const events = readJsonlRecords(this.paths.ledgerPath, disclose, judge(this.model.judgeEvent, "ledger event")); this.events = events.length; for (const ev of events) this.model.apply(this.rows, ev); this.handle = new AppendLog(this.paths.ledgerPath); } catch (e) { this.fence.release(); this.fence = undefined; throw e; } } withLock(key, fn) { const prev = this.locks.get(key) ?? Promise.resolve(); const run = prev.then(() => fn(), () => fn()); const tail = run.then(() => { }, () => { }); this.locks.set(key, tail); void tail.then(() => { if (this.locks.get(key) === tail) this.locks.delete(key); }); return run; } commit(ev, fsync, compactEvery) { this.log.append(ev, fsync); this.model.apply(this.rows, ev); this.events++; if (this.events >= compactEvery) { try { this.compact(); } catch { } } } compact() { if (this.released) { throw new Error("file ledger: this authority was released (its store was closed) — refusing to compact a directory it no longer owns"); } const lines = [...this.rows.values()].map((r) => JSON.stringify(r)).join("\n"); atomicWriteFile(this.paths.tmpDir, this.paths.snapshotPath, lines.length ? `${lines}\n` : ""); this.log.closeForSwap(); atomicWriteFile(this.paths.tmpDir, this.paths.ledgerPath, ""); this.events = 0; } release() { this.refs -= 1; if (this.refs > 0) return; if (this.table.get(this.key) === this) this.table.delete(this.key); this.released = true; this.handle?.close(); this.fence?.release(); this.fence = undefined; } get pendingEvents() { return this.events; } get log() { if (this.handle === undefined) throw new Error("file ledger: used before bootstrap"); return this.handle; } } export class SharedLedgerTable { model; live = new Map(); constructor(model) { this.model = model; } acquire(paths) { ensureDir(paths.dir); ensureDir(paths.tmpDir); const key = canonicalStoreKey(paths.dir); const existing = this.live.get(key); if (existing !== undefined) { existing.refs += 1; return existing; } const core = new LedgerCore(key, paths, this.model, this.live); this.live.set(key, core); try { core.bootstrap(); } catch (e) { if (this.live.get(key) === core) this.live.delete(key); throw e; } return core; } }