95 lines
3.1 KiB
JavaScript
95 lines
3.1 KiB
JavaScript
// 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);
|
|
}
|