hyp / apps /ui /src /lib /query-client.ts
Leon4gr45's picture
Upload folder using huggingface_hub
1dbc34b verified
/**
* React Query Client Configuration
*
* Central configuration for TanStack React Query.
* Provides default options for queries and mutations including
* caching, retries, and error handling.
*
* Mobile-aware: Automatically extends stale times and garbage collection
* on mobile devices to reduce unnecessary refetching, which causes
* blank screens, reloads, and battery drain on flaky mobile connections.
*/
import { QueryClient, keepPreviousData } from '@tanstack/react-query';
import { toast } from 'sonner';
import { createLogger } from '@automaker/utils/logger';
import { isConnectionError, handleServerOffline } from './http-api-client';
import { isMobileDevice } from './mobile-detect';
const logger = createLogger('QueryClient');
/**
* Mobile multiplier for stale times.
* On mobile, data stays "fresh" longer to avoid refetching on every
* component mount, which causes blank flickers and layout shifts.
* The WebSocket invalidation system still ensures critical updates
* (feature status changes, agent events) arrive in real-time.
*/
const MOBILE_STALE_MULTIPLIER = isMobileDevice ? 3 : 1;
/**
* Default stale times for different data types.
* On mobile, these are multiplied by MOBILE_STALE_MULTIPLIER to reduce
* unnecessary network requests while WebSocket handles real-time updates.
*/
export const STALE_TIMES = {
/** Features change frequently during auto-mode */
FEATURES: 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 1 min (3 min on mobile)
/** GitHub data is relatively stable */
GITHUB: 2 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 2 min (6 min on mobile)
/** Running agents state changes very frequently */
RUNNING_AGENTS: 5 * 1000 * MOBILE_STALE_MULTIPLIER, // 5s (15s on mobile)
/** Agent output changes during streaming */
AGENT_OUTPUT: 5 * 1000 * MOBILE_STALE_MULTIPLIER, // 5s (15s on mobile)
/** Usage data with polling */
USAGE: 30 * 1000 * MOBILE_STALE_MULTIPLIER, // 30s (90s on mobile)
/** Models rarely change */
MODELS: 5 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 5 min (15 min on mobile)
/** CLI status rarely changes */
CLI_STATUS: 5 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 5 min (15 min on mobile)
/** Settings are relatively stable */
SETTINGS: 2 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 2 min (6 min on mobile)
/** Worktrees change during feature development */
WORKTREES: 30 * 1000 * MOBILE_STALE_MULTIPLIER, // 30s (90s on mobile)
/** Sessions rarely change */
SESSIONS: 2 * 60 * 1000 * MOBILE_STALE_MULTIPLIER, // 2 min (6 min on mobile)
/** Default for unspecified queries */
DEFAULT: 30 * 1000 * MOBILE_STALE_MULTIPLIER, // 30s (90s on mobile)
} as const;
/**
* Default garbage collection times (gcTime, formerly cacheTime).
* On mobile, cache is kept longer so data persists across navigations
* and component unmounts, preventing blank screens on re-mount.
*/
export const GC_TIMES = {
/** Default garbage collection time - must exceed persist maxAge for cache to survive tab discard */
DEFAULT: isMobileDevice ? 15 * 60 * 1000 : 10 * 60 * 1000, // 15 min on mobile, 10 min desktop
/** Extended for expensive queries */
EXTENDED: isMobileDevice ? 30 * 60 * 1000 : 15 * 60 * 1000, // 30 min on mobile, 15 min desktop
} as const;
/**
* Global error handler for queries
*/
const handleQueryError = (error: Error) => {
logger.error('Query error:', error);
// Check for connection errors (server offline)
if (isConnectionError(error)) {
handleServerOffline();
return;
}
// Don't toast for auth errors - those are handled by http-api-client
if (error.message === 'Unauthorized') {
return;
}
};
/**
* Global error handler for mutations
*/
const handleMutationError = (error: Error) => {
logger.error('Mutation error:', error);
// Check for connection errors
if (isConnectionError(error)) {
handleServerOffline();
return;
}
// Don't toast for auth errors
if (error.message === 'Unauthorized') {
return;
}
// Show error toast for other errors
toast.error('Operation failed', {
description: error.message || 'An unexpected error occurred',
});
};
/**
* Create and configure the QueryClient singleton.
*
* Mobile optimizations:
* - refetchOnWindowFocus disabled on mobile (prevents refetch storms when
* switching apps, which causes the blank screen + reload cycle)
* - refetchOnMount uses 'always' on desktop but only refetches stale data
* on mobile (prevents unnecessary network requests on navigation)
* - Longer stale times and GC times via STALE_TIMES and GC_TIMES above
* - structuralSharing enabled to minimize re-renders when data hasn't changed
*/
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: STALE_TIMES.DEFAULT,
gcTime: GC_TIMES.DEFAULT,
retry: (failureCount, error) => {
// Don't retry on auth errors
if (error instanceof Error && error.message === 'Unauthorized') {
return false;
}
// Retry connection errors a few times before declaring server offline.
// This handles transient network blips without immediately redirecting to login.
if (isConnectionError(error)) {
return failureCount < 3;
}
// Retry up to 2 times for other errors (3 on mobile for flaky connections)
return failureCount < (isMobileDevice ? 3 : 2);
},
retryDelay: (attemptIndex, error) => {
// Use shorter delays for connection errors to recover quickly from blips
if (isConnectionError(error)) {
return Math.min(1000 * 2 ** attemptIndex, 5000); // 1s, 2s, 4s (capped at 5s)
}
return Math.min(1000 * 2 ** attemptIndex, 30000);
},
// On mobile, disable refetch on focus to prevent the blank screen + reload
// cycle that occurs when the user switches back to the app. WebSocket
// invalidation handles real-time updates; polling handles the rest.
refetchOnWindowFocus: !isMobileDevice,
refetchOnReconnect: true,
// On mobile, only refetch on mount if data is stale (true = refetch only when stale).
// On desktop, always refetch on mount for freshest data ('always' = refetch even if fresh).
// This prevents unnecessary network requests when navigating between
// routes, which was causing blank screen flickers on mobile.
refetchOnMount: isMobileDevice ? true : 'always',
// Keep previous data visible while refetching to prevent blank flashes.
// This is especially important on mobile where network is slower.
placeholderData: isMobileDevice ? keepPreviousData : undefined,
},
mutations: {
onError: handleMutationError,
retry: false, // Don't auto-retry mutations
},
},
});
/**
* Set up global query error handling
* This catches errors that aren't handled by individual queries
*/
queryClient.getQueryCache().subscribe((event) => {
if (event.type === 'updated' && event.query.state.status === 'error') {
const error = event.query.state.error;
if (error instanceof Error) {
handleQueryError(error);
}
}
});