feat(confidence-engine): use server investigation persistence

This commit is contained in:
2026-09-08 19:21:05 +01:00
parent 6dd447e56a
commit d6df1d210e
15 changed files with 425 additions and 114 deletions
+63 -23
View File
@@ -1,43 +1,81 @@
// investigation-storage — application-facing persistence boundary
// Owns the canonical identity contract: snapshot.id is the sole save identity.
// Delegates to the concrete localStorage provider internally.
// Delegates to the authenticated browser HTTP provider internally.
import { loadInvestigation as _load, saveInvestigation as _save, clearInvestigation as _clear, listInvestigations as _list, restartInvestigation as _restart } from "./providers/local-storage.js";
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.
* When snapshot carries an id — persist under that id key (identity-aware).
* When snapshot has no id — fall back to legacy singleton compatibility path.
* A durable id is required and is sent to the authenticated server API.
*/
export function saveInvestigation(snapshot, explicitId) {
if (snapshot && typeof snapshot === "object" && snapshot.id != null) {
return _save(snapshot, snapshot.id);
const id = snapshot?.id ?? explicitId;
if (!snapshot || typeof snapshot !== "object" || !id) {
return observeUnhandledRejection(Promise.reject(new Error("Investigation snapshot and id are required")));
}
// Legacy unidentified snapshot — singleton fallback for unmigrated callers
return _save(snapshot, explicitId);
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 when supplied;
* fall back to legacy singleton path otherwise.
* Canonical load contract: select by durable id through the authenticated server API.
*/
export function loadInvestigation(id) {
return _load(id != null ? id : undefined);
}
/**
* Canonical clear contract: remove by identity-aware key when supplied;
* fall back to legacy singleton keys otherwise.
*/
export function clearInvestigation(id) {
return _clear(id ?? undefined);
export async function loadInvestigation(id) {
if (!id) return null;
return _load(id);
}
/**
* Lists all durable-ID Investigation records as lightweight summaries.
* Excludes legacy singleton, sessionStorage state, unrelated storage, malformed entries.
* Server persistence is the authority; legacy browser storage is not consulted.
*/
export function listInvestigations() {
export async function listInvestigations() {
return _list();
}
@@ -49,6 +87,8 @@ export function listInvestigations() {
*
* Missing/invalid id → silently no-op (does NOT fall back to legacy singleton).
*/
export function restartInvestigation(id) {
export async function restartInvestigation(id) {
if (!id) return null;
await waitForSaves(id);
return _restart(id);
}