import type { EvalLanguage } from "./types.ts"; /** * Session-scoped value store for the eval kernels, mirroring the Codex * runtime's store/load contract: writes are staged during a cell and commit * only when the cell completes; a cancelled or failed cell discards them. * * Staging is per language (one active cell per language is the invariant the * detached cell manager enforces), so a commit/discard of the language slot * at settlement is unambiguous. The store lives for one session generation; * it is not persisted and not shared across sessions. */ export class SessionStore { readonly #committed = new Map>(); readonly #staged = new Map>(); /** * Stage a value under a string key for the language's active cell. * The value is JSON round-tripped: plain serializable values are stored * as their decoded form; functions, symbols, bigint, cycles, and * `undefined` throw a TypeError. Returns the stored (round-tripped) value. */ stage(language: EvalLanguage, key: string, value: unknown): unknown { if (typeof key !== "string") { throw new TypeError( `Unable to store. Key must be a string (got ${typeof key}).` ); } let serialized: string | undefined; try { serialized = JSON.stringify(value); } catch (error) { throw new TypeError( `Unable to store "${key}". Only plain serializable values can be stored.`, { cause: error } ); } // JSON.stringify returns undefined (not a string) for functions, symbols, // and undefined — all of which must be rejected, not silently dropped. if (serialized === undefined) { throw new TypeError( `Unable to store "${key}". Only plain serializable values can be stored.` ); } const roundTripped = JSON.parse(serialized) as unknown; this.#stagedFor(language).set(key, roundTripped); return roundTripped; } /** * Read the value stored under a key for a language: the staged value when * present, then the committed value, then `null` when neither exists. */ read(language: EvalLanguage, key: string): unknown { const staged = this.#stagedFor(language).get(key); if (staged !== undefined) { return staged; } return this.#committed.get(language)?.get(key) ?? null; } /** Merge the language's staged map into its committed map and clear the staged map. */ commit(language: EvalLanguage): void { const staged = this.#staged.get(language); if (staged === undefined) { return; } let committed = this.#committed.get(language); if (committed === undefined) { committed = new Map(); this.#committed.set(language, committed); } for (const [key, value] of staged) { committed.set(key, value); } staged.clear(); } /** Clear the language's staged map. Committed values stay. */ discard(language: EvalLanguage): void { this.#staged.delete(language); } #stagedFor(language: EvalLanguage): Map { let map = this.#staged.get(language); if (map === undefined) { map = new Map(); this.#staged.set(language, map); } return map; } }