Refactor/reacrhitecture to fsd+ #49

Merged
ilia merged 70 commits from refactor/reacrhitecture-to-fsd+ into main 2026-06-03 09:55:47 +00:00
13 changed files with 71 additions and 48 deletions
Showing only changes of commit 60e115309c - Show all commits
+4 -1
View File
@@ -6,7 +6,7 @@
descendants of this provider.
-->
<script lang="ts">
import { queryClient } from '$shared/api/queryClient';
import { getQueryClient } from '$shared/api/queryClient';
import { QueryClientProvider } from '@tanstack/svelte-query';
import type { Snippet } from 'svelte';
@@ -18,6 +18,9 @@ interface Props {
}
let { children }: Props = $props();
// First call to the lazy singleton — constructs the shared client for the app.
const queryClient = getQueryClient();
</script>
<QueryClientProvider client={queryClient}>
@@ -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 {
+2 -2
View File
@@ -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);
});
}
@@ -27,18 +27,21 @@ vi.mock('$shared/api/queryClient', async importOriginal => {
*/
const { QueryClient } = await import('@tanstack/query-core');
const actual = await importOriginal<typeof import('$shared/api/queryClient')>();
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<typeof vi.fn>;
type FontPage = { fonts: UnifiedFont[]; total: number; limit: number; offset: number };
@@ -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 = {}) {
@@ -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,
@@ -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
@@ -1,4 +1,6 @@
import { queryClient } from '$shared/api/queryClient';
import { getQueryClient } from '$shared/api/queryClient';
const queryClient = getQueryClient();
import {
afterEach,
beforeEach,
+35 -28
View File
@@ -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,
@@ -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,
+2 -2
View File
@@ -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