2026-01-13 19:49:51 +03:00
|
|
|
import { QueryClient } from '@tanstack/query-core';
|
2026-06-01 11:49:53 +03:00
|
|
|
import { NonRetryableError } from './nonRetryableError';
|
2026-05-28 21:37:23 +03:00
|
|
|
|
2026-05-24 20:33:46 +03:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
|
2026-06-02 23:13:03 +03:00
|
|
|
let queryClientInstance: QueryClient | undefined;
|
|
|
|
|
|
2026-01-13 19:49:51 +03:00
|
|
|
/**
|
2026-06-02 23:13:03 +03:00
|
|
|
* Shared TanStack Query client (lazy singleton).
|
2026-03-02 22:19:13 +03:00
|
|
|
*
|
2026-06-02 23:13:03 +03:00
|
|
|
* 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.
|
2026-03-02 22:19:13 +03:00
|
|
|
*
|
|
|
|
|
* 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
|
2026-01-13 19:49:51 +03:00
|
|
|
*/
|
2026-06-02 23:13:03 +03:00
|
|
|
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),
|
2026-05-28 21:37:23 +03:00
|
|
|
},
|
2026-01-13 19:49:51 +03:00
|
|
|
},
|
2026-06-02 23:13:03 +03:00
|
|
|
}));
|
|
|
|
|
}
|