File size: 4,486 Bytes
1dbc34b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | /**
* React Query Cache Persistence
*
* Persists the React Query cache to IndexedDB so that after a tab discard
* or page reload, the user sees cached data instantly while fresh data
* loads in the background.
*
* Uses @tanstack/react-query-persist-client with idb-keyval for IndexedDB storage.
* Cached data is treated as stale on restore and silently refetched.
*/
import { get, set, del } from 'idb-keyval';
import type { PersistedClient, Persister } from '@tanstack/react-query-persist-client';
import { createLogger } from '@automaker/utils/logger';
const logger = createLogger('QueryPersist');
const IDB_KEY = 'automaker-react-query-cache';
/**
* Maximum age of persisted cache before it's discarded (24 hours).
* After this time, the cache is considered too old and will be removed.
*/
export const PERSIST_MAX_AGE_MS = 24 * 60 * 60 * 1000;
/**
* Throttle time for persisting cache to IndexedDB.
* Prevents excessive writes during rapid query updates.
*/
export const PERSIST_THROTTLE_MS = 2000;
/**
* Query key prefixes that should NOT be persisted.
* Auth-related and volatile data should always be fetched fresh.
*/
const EXCLUDED_QUERY_KEY_PREFIXES = ['auth', 'health', 'wsToken', 'sandbox'];
/**
* Check if a query key should be excluded from persistence
*/
function shouldExcludeQuery(queryKey: readonly unknown[]): boolean {
if (queryKey.length === 0) return false;
const firstKey = String(queryKey[0]);
return EXCLUDED_QUERY_KEY_PREFIXES.some((prefix) => firstKey.startsWith(prefix));
}
/**
* Check whether there is a recent enough React Query cache in IndexedDB
* to consider the app "warm" (i.e., safe to skip blocking on the server
* health check and show the UI immediately).
*
* Returns true only if:
* 1. The cache exists and is recent (within maxAgeMs)
* 2. The cache buster matches the current build hash
*
* If the buster doesn't match, PersistQueryClientProvider will wipe the
* cache on restore — so we must NOT skip the server wait in that case,
* otherwise the board renders with empty queries and no data.
*
* This is a read-only probe — it does not restore the cache (that is
* handled by PersistQueryClientProvider automatically).
*/
export async function hasWarmIDBCache(
currentBuster: string,
maxAgeMs = PERSIST_MAX_AGE_MS
): Promise<boolean> {
try {
const client = await get<PersistedClient>(IDB_KEY);
if (!client) return false;
// PersistedClient stores a `timestamp` (ms) when it was last persisted
const age = Date.now() - (client.timestamp ?? 0);
if (age >= maxAgeMs) return false;
// If the buster doesn't match, PersistQueryClientProvider will wipe the cache.
// Treat this as a cold start — we need fresh data from the server.
if (currentBuster && client.buster !== currentBuster) return false;
return true;
} catch {
return false;
}
}
/**
* Create an IndexedDB-based persister for React Query.
*
* This persister:
* - Stores the full query cache in IndexedDB under a single key
* - Filters out auth/health queries that shouldn't be persisted
* - Handles errors gracefully (cache persistence is best-effort)
*/
export function createIDBPersister(): Persister {
return {
persistClient: async (client: PersistedClient) => {
try {
// Filter out excluded queries before persisting
const filteredClient: PersistedClient = {
...client,
clientState: {
...client.clientState,
queries: client.clientState.queries.filter(
(query) => !shouldExcludeQuery(query.queryKey)
),
// Don't persist mutations (they should be re-triggered, not replayed)
mutations: [],
},
};
await set(IDB_KEY, filteredClient);
} catch (error) {
logger.warn('Failed to persist query cache to IndexedDB:', error);
}
},
restoreClient: async () => {
try {
const client = await get<PersistedClient>(IDB_KEY);
if (client) {
logger.info('Restored React Query cache from IndexedDB');
}
return client ?? undefined;
} catch (error) {
logger.warn('Failed to restore query cache from IndexedDB:', error);
return undefined;
}
},
removeClient: async () => {
try {
await del(IDB_KEY);
} catch (error) {
logger.warn('Failed to remove query cache from IndexedDB:', error);
}
},
};
}
|