/* * Tier 2 offline support: persist the RTK Query cache (last-seen server data — * library/albums/artists/…) to the active backend's localStorage namespace and * replay it into the cache on startup. With the backend down, components keep * rendering the last-known data read-only instead of an error state. * * Per the architecture invariant, server data is NOT copied into a slice: this * snapshots the RTKQ cache itself and feeds it back through RTKQ's own * `extractRehydrationInfo` mechanism (see `api/index.ts`). */ import { instanceStorage } from '../config/instances'; import { rehydrateApi, type RehydrateApiPayload } from '../api/rehydrate'; import type { RootState } from './index'; const CACHE_KEY = 'rtkq'; type ApiState = RootState['api']; type QueryEntry = ApiState['queries'][string]; /** * Keep only successfully-fulfilled query results — pending/rejected entries * carry no usable data and subscriptions are rebuilt by components on mount. * Mutation results are never restored. */ const EMPTY_PROVIDED = { tags: {}, keys: {} }; function snapshot(apiState: ApiState): RehydrateApiPayload { const queries: Record = {}; for (const [key, entry] of Object.entries(apiState.queries)) { const q = entry as QueryEntry | undefined; if (q && q.status === 'fulfilled') queries[key] = q; } // Carry `provided` along so RTKQ can re-register invalidation tags for the // restored entries; it is also required structurally (see RehydrateApiPayload). return { queries, mutations: {}, provided: apiState.provided ?? EMPTY_PROVIDED, }; } function load(): RehydrateApiPayload | null { try { const raw = instanceStorage.get(CACHE_KEY); if (!raw) return null; const parsed = JSON.parse(raw) as Partial; if (!parsed.queries) return null; // `provided` may be absent in snapshots written before this field existed — // default it so the invalidation slice doesn't crash on `provided.tags`. return { queries: parsed.queries, mutations: {}, provided: parsed.provided ?? EMPTY_PROVIDED, }; } catch { return null; } } /** Replay the persisted cache into RTKQ. Call once after the store is created. */ export function rehydrateApiCache(dispatch: (action: unknown) => void): void { const cached = load(); if (cached) dispatch(rehydrateApi(cached)); } /** * Subscribe a store so the RTKQ cache is flushed to localStorage. Throttled, * since cache state churns on every in-flight query transition. */ export function startApiPersistence(store: { getState: () => RootState; subscribe: (listener: () => void) => () => void; }): () => void { let last = ''; let timer: ReturnType | null = null; const flush = () => { timer = null; const snap = JSON.stringify(snapshot(store.getState().api)); if (snap !== last) { instanceStorage.set(CACHE_KEY, snap); last = snap; } }; return store.subscribe(() => { if (timer) return; timer = setTimeout(flush, 2000); }); }