trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
105 lines (103 loc) • 3.33 kB
JavaScript
// src/vcs/idb-op-log.ts
var IdbOpLog = class {
ops = [];
hashes = /* @__PURE__ */ new Set();
db = null;
nextSeq = 0;
pendingWrites = Promise.resolve();
dbName;
storeName;
factory;
constructor(opts) {
this.dbName = opts.dbName;
this.storeName = opts.storeName ?? "ops";
const factory = opts.indexedDB ?? globalThis.indexedDB;
if (!factory) {
throw new Error(
"IdbOpLog requires IndexedDB. Pass `indexedDB` in options for non-browser hosts."
);
}
this.factory = factory;
}
async load() {
this.db = await this.openDb();
const records = await this.readAllRecords();
records.sort((a, b) => a.seq - b.seq);
this.ops = records.map((r) => r.op);
this.hashes = new Set(this.ops.map((op) => op.hash));
this.nextSeq = records.length > 0 ? records[records.length - 1].seq + 1 : 0;
}
append(op) {
if (this.hashes.has(op.hash)) return;
if (!this.db) {
throw new Error("IdbOpLog.append() called before load(). Await load() first.");
}
this.ops.push(op);
this.hashes.add(op.hash);
const seq = this.nextSeq++;
const record = { hash: op.hash, seq, op };
this.pendingWrites = this.pendingWrites.then(() => this.putRecord(record));
}
readAll() {
return [...this.ops];
}
getLastOp() {
return this.ops.length > 0 ? this.ops[this.ops.length - 1] : void 0;
}
count() {
return this.ops.length;
}
async flush() {
await this.pendingWrites;
}
async close() {
await this.flush();
this.db?.close();
this.db = null;
}
// --- private ---------------------------------------------------------------
openDb() {
return new Promise((resolve, reject) => {
const request = this.factory.open(this.dbName, 1);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: "hash" });
store.createIndex("seq", "seq", { unique: true });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error ?? new Error("IndexedDB open failed."));
request.onblocked = () => reject(new Error(`IDB open blocked for database '${this.dbName}'.`));
});
}
readAllRecords() {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error("IdbOpLog: database not open."));
return;
}
const tx = this.db.transaction(this.storeName, "readonly");
const store = tx.objectStore(this.storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result ?? []);
request.onerror = () => reject(request.error ?? new Error("IdbOpLog.getAll() failed."));
});
}
putRecord(record) {
return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error("IdbOpLog: database not open."));
return;
}
const tx = this.db.transaction(this.storeName, "readwrite");
const store = tx.objectStore(this.storeName);
const request = store.put(record);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error ?? new Error("IdbOpLog.put() failed."));
});
}
};
export {
IdbOpLog
};