mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
Comprehensive set of mobile and all improvements phase 1
This commit is contained in:
133
apps/ui/src/lib/query-persist.ts
Normal file
133
apps/ui/src/lib/query-persist.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user