UNPKG

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
// 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 };