UNPKG

@ai2070/l0

Version:

L0: The Missing Reliability Substrate for AI

341 lines 11.2 kB
import { InMemoryEventStore } from "./eventStore"; const adapterRegistry = new Map(); export function registerStorageAdapter(type, factory) { adapterRegistry.set(type, factory); } export function unregisterStorageAdapter(type) { return adapterRegistry.delete(type); } export function getRegisteredAdapters() { return Array.from(adapterRegistry.keys()); } export async function createEventStore(config) { const factory = adapterRegistry.get(config.type); if (!factory) { const available = getRegisteredAdapters().join(", ") || "none"; throw new Error(`Unknown storage adapter type: "${config.type}". Available adapters: ${available}`); } return factory(config); } registerStorageAdapter("memory", () => new InMemoryEventStore()); export class BaseEventStore { prefix; ttl; constructor(config = { type: "base" }) { this.prefix = config.prefix ?? "l0"; this.ttl = config.ttl ?? 0; } getStreamKey(streamId) { return `${this.prefix}:stream:${streamId}`; } getMetaKey(streamId) { return `${this.prefix}:meta:${streamId}`; } isExpired(timestamp) { if (this.ttl === 0) return false; return Date.now() - timestamp > this.ttl; } async getLastEvent(streamId) { const events = await this.getEvents(streamId); return events.length > 0 ? events[events.length - 1] : null; } async getEventsAfter(streamId, afterSeq) { const events = await this.getEvents(streamId); return events.filter((e) => e.seq > afterSeq); } } export class BaseEventStoreWithSnapshots extends BaseEventStore { getSnapshotKey(streamId) { return `${this.prefix}:snapshot:${streamId}`; } async getSnapshotBefore(streamId, seq) { const snapshot = await this.getSnapshot(streamId); if (snapshot && snapshot.seq <= seq) { return snapshot; } return null; } } export class FileEventStore extends BaseEventStoreWithSnapshots { basePath; fs = null; path = null; constructor(config) { super(config); this.basePath = config.basePath ?? config.connection ?? "./l0-events"; } async ensureFs() { if (!this.fs) { this.fs = await import("fs/promises"); this.path = await import("path"); await this.fs.mkdir(this.basePath, { recursive: true }); } } static validateStreamId(streamId) { if (!streamId || streamId.length === 0) { throw new Error("Invalid stream ID: must not be empty"); } if (!/^[a-zA-Z0-9_-]+$/.test(streamId)) { throw new Error("Invalid stream ID: only alphanumeric characters, hyphens, and underscores are allowed"); } return streamId; } getFilePath(streamId) { const safeId = FileEventStore.validateStreamId(streamId); return this.path.join(this.basePath, `${safeId}.json`); } getSnapshotFilePath(streamId) { const safeId = FileEventStore.validateStreamId(streamId); return this.path.join(this.basePath, `${safeId}.snapshot.json`); } async append(streamId, event) { await this.ensureFs(); const filePath = this.getFilePath(streamId); let events = []; try { const content = await this.fs.readFile(filePath, "utf-8"); events = JSON.parse(content); } catch { } const envelope = { streamId, seq: events.length, event, }; events.push(envelope); await this.fs.writeFile(filePath, JSON.stringify(events, null, 2)); } async getEvents(streamId) { await this.ensureFs(); const filePath = this.getFilePath(streamId); try { const content = await this.fs.readFile(filePath, "utf-8"); const events = JSON.parse(content); if (this.ttl > 0) { return events.filter((e) => !this.isExpired(e.event.ts)); } return events; } catch { return []; } } async exists(streamId) { await this.ensureFs(); const filePath = this.getFilePath(streamId); try { await this.fs.access(filePath); return true; } catch { return false; } } async delete(streamId) { await this.ensureFs(); const filePath = this.getFilePath(streamId); const snapshotPath = this.getSnapshotFilePath(streamId); try { await this.fs.unlink(filePath); } catch { } try { await this.fs.unlink(snapshotPath); } catch { } } async listStreams() { await this.ensureFs(); try { const files = await this.fs.readdir(this.basePath); return files .filter((f) => f.endsWith(".json") && !f.endsWith(".snapshot.json")) .map((f) => f.replace(".json", "")); } catch { return []; } } async saveSnapshot(snapshot) { await this.ensureFs(); const filePath = this.getSnapshotFilePath(snapshot.streamId); await this.fs.writeFile(filePath, JSON.stringify(snapshot, null, 2)); } async getSnapshot(streamId) { await this.ensureFs(); const filePath = this.getSnapshotFilePath(streamId); try { const content = await this.fs.readFile(filePath, "utf-8"); return JSON.parse(content); } catch { return null; } } } registerStorageAdapter("file", (config) => new FileEventStore(config)); export class LocalStorageEventStore extends BaseEventStoreWithSnapshots { storage; constructor(config = { type: "localStorage" }) { super(config); const globalObj = typeof globalThis !== "undefined" ? globalThis : {}; const ls = globalObj.localStorage; if (!ls) { throw new Error("LocalStorage is not available in this environment"); } this.storage = ls; } async append(streamId, event) { const key = this.getStreamKey(streamId); const existing = this.storage.getItem(key); const events = existing ? JSON.parse(existing) : []; const envelope = { streamId, seq: events.length, event, }; events.push(envelope); this.storage.setItem(key, JSON.stringify(events)); this.addToStreamList(streamId); } async getEvents(streamId) { const key = this.getStreamKey(streamId); const content = this.storage.getItem(key); if (!content) return []; const events = JSON.parse(content); if (this.ttl > 0) { return events.filter((e) => !this.isExpired(e.event.ts)); } return events; } async exists(streamId) { const key = this.getStreamKey(streamId); return this.storage.getItem(key) !== null; } async delete(streamId) { this.storage.removeItem(this.getStreamKey(streamId)); this.storage.removeItem(this.getSnapshotKey(streamId)); this.removeFromStreamList(streamId); } async listStreams() { const listKey = `${this.prefix}:streams`; const content = this.storage.getItem(listKey); return content ? JSON.parse(content) : []; } async saveSnapshot(snapshot) { const key = this.getSnapshotKey(snapshot.streamId); this.storage.setItem(key, JSON.stringify(snapshot)); } async getSnapshot(streamId) { const key = this.getSnapshotKey(streamId); const content = this.storage.getItem(key); return content ? JSON.parse(content) : null; } addToStreamList(streamId) { const listKey = `${this.prefix}:streams`; const existing = this.storage.getItem(listKey); const streams = existing ? JSON.parse(existing) : []; if (!streams.includes(streamId)) { streams.push(streamId); this.storage.setItem(listKey, JSON.stringify(streams)); } } removeFromStreamList(streamId) { const listKey = `${this.prefix}:streams`; const existing = this.storage.getItem(listKey); if (!existing) return; const streams = JSON.parse(existing); const filtered = streams.filter((s) => s !== streamId); this.storage.setItem(listKey, JSON.stringify(filtered)); } } registerStorageAdapter("localStorage", (config) => { return new LocalStorageEventStore(config); }); export class CompositeEventStore { stores; primaryIndex; constructor(stores, primaryIndex = 0) { if (stores.length === 0) { throw new Error("CompositeEventStore requires at least one store"); } this.stores = stores; this.primaryIndex = primaryIndex; } get primary() { return this.stores[this.primaryIndex]; } async append(streamId, event) { await Promise.all(this.stores.map((store) => store.append(streamId, event))); } async getEvents(streamId) { return this.primary.getEvents(streamId); } async exists(streamId) { return this.primary.exists(streamId); } async getLastEvent(streamId) { return this.primary.getLastEvent(streamId); } async getEventsAfter(streamId, afterSeq) { return this.primary.getEventsAfter(streamId, afterSeq); } async delete(streamId) { await Promise.all(this.stores.map((store) => store.delete(streamId))); } async listStreams() { return this.primary.listStreams(); } } export function createCompositeStore(stores, primaryIndex) { return new CompositeEventStore(stores, primaryIndex); } export class TTLEventStore { store; ttl; constructor(store, ttlMs) { this.store = store; this.ttl = ttlMs; } isExpired(timestamp) { return Date.now() - timestamp > this.ttl; } filterExpired(events) { return events.filter((e) => !this.isExpired(e.event.ts)); } async append(streamId, event) { return this.store.append(streamId, event); } async getEvents(streamId) { const events = await this.store.getEvents(streamId); return this.filterExpired(events); } async exists(streamId) { const events = await this.getEvents(streamId); return events.length > 0; } async getLastEvent(streamId) { const events = await this.getEvents(streamId); return events.length > 0 ? events[events.length - 1] : null; } async getEventsAfter(streamId, afterSeq) { const events = await this.getEvents(streamId); return events.filter((e) => e.seq > afterSeq); } async delete(streamId) { return this.store.delete(streamId); } async listStreams() { return this.store.listStreams(); } } export function withTTL(store, ttlMs) { return new TTLEventStore(store, ttlMs); } //# sourceMappingURL=storageAdapters.js.map