// investigation-storage — application-facing persistence boundary // Owns the canonical identity contract: snapshot.id is the sole save identity. // Delegates to the authenticated browser HTTP provider internally. import { loadInvestigation as _load, saveInvestigation as _save, listInvestigations as _list, restartInvestigation as _restart } from "./providers/server-http.js"; const saveStates = new Map(); function observeUnhandledRejection(promise) { promise.catch(() => {}); return promise; } function getSaveState(id) { if (!saveStates.has(id)) saveStates.set(id, { inFlight: false, pending: null, idleWaiters: [] }); return saveStates.get(id); } async function drainSaveState(id, state) { while (state.pending) { const pending = state.pending; state.pending = null; try { const saved = await _save(pending.snapshot, id); pending.waiters.forEach(({ resolve }) => resolve(saved)); } catch (error) { pending.waiters.forEach(({ reject }) => reject(error)); } } state.inFlight = false; state.idleWaiters.splice(0).forEach((resolve) => resolve()); } function waitForSaves(id) { const state = saveStates.get(id); if (!state?.inFlight && !state?.pending) return Promise.resolve(); return new Promise((resolve) => state.idleWaiters.push(resolve)); } /** * Canonical save contract: snapshot.id is the sole identity authority. * A durable id is required and is sent to the authenticated server API. */ export function saveInvestigation(snapshot, explicitId) { const id = snapshot?.id ?? explicitId; if (!snapshot || typeof snapshot !== "object" || !id) { return observeUnhandledRejection(Promise.reject(new Error("Investigation snapshot and id are required"))); } const state = getSaveState(id); const promise = new Promise((resolve, reject) => { // A pending entry has not started yet, so replacing it safely coalesces intermediate autosaves. if (state.pending) { state.pending.snapshot = snapshot; state.pending.waiters.push({ resolve, reject }); } else { state.pending = { snapshot, waiters: [{ resolve, reject }] }; } }); if (!state.inFlight) { state.inFlight = true; void drainSaveState(id, state); } return observeUnhandledRejection(promise); } /** * Canonical load contract: select by durable id through the authenticated server API. */ export async function loadInvestigation(id) { if (!id) return null; return _load(id); } /** * Lists all durable-ID Investigation records as lightweight summaries. * Server persistence is the authority; legacy browser storage is not consulted. */ export async function listInvestigations() { return _list(); } /** * Semantic restart: reset reasoning/report state within the Investigation container. * * The Investigation is NOT deleted or replaced. Its durable `id` and `scenario` (container) * are preserved. All reasoning-state fields are cleared so a new clean pass can begin. * * Missing/invalid id → silently no-op (does NOT fall back to legacy singleton). */ export async function restartInvestigation(id) { if (!id) return null; await waitForSaves(id); return _restart(id); }