- loadInvestigation(id) selects by durable ID when provided, null for unknown - saveInvestigation(snapshot, id) persists under provider-chosen key derived from id - clearInvestigation(id) removes specific investigation by identity when provided - localStorage representation: confidence-engine-investigation:<durable-id> - Backward-compatible singleton path preserved for existing unmigrated callers - 6 new targeted tests proving two independently addressable Investigations
148 lines
5.0 KiB
JavaScript
148 lines
5.0 KiB
JavaScript
// ── canonical identifiers ────────────────────────────────────────────
|
||
|
||
const CANONICAL_KEY = "confidence-engine-investigation";
|
||
const LEGACY_KEY = "confidence-engine-session";
|
||
const SCHEMA_VERSION = 1;
|
||
|
||
// Multi-Investigation key prefix (v0.60c)
|
||
const INVESTIGATION_PREFIX = "confidence-engine-investigation:";
|
||
|
||
// ── helpers ──────────────────────────────────────────────────────────
|
||
|
||
function safeGet(storage, key) {
|
||
if (!storage || typeof storage.getItem !== "function") return null;
|
||
try { return storage.getItem(key); } catch (_) { return null; }
|
||
}
|
||
|
||
function isPlainObject(value) {
|
||
return (
|
||
value !== null &&
|
||
typeof value === "object" &&
|
||
!Array.isArray(value) &&
|
||
Object.prototype.toString.call(value) === "[object Object]"
|
||
);
|
||
}
|
||
|
||
// ── loadInvestigation (identity-aware) ───────────────────────────────
|
||
|
||
/**
|
||
* Returns the persisted investigation snapshot keyed by durable id
|
||
* (normalised to current schema), or null when no usable state exists.
|
||
*
|
||
* When called without an id argument, reads from the legacy singleton key
|
||
* for backward-compatible consumers that have not yet migrated.
|
||
*/
|
||
export function loadInvestigation(id) {
|
||
const storage = _getTargetStorage();
|
||
if (!storage) return null;
|
||
|
||
// — identity-aware contract: select by durable id —
|
||
if (id != null) {
|
||
const raw = safeGet(storage, `${INVESTIGATION_PREFIX}${id}`);
|
||
if (raw === null) return null;
|
||
try {
|
||
const parsed = JSON.parse(raw);
|
||
if (isPlainObject(parsed)) {
|
||
if (!("schemaVersion" in parsed)) parsed.schemaVersion = SCHEMA_VERSION;
|
||
return parsed;
|
||
}
|
||
} catch (_) { return null; }
|
||
return null;
|
||
}
|
||
|
||
// — backward-compatible singleton path (no id supplied) —
|
||
// 1 – canonical key first
|
||
let raw = safeGet(storage, CANONICAL_KEY);
|
||
if (raw !== null) {
|
||
try {
|
||
const parsed = JSON.parse(raw);
|
||
if (isPlainObject(parsed)) {
|
||
if (!("schemaVersion" in parsed)) parsed.schemaVersion = SCHEMA_VERSION;
|
||
return parsed;
|
||
}
|
||
} catch (_) { /* malformed — fall through to legacy */ }
|
||
}
|
||
|
||
// 2 – legacy fallback
|
||
const legacyStorage = _getLegacyStorage();
|
||
if (!legacyStorage) return null;
|
||
|
||
raw = safeGet(legacyStorage, LEGACY_KEY);
|
||
if (raw === null) return null;
|
||
let parsed;
|
||
try {
|
||
parsed = JSON.parse(raw);
|
||
} catch (_) { return null; }
|
||
|
||
if (!isPlainObject(parsed)) return null;
|
||
|
||
// Normalise schemaVersion for legacy data
|
||
if (!("schemaVersion" in parsed)) parsed.schemaVersion = SCHEMA_VERSION;
|
||
|
||
// Migrate into canonical key
|
||
_persist(storage, CANONICAL_KEY, JSON.stringify(parsed));
|
||
|
||
return parsed;
|
||
}
|
||
|
||
// ── saveInvestigation (identity-aware) ───────────────────────────────
|
||
|
||
/**
|
||
* Persists the supplied snapshot under its durable id key.
|
||
* The caller's object is never mutated.
|
||
*
|
||
* When called without an id argument, writes to the legacy singleton key
|
||
* for backward-compatible consumers that have not yet migrated.
|
||
*/
|
||
export function saveInvestigation(snapshot, id) {
|
||
const storage = _getTargetStorage();
|
||
if (!storage) return; // silently no-op in non-browser
|
||
|
||
try {
|
||
const record = JSON.parse(JSON.stringify(snapshot));
|
||
record.schemaVersion = SCHEMA_VERSION;
|
||
const key = id != null ? `${INVESTIGATION_PREFIX}${id}` : CANONICAL_KEY;
|
||
_persist(storage, key, JSON.stringify(record));
|
||
} catch (_) { /* storage errors must not crash caller */ }
|
||
}
|
||
|
||
// ── clearInvestigation (identity-aware) ──────────────────────────────
|
||
|
||
/**
|
||
* Removes a specific investigation by durable id.
|
||
* When called without an id, clears the legacy singleton keys only.
|
||
*/
|
||
export function clearInvestigation(id) {
|
||
const storage = _getTargetStorage();
|
||
if (!storage) return;
|
||
if (id != null) {
|
||
try { storage.removeItem(`${INVESTIGATION_PREFIX}${id}`); } catch (_) {}
|
||
} else {
|
||
try { storage.removeItem(CANONICAL_KEY); } catch (_) {}
|
||
const legacyStorage = _getLegacyStorage();
|
||
if (legacyStorage) {
|
||
try { legacyStorage.removeItem(LEGACY_KEY); } catch (_) {}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── internals ────────────────────────────────────────────────────────
|
||
|
||
function _persist(storage, key, value) {
|
||
try { storage.setItem(key, value); } catch (_) {}
|
||
}
|
||
|
||
function _getTargetStorage() {
|
||
if (typeof globalThis.window === "undefined") return null;
|
||
const s = globalThis.window.localStorage;
|
||
if (!s || typeof s.getItem !== "function") return null;
|
||
return s;
|
||
}
|
||
|
||
function _getLegacyStorage() {
|
||
if (typeof globalThis.window === "undefined") return null;
|
||
const s = globalThis.window.sessionStorage;
|
||
if (!s || typeof s.getItem !== "function") return null;
|
||
return s;
|
||
}
|