Files
confidence-engine/lib/storage/providers/local-storage.js
T
robbond 8c85120b1c feat(confidence-engine): v0.60c identity-aware investigation storage
- 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
2026-09-03 18:09:03 +01:00

148 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 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;
}