From 07d044f4d6acd9550fc2c887464da0d5719bc8c5 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Sun, 24 May 2026 19:41:40 +0300 Subject: [PATCH] refactor: extract BatchFontStore into new FetchFontsByIds feature The byId font fetch was a verb-oriented capability with a single consumer driven by a feature need (materializing comparison picks). That shape belongs at the feature layer, not on the entity. Move: - entities/Font/model/store/batchFontStore -> features/FetchFontsByIds/model/store/fontsByIdsStore - Class BatchFontStore -> FontsByIdsStore entities/Font retains the transport primitives (fetchFontsByIds, seedFontCache) and the keyspace (fontKeys); the feature wraps them in the reactive store. comparisonStore now imports FontsByIdsStore from the new feature. The proxy API is imported via direct path so vi.spyOn on the source module still observes the call. --- src/entities/Font/api/index.ts | 1 + src/entities/Font/model/store/index.ts | 3 -- src/features/FetchFontsByIds/index.ts | 1 + src/features/FetchFontsByIds/model/index.ts | 1 + .../fontsByIdsStore.svelte.ts} | 17 +++++------ .../fontsByIdsStore/fontsByIdsStore.test.ts} | 30 +++++++++---------- .../model/stores/comparisonStore.svelte.ts | 18 +++++------ .../model/stores/comparisonStore.test.ts | 8 ++--- 8 files changed, 37 insertions(+), 42 deletions(-) create mode 100644 src/features/FetchFontsByIds/index.ts create mode 100644 src/features/FetchFontsByIds/model/index.ts rename src/{entities/Font/model/store/batchFontStore/batchFontStore.svelte.ts => features/FetchFontsByIds/model/store/fontsByIdsStore/fontsByIdsStore.svelte.ts} (84%) rename src/{entities/Font/model/store/batchFontStore/batchFontStore.test.ts => features/FetchFontsByIds/model/store/fontsByIdsStore/fontsByIdsStore.test.ts} (85%) diff --git a/src/entities/Font/api/index.ts b/src/entities/Font/api/index.ts index 0a1dde1..aad2be4 100644 --- a/src/entities/Font/api/index.ts +++ b/src/entities/Font/api/index.ts @@ -9,6 +9,7 @@ export { fetchFontsByIds, fetchProxyFontById, fetchProxyFonts, + seedFontCache, } from './proxy/proxyFonts'; export type { ProxyFontsParams, diff --git a/src/entities/Font/model/store/index.ts b/src/entities/Font/model/store/index.ts index 48dd140..bc6e56a 100644 --- a/src/entities/Font/model/store/index.ts +++ b/src/entities/Font/model/store/index.ts @@ -1,9 +1,6 @@ // Applied fonts manager export * from './appliedFontsStore/appliedFontsStore.svelte'; -// Batch font store -export { BatchFontStore } from './batchFontStore/batchFontStore.svelte'; - // Single FontStore export { createFontStore, diff --git a/src/features/FetchFontsByIds/index.ts b/src/features/FetchFontsByIds/index.ts new file mode 100644 index 0000000..600c651 --- /dev/null +++ b/src/features/FetchFontsByIds/index.ts @@ -0,0 +1 @@ +export { FontsByIdsStore } from './model'; diff --git a/src/features/FetchFontsByIds/model/index.ts b/src/features/FetchFontsByIds/model/index.ts new file mode 100644 index 0000000..c449f87 --- /dev/null +++ b/src/features/FetchFontsByIds/model/index.ts @@ -0,0 +1 @@ +export { FontsByIdsStore } from './store/fontsByIdsStore/fontsByIdsStore.svelte'; diff --git a/src/entities/Font/model/store/batchFontStore/batchFontStore.svelte.ts b/src/features/FetchFontsByIds/model/store/fontsByIdsStore/fontsByIdsStore.svelte.ts similarity index 84% rename from src/entities/Font/model/store/batchFontStore/batchFontStore.svelte.ts rename to src/features/FetchFontsByIds/model/store/fontsByIdsStore/fontsByIdsStore.svelte.ts index 7b0e9fe..9b177a8 100644 --- a/src/entities/Font/model/store/batchFontStore/batchFontStore.svelte.ts +++ b/src/features/FetchFontsByIds/model/store/fontsByIdsStore/fontsByIdsStore.svelte.ts @@ -1,14 +1,14 @@ -import { fontKeys } from '$shared/api/queryKeys'; -import { BaseQueryStore } from '$shared/lib/helpers/BaseQueryStore.svelte'; import { fetchFontsByIds, seedFontCache, -} from '../../../api/proxy/proxyFonts'; +} from '$entities/Font/api/proxy/proxyFonts'; import { FontNetworkError, FontResponseError, -} from '../../../lib/errors/errors'; -import type { UnifiedFont } from '../../types'; +} from '$entities/Font/lib/errors/errors'; +import type { UnifiedFont } from '$entities/Font/model/types'; +import { fontKeys } from '$shared/api/queryKeys'; +import { BaseQueryStore } from '$shared/lib/helpers/BaseQueryStore.svelte'; /** * Internal fetcher that seeds the cache and handles error wrapping. @@ -35,11 +35,10 @@ async function fetchAndSeed(ids: string[]): Promise { } /** - * Reactive store for fetching and caching batches of fonts by ID. - * Integrates with TanStack Query via BaseQueryStore and handles - * normalized cache seeding. + * Reactive store for fetching specific fonts by ID via the proxy batch endpoint. + * Wraps TanStack Query and seeds the detail cache for sibling consumers. */ -export class BatchFontStore extends BaseQueryStore { +export class FontsByIdsStore extends BaseQueryStore { constructor(initialIds: string[] = []) { super({ queryKey: fontKeys.batch(initialIds), diff --git a/src/entities/Font/model/store/batchFontStore/batchFontStore.test.ts b/src/features/FetchFontsByIds/model/store/fontsByIdsStore/fontsByIdsStore.test.ts similarity index 85% rename from src/entities/Font/model/store/batchFontStore/batchFontStore.test.ts rename to src/features/FetchFontsByIds/model/store/fontsByIdsStore/fontsByIdsStore.test.ts index 3b112ac..7849535 100644 --- a/src/entities/Font/model/store/batchFontStore/batchFontStore.test.ts +++ b/src/features/FetchFontsByIds/model/store/fontsByIdsStore/fontsByIdsStore.test.ts @@ -1,3 +1,8 @@ +import * as api from '$entities/Font/api/proxy/proxyFonts'; +import { + FontNetworkError, + FontResponseError, +} from '$entities/Font/lib/errors/errors'; import { queryClient } from '$shared/api/queryClient'; import { fontKeys } from '$shared/api/queryKeys'; import { @@ -7,14 +12,9 @@ import { it, vi, } from 'vitest'; -import * as api from '../../../api/proxy/proxyFonts'; -import { - FontNetworkError, - FontResponseError, -} from '../../../lib/errors/errors'; -import { BatchFontStore } from './batchFontStore.svelte'; +import { FontsByIdsStore } from './fontsByIdsStore.svelte'; -describe('BatchFontStore', () => { +describe('FontsByIdsStore', () => { beforeEach(() => { queryClient.clear(); vi.clearAllMocks(); @@ -23,7 +23,7 @@ describe('BatchFontStore', () => { describe('Fetch Behavior', () => { it('should skip fetch when initialized with empty IDs', async () => { const spy = vi.spyOn(api, 'fetchFontsByIds'); - const store = new BatchFontStore([]); + const store = new FontsByIdsStore([]); expect(spy).not.toHaveBeenCalled(); expect(store.fonts).toEqual([]); }); @@ -31,7 +31,7 @@ describe('BatchFontStore', () => { it('should fetch and seed cache for valid IDs', async () => { const fonts = [{ id: 'a', name: 'A' }] as any[]; vi.spyOn(api, 'fetchFontsByIds').mockResolvedValue(fonts); - const store = new BatchFontStore(['a']); + const store = new FontsByIdsStore(['a']); await vi.waitFor(() => expect(store.fonts).toEqual(fonts), { timeout: 1000 }); expect(queryClient.getQueryData(fontKeys.detail('a'))).toEqual(fonts[0]); }); @@ -42,7 +42,7 @@ describe('BatchFontStore', () => { vi.spyOn(api, 'fetchFontsByIds').mockImplementation(() => new Promise(r => setTimeout(() => r([{ id: 'a' }] as any), 50)) ); - const store = new BatchFontStore(['a']); + const store = new FontsByIdsStore(['a']); expect(store.isLoading).toBe(true); await vi.waitFor(() => expect(store.isLoading).toBe(false), { timeout: 1000 }); }); @@ -51,7 +51,7 @@ describe('BatchFontStore', () => { describe('Error Handling', () => { it('should wrap network failures in FontNetworkError', async () => { vi.spyOn(api, 'fetchFontsByIds').mockRejectedValue(new Error('Network fail')); - const store = new BatchFontStore(['a']); + const store = new FontsByIdsStore(['a']); await vi.waitFor(() => expect(store.isError).toBe(true), { timeout: 1000 }); expect(store.error).toBeInstanceOf(FontNetworkError); }); @@ -59,7 +59,7 @@ describe('BatchFontStore', () => { it('should handle malformed API responses with FontResponseError', async () => { // Mocking a malformed response that the store should validate vi.spyOn(api, 'fetchFontsByIds').mockResolvedValue(null as any); - const store = new BatchFontStore(['a']); + const store = new FontsByIdsStore(['a']); await vi.waitFor(() => expect(store.isError).toBe(true), { timeout: 1000 }); expect(store.error).toBeInstanceOf(FontResponseError); }); @@ -67,7 +67,7 @@ describe('BatchFontStore', () => { it('should have null error in success state', async () => { const fonts = [{ id: 'a' }] as any[]; vi.spyOn(api, 'fetchFontsByIds').mockResolvedValue(fonts); - const store = new BatchFontStore(['a']); + const store = new FontsByIdsStore(['a']); await vi.waitFor(() => expect(store.fonts).toEqual(fonts), { timeout: 1000 }); expect(store.error).toBeNull(); }); @@ -78,7 +78,7 @@ describe('BatchFontStore', () => { const fonts1 = [{ id: 'a' }] as any[]; const spy = vi.spyOn(api, 'fetchFontsByIds').mockResolvedValueOnce(fonts1); - const store = new BatchFontStore(['a']); + const store = new FontsByIdsStore(['a']); await vi.waitFor(() => expect(store.fonts).toEqual(fonts1), { timeout: 1000 }); spy.mockClear(); @@ -97,7 +97,7 @@ describe('BatchFontStore', () => { .mockResolvedValueOnce(fonts1) .mockResolvedValueOnce(fonts2); - const store = new BatchFontStore(['a']); + const store = new FontsByIdsStore(['a']); await vi.waitFor(() => expect(store.fonts).toEqual(fonts1), { timeout: 1000 }); store.setIds(['b']); diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts index cf19174..1c6bc3c 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts @@ -7,14 +7,13 @@ * * Features: * - Persistent font selection (survives page refresh) - * - Font loading state tracking via BatchFontStore + TanStack Query + * - Font loading state tracking via FontsByIdsStore + TanStack Query * - Sample text management * - Typography controls (size, weight, line height, spacing) * - Slider position for character-by-character morphing */ import { - BatchFontStore, type FontLoadRequestConfig, type UnifiedFont, appliedFontsManager, @@ -22,6 +21,7 @@ import { getFontUrl, } from '$entities/Font'; import { typographySettingsStore } from '$features/AdjustTypography/model'; +import { FontsByIdsStore } from '$features/FetchFontsByIds'; import { createPersistentStore } from '$shared/lib'; import { untrack } from 'svelte'; import { getPretextFontString } from '../../lib'; @@ -51,7 +51,7 @@ const storage = createPersistentStore('glyphdiff:comparison', { /** * Store for managing font comparison state. * - * Uses BatchFontStore (TanStack Query) to fetch fonts by ID, replacing + * Uses FontsByIdsStore (TanStack Query) to fetch fonts by ID, replacing * the previous hand-rolled async fetch approach. Three reactive effects * handle: (1) syncing batch results into fontA/fontB, (2) triggering the * CSS Font Loading API, and (3) falling back to default fonts when @@ -85,17 +85,17 @@ export class ComparisonStore { /** * TanStack Query-backed store for efficient batch font retrieval */ - #batchStore: BatchFontStore; + #fontsByIdsStore: FontsByIdsStore; constructor() { // Synchronously seed the batch store with any IDs already in storage const { fontAId, fontBId } = storage.value; - this.#batchStore = new BatchFontStore(fontAId && fontBId ? [fontAId, fontBId] : []); + this.#fontsByIdsStore = new FontsByIdsStore(fontAId && fontBId ? [fontAId, fontBId] : []); $effect.root(() => { // Effect 1: Sync batch results → fontA / fontB $effect(() => { - const fonts = this.#batchStore.fonts; + const fonts = this.#fontsByIdsStore.fonts; if (fonts.length === 0) { return; } @@ -157,7 +157,7 @@ export class ComparisonStore { const id1 = fonts[0].id; const id2 = fonts[fonts.length - 1].id; storage.value = { fontAId: id1, fontBId: id2 }; - this.#batchStore.setIds([id1, id2]); + this.#fontsByIdsStore.setIds([id1, id2]); }); } }); @@ -316,7 +316,7 @@ export class ComparisonStore { * True if any font is currently being fetched or loaded (reactive) */ get isLoading() { - return this.#batchStore.isLoading || !this.#fontsReady; + return this.#fontsByIdsStore.isLoading || !this.#fontsReady; } /** @@ -325,7 +325,7 @@ export class ComparisonStore { resetAll() { this.#fontA = undefined; this.#fontB = undefined; - this.#batchStore.setIds([]); + this.#fontsByIdsStore.setIds([]); storage.clear(); typographySettingsStore.reset(); } diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts index d42686d..5a3146e 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts @@ -1,7 +1,7 @@ /** * Unit tests for ComparisonStore (TanStack Query refactor) * - * Uses the real BatchFontStore so Svelte $state reactivity works correctly. + * Uses the real FontsByIdsStore so Svelte $state reactivity works correctly. * Controls network behaviour via vi.spyOn on the proxyFonts API layer. */ @@ -53,12 +53,8 @@ vi.mock('$shared/lib/helpers/createPersistentStore/createPersistentStore.svelte' vi.mock('$entities/Font', async importOriginal => { const actual = await importOriginal(); - const { BatchFontStore } = await import( - '$entities/Font/model/store/batchFontStore/batchFontStore.svelte' - ); return { ...actual, - BatchFontStore, fontStore: { fonts: [] }, appliedFontsManager: { touch: vi.fn(), @@ -129,7 +125,7 @@ describe('ComparisonStore', () => { }); }); - describe('Restoration from Storage (via BatchFontStore)', () => { + describe('Restoration from Storage (via FontsByIdsStore)', () => { it('should restore fontA and fontB from stored IDs', async () => { mockStorage._value.fontAId = mockFontA.id; mockStorage._value.fontBId = mockFontB.id;