@sema-agent/core
Version:
Stateless, task-oriented AI agent core
121 lines (120 loc) • 3.91 kB
JavaScript
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;
}
}