import { QueryClient } from '@tanstack/query-core'; import { NonRetryableError } from './nonRetryableError'; /** * Data remains fresh for this long after fetch. Stores that override * staleness (e.g. filtered queries) can use 0 to bypass. */ export const DEFAULT_QUERY_STALE_TIME_MS = 5 * 60 * 1000; /** * Unused cache entries are garbage collected after this long. */ export const DEFAULT_QUERY_GC_TIME_MS = 10 * 60 * 1000; /** * How many times a failed query is retried before surfacing the error. */ export const QUERY_RETRY_COUNT = 3; /** * Base delay for exponential retry backoff. */ export const QUERY_RETRY_BASE_DELAY_MS = 1000; /** * Upper bound on retry delay regardless of attempt index. */ export const QUERY_RETRY_MAX_DELAY_MS = 30000; let queryClientInstance: QueryClient | undefined; /** * Shared TanStack Query client (lazy singleton). * * Construction is deferred to the first call so importing this module is inert: * module eval runs no `new QueryClient()`, so the module is genuinely * side-effect-free and needs no `sideEffects` allowlist exception. The * app-layer `QueryProvider` is the first caller; every store reuses the same * instance. Matches the lazy-accessor pattern used by the font stores. * * Cache behavior: * - Data stays fresh for 5 minutes (staleTime) * - Unused data is garbage collected after 10 minutes (gcTime) * - No refetch on window focus (reduces unnecessary network requests) * - 3 retries with exponential backoff on failure */ export function getQueryClient(): QueryClient { return (queryClientInstance ??= new QueryClient({ defaultOptions: { queries: { staleTime: DEFAULT_QUERY_STALE_TIME_MS, gcTime: DEFAULT_QUERY_GC_TIME_MS, /** * Don't refetch when window regains focus */ refetchOnWindowFocus: false, /** * Refetch on mount if data is stale */ refetchOnMount: true, retry: (failureCount, error) => { if (error instanceof NonRetryableError) { return false; } return failureCount < QUERY_RETRY_COUNT; }, /** * Exponential backoff: 1s, 2s, 4s, 8s... capped at 30s */ retryDelay: attemptIndex => Math.min(QUERY_RETRY_BASE_DELAY_MS * 2 ** attemptIndex, QUERY_RETRY_MAX_DELAY_MS), }, }, })); }