Refactor/reacrhitecture to fsd+ #49
@@ -6,7 +6,7 @@
|
|||||||
descendants of this provider.
|
descendants of this provider.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { queryClient } from '$shared/api/queryClient';
|
import { getQueryClient } from '$shared/api/queryClient';
|
||||||
import { QueryClientProvider } from '@tanstack/svelte-query';
|
import { QueryClientProvider } from '@tanstack/svelte-query';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
@@ -18,6 +18,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { children }: Props = $props();
|
let { children }: Props = $props();
|
||||||
|
|
||||||
|
// First call to the lazy singleton — constructs the shared client for the app.
|
||||||
|
const queryClient = getQueryClient();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ vi.mock('$shared/api/api', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { api } from '$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 { fontKeys } from '$shared/api/queryKeys';
|
||||||
import { FontResponseError } from '../../lib/errors/errors';
|
import { FontResponseError } from '../../lib/errors/errors';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from '$shared/api/api';
|
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 { fontKeys } from '$shared/api/queryKeys';
|
||||||
import { buildQueryString } from '$shared/lib/utils';
|
import { buildQueryString } from '$shared/lib/utils';
|
||||||
import type { QueryParams } 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 {
|
export function seedFontCache(fonts: UnifiedFont[]): void {
|
||||||
fonts.forEach(font => {
|
fonts.forEach(font => {
|
||||||
queryClient.setQueryData(fontKeys.detail(font.id), font);
|
getQueryClient().setQueryData(fontKeys.detail(font.id), font);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,18 +27,21 @@ vi.mock('$shared/api/queryClient', async importOriginal => {
|
|||||||
*/
|
*/
|
||||||
const { QueryClient } = await import('@tanstack/query-core');
|
const { QueryClient } = await import('@tanstack/query-core');
|
||||||
const actual = await importOriginal<typeof import('$shared/api/queryClient')>();
|
const actual = await importOriginal<typeof import('$shared/api/queryClient')>();
|
||||||
|
const mockClient = new QueryClient({
|
||||||
|
defaultOptions: { queries: { retry: 0, gcTime: 0 } },
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
queryClient: new QueryClient({
|
getQueryClient: () => mockClient,
|
||||||
defaultOptions: { queries: { retry: 0, gcTime: 0 } },
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
vi.mock('../../../api', () => ({ fetchProxyFonts: vi.fn() }));
|
vi.mock('../../../api', () => ({ fetchProxyFonts: vi.fn() }));
|
||||||
|
|
||||||
import { queryClient } from '$shared/api/queryClient';
|
import { getQueryClient } from '$shared/api/queryClient';
|
||||||
import { fetchProxyFonts } from '../../../api';
|
import { fetchProxyFonts } from '../../../api';
|
||||||
|
|
||||||
|
const queryClient = getQueryClient();
|
||||||
|
|
||||||
const fetch = fetchProxyFonts as ReturnType<typeof vi.fn>;
|
const fetch = fetchProxyFonts as ReturnType<typeof vi.fn>;
|
||||||
|
|
||||||
type FontPage = { fonts: UnifiedFont[]; total: number; limit: number; offset: number };
|
type FontPage = { fonts: UnifiedFont[]; total: number; limit: number; offset: number };
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
DEFAULT_QUERY_GC_TIME_MS,
|
DEFAULT_QUERY_GC_TIME_MS,
|
||||||
DEFAULT_QUERY_STALE_TIME_MS,
|
DEFAULT_QUERY_STALE_TIME_MS,
|
||||||
queryClient,
|
getQueryClient,
|
||||||
} from '$shared/api/queryClient';
|
} from '$shared/api/queryClient';
|
||||||
import {
|
import {
|
||||||
type InfiniteData,
|
type InfiniteData,
|
||||||
@@ -46,7 +46,7 @@ export class FontCatalogStore {
|
|||||||
readonly unknown[],
|
readonly unknown[],
|
||||||
PageParam
|
PageParam
|
||||||
>;
|
>;
|
||||||
#qc = queryClient;
|
#qc = getQueryClient();
|
||||||
#unsubscribe: () => void;
|
#unsubscribe: () => void;
|
||||||
|
|
||||||
constructor(params: FontStoreParams = {}) {
|
constructor(params: FontStoreParams = {}) {
|
||||||
|
|||||||
@@ -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 { fontKeys } from '$shared/api/queryKeys';
|
||||||
import {
|
import {
|
||||||
beforeEach,
|
beforeEach,
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@ import type { FilterMetadata } from '$features/FilterAndSortFonts/api/filters/fi
|
|||||||
import {
|
import {
|
||||||
DEFAULT_QUERY_GC_TIME_MS,
|
DEFAULT_QUERY_GC_TIME_MS,
|
||||||
DEFAULT_QUERY_STALE_TIME_MS,
|
DEFAULT_QUERY_STALE_TIME_MS,
|
||||||
queryClient,
|
getQueryClient,
|
||||||
} from '$shared/api/queryClient';
|
} from '$shared/api/queryClient';
|
||||||
import {
|
import {
|
||||||
type QueryKey,
|
type QueryKey,
|
||||||
@@ -49,7 +49,7 @@ export class AvailableFilterStore {
|
|||||||
/**
|
/**
|
||||||
* Shared query client
|
* Shared query client
|
||||||
*/
|
*/
|
||||||
protected qc = queryClient;
|
protected qc = getQueryClient();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new filters store
|
* Creates a new filters store
|
||||||
|
|||||||
+3
-1
@@ -1,4 +1,6 @@
|
|||||||
import { queryClient } from '$shared/api/queryClient';
|
import { getQueryClient } from '$shared/api/queryClient';
|
||||||
|
|
||||||
|
const queryClient = getQueryClient();
|
||||||
import {
|
import {
|
||||||
afterEach,
|
afterEach,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
|
|||||||
@@ -27,11 +27,16 @@ export const QUERY_RETRY_BASE_DELAY_MS = 1000;
|
|||||||
*/
|
*/
|
||||||
export const QUERY_RETRY_MAX_DELAY_MS = 30000;
|
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.
|
* Construction is deferred to the first call so importing this module is inert:
|
||||||
* Used by all font stores for data fetching and caching.
|
* 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:
|
* Cache behavior:
|
||||||
* - Data stays fresh for 5 minutes (staleTime)
|
* - 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)
|
* - No refetch on window focus (reduces unnecessary network requests)
|
||||||
* - 3 retries with exponential backoff on failure
|
* - 3 retries with exponential backoff on failure
|
||||||
*/
|
*/
|
||||||
export const queryClient = new QueryClient({
|
export function getQueryClient(): QueryClient {
|
||||||
defaultOptions: {
|
return (queryClientInstance ??= new QueryClient({
|
||||||
queries: {
|
defaultOptions: {
|
||||||
staleTime: DEFAULT_QUERY_STALE_TIME_MS,
|
queries: {
|
||||||
gcTime: DEFAULT_QUERY_GC_TIME_MS,
|
staleTime: DEFAULT_QUERY_STALE_TIME_MS,
|
||||||
/**
|
gcTime: DEFAULT_QUERY_GC_TIME_MS,
|
||||||
* Don't refetch when window regains focus
|
/**
|
||||||
*/
|
* Don't refetch when window regains focus
|
||||||
refetchOnWindowFocus: false,
|
*/
|
||||||
/**
|
refetchOnWindowFocus: false,
|
||||||
* Refetch on mount if data is stale
|
/**
|
||||||
*/
|
* Refetch on mount if data is stale
|
||||||
refetchOnMount: true,
|
*/
|
||||||
retry: (failureCount, error) => {
|
refetchOnMount: true,
|
||||||
if (error instanceof NonRetryableError) {
|
retry: (failureCount, error) => {
|
||||||
return false;
|
if (error instanceof NonRetryableError) {
|
||||||
}
|
return false;
|
||||||
return failureCount < QUERY_RETRY_COUNT;
|
}
|
||||||
|
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 {
|
import {
|
||||||
QueryObserver,
|
QueryObserver,
|
||||||
type QueryObserverOptions,
|
type QueryObserverOptions,
|
||||||
@@ -20,7 +20,7 @@ export abstract class BaseQueryStore<TData, TError = Error> {
|
|||||||
#unsubscribe: () => void;
|
#unsubscribe: () => void;
|
||||||
|
|
||||||
constructor(options: QueryObserverOptions<TData, TError, TData, any, any>) {
|
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.#unsubscribe = this.#observer.subscribe(result => {
|
||||||
this.#result = result;
|
this.#result = result;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { queryClient } from '$shared/api/queryClient';
|
import { getQueryClient } from '$shared/api/queryClient';
|
||||||
|
|
||||||
|
const queryClient = getQueryClient();
|
||||||
import {
|
import {
|
||||||
beforeEach,
|
beforeEach,
|
||||||
describe,
|
describe,
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
import type { UnifiedFont } from '$entities/Font';
|
import type { UnifiedFont } from '$entities/Font';
|
||||||
import { UNIFIED_FONTS } from '$entities/Font/testing';
|
import { UNIFIED_FONTS } from '$entities/Font/testing';
|
||||||
import { queryClient } from '$shared/api/queryClient';
|
import { getQueryClient } from '$shared/api/queryClient';
|
||||||
|
|
||||||
|
const queryClient = getQueryClient();
|
||||||
import {
|
import {
|
||||||
beforeEach,
|
beforeEach,
|
||||||
describe,
|
describe,
|
||||||
|
|||||||
@@ -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 * as matchers from '@testing-library/jest-dom/matchers';
|
||||||
import { cleanup } from '@testing-library/svelte';
|
import { cleanup } from '@testing-library/svelte';
|
||||||
import {
|
import {
|
||||||
@@ -14,7 +14,7 @@ expect.extend(matchers);
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
queryClient.clear();
|
getQueryClient().clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock window.matchMedia for components that use it
|
// Mock window.matchMedia for components that use it
|
||||||
|
|||||||
Reference in New Issue
Block a user