// Shared memo for the strategic storage registry hydrated by bootstrap. // // Why this exists: src/services/bootstrap.ts getHydratedData() is // single-use — it deletes the value on first read. The Energy Atlas has // TWO consumers that both need the raw registry on first paint: // (1) DeckGLMap's createEnergyStorageLayer(), (2) StorageFacilityMapPanel's // bootstrap projection. Whichever read first would drain the cache and // the other would fall back to RPC (map would have no first-paint dots). // This module reads once and memoizes so both consumers get identical // data from the same source of truth. // // Update path: when the panel's background RPC fetch completes, it calls // setCachedStorageFacilityRegistry() to refresh the memo. The map picks // up the new data on its next re-render cycle (triggered by state change // from any other source), keeping map ↔ panel aligned. // // Mirror of src/shared/pipeline-registry-store.ts — same rationale, same // test hooks. Kept as a separate module because the two registries use // different bootstrap keys and have no shared projection logic. import { getHydratedData } from '@/services/bootstrap'; export interface RawStorageFacilityRegistry { facilities?: Record; classifierVersion?: string; updatedAt?: string; } interface CachedRegistry { registry: RawStorageFacilityRegistry | undefined; /** Source: 'bootstrap' on first drain, 'rpc' after panel refresh. */ source: 'bootstrap' | 'rpc' | 'none'; } let cache: CachedRegistry = { registry: undefined, source: 'none' }; let drained = false; // Indirection so tests can inject a fake reader. The literal // getHydratedData('storageFacilities') call below satisfies // tests/bootstrap.test.mjs's consumer-coverage grep. type BootstrapReader = (key: string) => unknown; function defaultBootstrapReader(key: string): unknown { if (key === 'storageFacilities') return getHydratedData('storageFacilities'); return getHydratedData(key); } let reader: BootstrapReader = defaultBootstrapReader; /** * Returns the cached storage registry. On first call, drains the * bootstrap hydration slot and stores the result. Subsequent calls return * the same cached value regardless of how many consumers read. */ export function getCachedStorageFacilityRegistry(): CachedRegistry { if (!drained) { drained = true; const registry = reader('storageFacilities') as RawStorageFacilityRegistry | undefined; if (registry) { cache = { registry, source: 'bootstrap' }; } } return cache; } /** * Updates the cache from a fresh RPC response (the panel calls this after * its background listStorageFacilities() settles so the map picks up the * newer classifierVersion / fetchedAt on its next render). */ export function setCachedStorageFacilityRegistry(registry: RawStorageFacilityRegistry): void { drained = true; cache = { registry, source: 'rpc' }; } /** Test-only: reset cache state so suites can exercise the drain-once behavior. */ export function __resetStorageFacilityRegistryStoreForTests(): void { cache = { registry: undefined, source: 'none' }; drained = false; reader = defaultBootstrapReader; } /** Test-only: inject a fake bootstrap reader. */ export function __setBootstrapReaderForTests(fn: (key: string) => unknown): void { reader = fn; }