import { uuidv7 } from "@earendil-works/pi-ai"; import { type LeafEntry, SessionError, type SessionMetadata, type SessionStorage, type SessionTreeEntry, } from "../types.ts"; function updateLabelCache(labelsById: Map, entry: SessionTreeEntry): void { if (entry.type !== "label") return; const label = entry.label?.trim(); if (label) { labelsById.set(entry.targetId, label); } else { labelsById.delete(entry.targetId); } } function buildLabelsById(entries: SessionTreeEntry[]): Map { const labelsById = new Map(); for (const entry of entries) { updateLabelCache(labelsById, entry); } return labelsById; } function generateEntryId(byId: { has(id: string): boolean }): string { for (let i = 0; i < 100; i++) { // The uuidv7 prefix is timestamp-derived and nearly constant between calls, // so short ids must come from the random tail. const id = uuidv7().slice(-8); if (!byId.has(id)) return id; } return uuidv7(); } function leafIdAfterEntry(entry: SessionTreeEntry): string | null { return entry.type === "leaf" ? entry.targetId : entry.id; } export class InMemorySessionStorage implements SessionStorage { private readonly metadata: TMetadata; private entries: SessionTreeEntry[]; private byId: Map; private labelsById: Map; private leafId: string | null; constructor(options?: { entries?: SessionTreeEntry[]; metadata?: TMetadata }) { this.entries = options?.entries ? [...options.entries] : []; this.byId = new Map(this.entries.map((entry) => [entry.id, entry])); this.labelsById = buildLabelsById(this.entries); this.leafId = null; for (const entry of this.entries) this.leafId = leafIdAfterEntry(entry); if (this.leafId !== null && !this.byId.has(this.leafId)) { throw new SessionError("invalid_session", `Entry ${this.leafId} not found`); } this.metadata = options?.metadata ?? ({ id: uuidv7(), createdAt: new Date().toISOString() } as TMetadata); } async getMetadata(): Promise { return this.metadata; } async getLeafId(): Promise { if (this.leafId !== null && !this.byId.has(this.leafId)) { throw new SessionError("invalid_session", `Entry ${this.leafId} not found`); } return this.leafId; } async setLeafId(leafId: string | null): Promise { if (leafId !== null && !this.byId.has(leafId)) { throw new SessionError("not_found", `Entry ${leafId} not found`); } const entry: LeafEntry = { type: "leaf", id: generateEntryId(this.byId), parentId: this.leafId, timestamp: new Date().toISOString(), targetId: leafId, }; this.entries.push(entry); this.byId.set(entry.id, entry); this.leafId = leafId; } async createEntryId(): Promise { return generateEntryId(this.byId); } async appendEntry(entry: SessionTreeEntry): Promise { this.entries.push(entry); this.byId.set(entry.id, entry); updateLabelCache(this.labelsById, entry); this.leafId = leafIdAfterEntry(entry); } async getEntry(id: string): Promise { return this.byId.get(id); } async findEntries( type: TType, ): Promise>> { return this.entries.filter((entry): entry is Extract => entry.type === type); } async getLabel(id: string): Promise { return this.labelsById.get(id); } async getPathToRoot(leafId: string | null): Promise { if (leafId === null) return []; const path: SessionTreeEntry[] = []; let current = this.byId.get(leafId); if (!current) throw new SessionError("not_found", `Entry ${leafId} not found`); while (current) { path.unshift(current); if (!current.parentId) break; const parent = this.byId.get(current.parentId); if (!parent) throw new SessionError("invalid_session", `Entry ${current.parentId} not found`); current = parent; } return path; } async getEntries(): Promise { return [...this.entries]; } }