refactor(shared/api): lazify queryClient to remove eager-construction footgun
queryClient.ts constructed the TanStack client at module eval but was not in the sideEffects allowlist, so Rollup treated the module as pure — safe only while a value-importer keeps it alive (D-3). - replace the eager `queryClient` singleton with a memoized getQueryClient() factory; construction is deferred to first call - the module is now genuinely side-effect-free, so no sideEffects exception is needed and construction can never be legally tree-shaken away - route all consumers through getQueryClient() (QueryProvider as first caller; stores via class fields/observers; tests via a local alias; the fontCatalogStore spec mock now overrides getQueryClient)
This commit is contained in:
@@ -27,11 +27,16 @@ export const QUERY_RETRY_BASE_DELAY_MS = 1000;
|
||||
*/
|
||||
export const QUERY_RETRY_MAX_DELAY_MS = 30000;
|
||||
|
||||
let queryClientInstance: QueryClient | undefined;
|
||||
|
||||
/**
|
||||
* TanStack Query client instance
|
||||
* Shared TanStack Query client (lazy singleton).
|
||||
*
|
||||
* Configured for optimal caching and refetching behavior.
|
||||
* Used by all font stores for data fetching and caching.
|
||||
* 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)
|
||||
@@ -39,30 +44,32 @@ export const QUERY_RETRY_MAX_DELAY_MS = 30000;
|
||||
* - No refetch on window focus (reduces unnecessary network requests)
|
||||
* - 3 retries with exponential backoff on failure
|
||||
*/
|
||||
export const queryClient = 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;
|
||||
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),
|
||||
},
|
||||
/**
|
||||
* 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),
|
||||
},
|
||||
},
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { queryClient } from '$shared/api/queryClient';
|
||||
import { getQueryClient } from '$shared/api/queryClient';
|
||||
import {
|
||||
QueryObserver,
|
||||
type QueryObserverOptions,
|
||||
@@ -20,7 +20,7 @@ export abstract class BaseQueryStore<TData, TError = Error> {
|
||||
#unsubscribe: () => void;
|
||||
|
||||
constructor(options: QueryObserverOptions<TData, TError, TData, any, any>) {
|
||||
this.#observer = new QueryObserver(queryClient, options);
|
||||
this.#observer = new QueryObserver(getQueryClient(), options);
|
||||
this.#unsubscribe = this.#observer.subscribe(result => {
|
||||
this.#result = result;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { queryClient } from '$shared/api/queryClient';
|
||||
import { getQueryClient } from '$shared/api/queryClient';
|
||||
|
||||
const queryClient = getQueryClient();
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
|
||||
Reference in New Issue
Block a user