71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
|
|
/**
|
||
|
|
* Persists the React Query cache to IndexedDB so the dashboard is *viewable*
|
||
|
|
* offline (last-synced data) and survives a reload with no connection.
|
||
|
|
*
|
||
|
|
* Uses `dehydrate`/`hydrate` from @tanstack/react-query directly — no extra
|
||
|
|
* dependency. Writes are debounced; reads are guarded by a schema buster, a
|
||
|
|
* max-age, and a tenant scope so one café never hydrates another's data.
|
||
|
|
*/
|
||
|
|
import { dehydrate, hydrate, type QueryClient } from "@tanstack/react-query";
|
||
|
|
import { kvGet, kvSet } from "@/lib/offline/offline-db";
|
||
|
|
|
||
|
|
const CACHE_KEY = "rq-cache";
|
||
|
|
/** Bump when cached shapes change so stale persisted data is dropped on deploy. */
|
||
|
|
const BUSTER = "v1";
|
||
|
|
const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h
|
||
|
|
const SAVE_DEBOUNCE_MS = 1000;
|
||
|
|
|
||
|
|
type PersistedCache = {
|
||
|
|
buster: string;
|
||
|
|
timestamp: number;
|
||
|
|
/** Tenant/user scope this cache belongs to (cafeId, or "anon"). */
|
||
|
|
scope: string;
|
||
|
|
state: unknown;
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Hydrate the query cache from IndexedDB if a valid snapshot exists for this
|
||
|
|
* scope. Safe to call before or after queries mount.
|
||
|
|
*/
|
||
|
|
export async function restoreQueryCache(qc: QueryClient, scope: string): Promise<void> {
|
||
|
|
const saved = await kvGet<PersistedCache>(CACHE_KEY);
|
||
|
|
if (!saved) return;
|
||
|
|
if (saved.buster !== BUSTER) return; // schema changed
|
||
|
|
if (saved.scope !== scope) return; // different tenant/user — do not leak
|
||
|
|
if (Date.now() - saved.timestamp > MAX_AGE_MS) return; // too old
|
||
|
|
try {
|
||
|
|
hydrate(qc, saved.state as never);
|
||
|
|
} catch {
|
||
|
|
// corrupt snapshot — ignore, it will be overwritten on next save
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Subscribe to cache changes and persist a debounced snapshot. Returns an
|
||
|
|
* unsubscribe function.
|
||
|
|
*/
|
||
|
|
export function startPersisting(qc: QueryClient, scope: string): () => void {
|
||
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||
|
|
|
||
|
|
const save = () => {
|
||
|
|
timer = null;
|
||
|
|
const snapshot: PersistedCache = {
|
||
|
|
buster: BUSTER,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
scope,
|
||
|
|
state: dehydrate(qc),
|
||
|
|
};
|
||
|
|
void kvSet(CACHE_KEY, snapshot);
|
||
|
|
};
|
||
|
|
|
||
|
|
const unsubscribe = qc.getQueryCache().subscribe(() => {
|
||
|
|
if (timer) return; // a save is already scheduled
|
||
|
|
timer = setTimeout(save, SAVE_DEBOUNCE_MS);
|
||
|
|
});
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
if (timer) clearTimeout(timer);
|
||
|
|
unsubscribe();
|
||
|
|
};
|
||
|
|
}
|