mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-17 10:03:08 +00:00
134 lines
4.4 KiB
TypeScript
134 lines
4.4 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
},
|
|
};
|
|
}
|