diff --git a/src/app/providers/QueryProvider.svelte b/src/app/providers/QueryProvider.svelte index 8806e54..eeb42b7 100644 --- a/src/app/providers/QueryProvider.svelte +++ b/src/app/providers/QueryProvider.svelte @@ -6,7 +6,7 @@ descendants of this provider. --> diff --git a/src/entities/Font/api/proxy/proxyFonts.test.ts b/src/entities/Font/api/proxy/proxyFonts.test.ts index 10bbaa8..a6ccc67 100644 --- a/src/entities/Font/api/proxy/proxyFonts.test.ts +++ b/src/entities/Font/api/proxy/proxyFonts.test.ts @@ -19,7 +19,9 @@ vi.mock('$shared/api/api', () => ({ })); import { api } from '$shared/api/api'; -import { queryClient } from '$shared/api/queryClient'; +import { getQueryClient } from '$shared/api/queryClient'; + +const queryClient = getQueryClient(); import { fontKeys } from '$shared/api/queryKeys'; import { FontResponseError } from '../../lib/errors/errors'; import { diff --git a/src/entities/Font/api/proxy/proxyFonts.ts b/src/entities/Font/api/proxy/proxyFonts.ts index 5b953de..515c4c8 100644 --- a/src/entities/Font/api/proxy/proxyFonts.ts +++ b/src/entities/Font/api/proxy/proxyFonts.ts @@ -11,7 +11,7 @@ */ import { api } from '$shared/api/api'; -import { queryClient } from '$shared/api/queryClient'; +import { getQueryClient } from '$shared/api/queryClient'; import { fontKeys } from '$shared/api/queryKeys'; import { buildQueryString } from '$shared/lib/utils'; import type { QueryParams } from '$shared/lib/utils'; @@ -26,7 +26,7 @@ import type { UnifiedFont } from '../../model/types'; */ export function seedFontCache(fonts: UnifiedFont[]): void { fonts.forEach(font => { - queryClient.setQueryData(fontKeys.detail(font.id), font); + getQueryClient().setQueryData(fontKeys.detail(font.id), font); }); } diff --git a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.spec.ts b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.spec.ts index 230512e..278e8e0 100644 --- a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.spec.ts +++ b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.spec.ts @@ -27,18 +27,21 @@ vi.mock('$shared/api/queryClient', async importOriginal => { */ const { QueryClient } = await import('@tanstack/query-core'); const actual = await importOriginal(); + const mockClient = new QueryClient({ + defaultOptions: { queries: { retry: 0, gcTime: 0 } }, + }); return { ...actual, - queryClient: new QueryClient({ - defaultOptions: { queries: { retry: 0, gcTime: 0 } }, - }), + getQueryClient: () => mockClient, }; }); vi.mock('../../../api', () => ({ fetchProxyFonts: vi.fn() })); -import { queryClient } from '$shared/api/queryClient'; +import { getQueryClient } from '$shared/api/queryClient'; import { fetchProxyFonts } from '../../../api'; +const queryClient = getQueryClient(); + const fetch = fetchProxyFonts as ReturnType; type FontPage = { fonts: UnifiedFont[]; total: number; limit: number; offset: number }; diff --git a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts index 72b3d56..ceff630 100644 --- a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts +++ b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts @@ -1,7 +1,7 @@ import { DEFAULT_QUERY_GC_TIME_MS, DEFAULT_QUERY_STALE_TIME_MS, - queryClient, + getQueryClient, } from '$shared/api/queryClient'; import { type InfiniteData, @@ -46,7 +46,7 @@ export class FontCatalogStore { readonly unknown[], PageParam >; - #qc = queryClient; + #qc = getQueryClient(); #unsubscribe: () => void; constructor(params: FontStoreParams = {}) { diff --git a/src/entities/Font/model/store/fontsByIdsStore/fontsByIdsStore.test.ts b/src/entities/Font/model/store/fontsByIdsStore/fontsByIdsStore.test.ts index 0acf5a9..16ce2fe 100644 --- a/src/entities/Font/model/store/fontsByIdsStore/fontsByIdsStore.test.ts +++ b/src/entities/Font/model/store/fontsByIdsStore/fontsByIdsStore.test.ts @@ -1,4 +1,6 @@ -import { queryClient } from '$shared/api/queryClient'; +import { getQueryClient } from '$shared/api/queryClient'; + +const queryClient = getQueryClient(); import { fontKeys } from '$shared/api/queryKeys'; import { beforeEach, diff --git a/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.svelte.ts b/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.svelte.ts index 265b153..02d2c3f 100644 --- a/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.svelte.ts +++ b/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.svelte.ts @@ -20,7 +20,7 @@ import type { FilterMetadata } from '$features/FilterAndSortFonts/api/filters/fi import { DEFAULT_QUERY_GC_TIME_MS, DEFAULT_QUERY_STALE_TIME_MS, - queryClient, + getQueryClient, } from '$shared/api/queryClient'; import { type QueryKey, @@ -49,7 +49,7 @@ export class AvailableFilterStore { /** * Shared query client */ - protected qc = queryClient; + protected qc = getQueryClient(); /** * Creates a new filters store diff --git a/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.test.ts b/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.test.ts index 8f489d4..e36fe76 100644 --- a/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.test.ts +++ b/src/features/FilterAndSortFonts/model/store/availableFilterStore/availableFilterStore.test.ts @@ -1,4 +1,6 @@ -import { queryClient } from '$shared/api/queryClient'; +import { getQueryClient } from '$shared/api/queryClient'; + +const queryClient = getQueryClient(); import { afterEach, beforeEach, diff --git a/src/shared/api/queryClient.ts b/src/shared/api/queryClient.ts index 1e921e5..95ba830 100644 --- a/src/shared/api/queryClient.ts +++ b/src/shared/api/queryClient.ts @@ -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), }, - }, -}); + })); +} diff --git a/src/shared/lib/helpers/BaseQueryStore/BaseQueryStore.svelte.ts b/src/shared/lib/helpers/BaseQueryStore/BaseQueryStore.svelte.ts index c50cb8a..ee0a3ea 100644 --- a/src/shared/lib/helpers/BaseQueryStore/BaseQueryStore.svelte.ts +++ b/src/shared/lib/helpers/BaseQueryStore/BaseQueryStore.svelte.ts @@ -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 { #unsubscribe: () => void; constructor(options: QueryObserverOptions) { - this.#observer = new QueryObserver(queryClient, options); + this.#observer = new QueryObserver(getQueryClient(), options); this.#unsubscribe = this.#observer.subscribe(result => { this.#result = result; }); diff --git a/src/shared/lib/helpers/BaseQueryStore/BaseQueryStore.test.ts b/src/shared/lib/helpers/BaseQueryStore/BaseQueryStore.test.ts index 00b8ea0..31654c7 100644 --- a/src/shared/lib/helpers/BaseQueryStore/BaseQueryStore.test.ts +++ b/src/shared/lib/helpers/BaseQueryStore/BaseQueryStore.test.ts @@ -1,4 +1,6 @@ -import { queryClient } from '$shared/api/queryClient'; +import { getQueryClient } from '$shared/api/queryClient'; + +const queryClient = getQueryClient(); import { beforeEach, describe, diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts index 323e0b3..b151733 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts @@ -11,7 +11,9 @@ import type { UnifiedFont } from '$entities/Font'; import { UNIFIED_FONTS } from '$entities/Font/testing'; -import { queryClient } from '$shared/api/queryClient'; +import { getQueryClient } from '$shared/api/queryClient'; + +const queryClient = getQueryClient(); import { beforeEach, describe, diff --git a/vitest.setup.component.ts b/vitest.setup.component.ts index 32236e5..001a965 100644 --- a/vitest.setup.component.ts +++ b/vitest.setup.component.ts @@ -1,4 +1,4 @@ -import { queryClient } from '$shared/api/queryClient'; +import { getQueryClient } from '$shared/api/queryClient'; import * as matchers from '@testing-library/jest-dom/matchers'; import { cleanup } from '@testing-library/svelte'; import { @@ -14,7 +14,7 @@ expect.extend(matchers); afterEach(() => { cleanup(); - queryClient.clear(); + getQueryClient().clear(); }); // Mock window.matchMedia for components that use it