@ai2070/l0
Version:
L0: The Missing Reliability Substrate for AI
341 lines • 11.2 kB
JavaScript
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