Chore/architecture refactoring #42

Merged
ilia merged 29 commits from chore/architecture-refactoring into main 2026-05-25 08:43:07 +00:00
8 changed files with 37 additions and 42 deletions
Showing only changes of commit 07d044f4d6 - Show all commits
+1
View File
@@ -9,6 +9,7 @@ export {
fetchFontsByIds, fetchFontsByIds,
fetchProxyFontById, fetchProxyFontById,
fetchProxyFonts, fetchProxyFonts,
seedFontCache,
} from './proxy/proxyFonts'; } from './proxy/proxyFonts';
export type { export type {
ProxyFontsParams, ProxyFontsParams,
-3
View File
@@ -1,9 +1,6 @@
// Applied fonts manager // Applied fonts manager
export * from './appliedFontsStore/appliedFontsStore.svelte'; export * from './appliedFontsStore/appliedFontsStore.svelte';
// Batch font store
export { BatchFontStore } from './batchFontStore/batchFontStore.svelte';
// Single FontStore // Single FontStore
export { export {
createFontStore, createFontStore,
+1
View File
@@ -0,0 +1 @@
export { FontsByIdsStore } from './model';
@@ -0,0 +1 @@
export { FontsByIdsStore } from './store/fontsByIdsStore/fontsByIdsStore.svelte';
@@ -1,14 +1,14 @@
import { fontKeys } from '$shared/api/queryKeys';
import { BaseQueryStore } from '$shared/lib/helpers/BaseQueryStore.svelte';
import { import {
fetchFontsByIds, fetchFontsByIds,
seedFontCache, seedFontCache,
} from '../../../api/proxy/proxyFonts'; } from '$entities/Font/api/proxy/proxyFonts';
import { import {
FontNetworkError, FontNetworkError,
FontResponseError, FontResponseError,
} from '../../../lib/errors/errors'; } from '$entities/Font/lib/errors/errors';
import type { UnifiedFont } from '../../types'; 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. * Internal fetcher that seeds the cache and handles error wrapping.
@@ -35,11 +35,10 @@ async function fetchAndSeed(ids: string[]): Promise<UnifiedFont[]> {
} }
/** /**
* Reactive store for fetching and caching batches of fonts by ID. * Reactive store for fetching specific fonts by ID via the proxy batch endpoint.
* Integrates with TanStack Query via BaseQueryStore and handles * Wraps TanStack Query and seeds the detail cache for sibling consumers.
* normalized cache seeding.
*/ */
export class BatchFontStore extends BaseQueryStore<UnifiedFont[]> { export class FontsByIdsStore extends BaseQueryStore<UnifiedFont[]> {
constructor(initialIds: string[] = []) { constructor(initialIds: string[] = []) {
super({ super({
queryKey: fontKeys.batch(initialIds), queryKey: fontKeys.batch(initialIds),
@@ -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 { queryClient } from '$shared/api/queryClient';
import { fontKeys } from '$shared/api/queryKeys'; import { fontKeys } from '$shared/api/queryKeys';
import { import {
@@ -7,14 +12,9 @@ import {
it, it,
vi, vi,
} from 'vitest'; } from 'vitest';
import * as api from '../../../api/proxy/proxyFonts'; import { FontsByIdsStore } from './fontsByIdsStore.svelte';
import {
FontNetworkError,
FontResponseError,
} from '../../../lib/errors/errors';
import { BatchFontStore } from './batchFontStore.svelte';
describe('BatchFontStore', () => { describe('FontsByIdsStore', () => {
beforeEach(() => { beforeEach(() => {
queryClient.clear(); queryClient.clear();
vi.clearAllMocks(); vi.clearAllMocks();
@@ -23,7 +23,7 @@ describe('BatchFontStore', () => {
describe('Fetch Behavior', () => { describe('Fetch Behavior', () => {
it('should skip fetch when initialized with empty IDs', async () => { it('should skip fetch when initialized with empty IDs', async () => {
const spy = vi.spyOn(api, 'fetchFontsByIds'); const spy = vi.spyOn(api, 'fetchFontsByIds');
const store = new BatchFontStore([]); const store = new FontsByIdsStore([]);
expect(spy).not.toHaveBeenCalled(); expect(spy).not.toHaveBeenCalled();
expect(store.fonts).toEqual([]); expect(store.fonts).toEqual([]);
}); });
@@ -31,7 +31,7 @@ describe('BatchFontStore', () => {
it('should fetch and seed cache for valid IDs', async () => { it('should fetch and seed cache for valid IDs', async () => {
const fonts = [{ id: 'a', name: 'A' }] as any[]; const fonts = [{ id: 'a', name: 'A' }] as any[];
vi.spyOn(api, 'fetchFontsByIds').mockResolvedValue(fonts); 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 }); await vi.waitFor(() => expect(store.fonts).toEqual(fonts), { timeout: 1000 });
expect(queryClient.getQueryData(fontKeys.detail('a'))).toEqual(fonts[0]); expect(queryClient.getQueryData(fontKeys.detail('a'))).toEqual(fonts[0]);
}); });
@@ -42,7 +42,7 @@ describe('BatchFontStore', () => {
vi.spyOn(api, 'fetchFontsByIds').mockImplementation(() => vi.spyOn(api, 'fetchFontsByIds').mockImplementation(() =>
new Promise(r => setTimeout(() => r([{ id: 'a' }] as any), 50)) 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); expect(store.isLoading).toBe(true);
await vi.waitFor(() => expect(store.isLoading).toBe(false), { timeout: 1000 }); await vi.waitFor(() => expect(store.isLoading).toBe(false), { timeout: 1000 });
}); });
@@ -51,7 +51,7 @@ describe('BatchFontStore', () => {
describe('Error Handling', () => { describe('Error Handling', () => {
it('should wrap network failures in FontNetworkError', async () => { it('should wrap network failures in FontNetworkError', async () => {
vi.spyOn(api, 'fetchFontsByIds').mockRejectedValue(new Error('Network fail')); 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 }); await vi.waitFor(() => expect(store.isError).toBe(true), { timeout: 1000 });
expect(store.error).toBeInstanceOf(FontNetworkError); expect(store.error).toBeInstanceOf(FontNetworkError);
}); });
@@ -59,7 +59,7 @@ describe('BatchFontStore', () => {
it('should handle malformed API responses with FontResponseError', async () => { it('should handle malformed API responses with FontResponseError', async () => {
// Mocking a malformed response that the store should validate // Mocking a malformed response that the store should validate
vi.spyOn(api, 'fetchFontsByIds').mockResolvedValue(null as any); 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 }); await vi.waitFor(() => expect(store.isError).toBe(true), { timeout: 1000 });
expect(store.error).toBeInstanceOf(FontResponseError); expect(store.error).toBeInstanceOf(FontResponseError);
}); });
@@ -67,7 +67,7 @@ describe('BatchFontStore', () => {
it('should have null error in success state', async () => { it('should have null error in success state', async () => {
const fonts = [{ id: 'a' }] as any[]; const fonts = [{ id: 'a' }] as any[];
vi.spyOn(api, 'fetchFontsByIds').mockResolvedValue(fonts); 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 }); await vi.waitFor(() => expect(store.fonts).toEqual(fonts), { timeout: 1000 });
expect(store.error).toBeNull(); expect(store.error).toBeNull();
}); });
@@ -78,7 +78,7 @@ describe('BatchFontStore', () => {
const fonts1 = [{ id: 'a' }] as any[]; const fonts1 = [{ id: 'a' }] as any[];
const spy = vi.spyOn(api, 'fetchFontsByIds').mockResolvedValueOnce(fonts1); 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 }); await vi.waitFor(() => expect(store.fonts).toEqual(fonts1), { timeout: 1000 });
spy.mockClear(); spy.mockClear();
@@ -97,7 +97,7 @@ describe('BatchFontStore', () => {
.mockResolvedValueOnce(fonts1) .mockResolvedValueOnce(fonts1)
.mockResolvedValueOnce(fonts2); .mockResolvedValueOnce(fonts2);
const store = new BatchFontStore(['a']); const store = new FontsByIdsStore(['a']);
await vi.waitFor(() => expect(store.fonts).toEqual(fonts1), { timeout: 1000 }); await vi.waitFor(() => expect(store.fonts).toEqual(fonts1), { timeout: 1000 });
store.setIds(['b']); store.setIds(['b']);
@@ -7,14 +7,13 @@
* *
* Features: * Features:
* - Persistent font selection (survives page refresh) * - 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 * - Sample text management
* - Typography controls (size, weight, line height, spacing) * - Typography controls (size, weight, line height, spacing)
* - Slider position for character-by-character morphing * - Slider position for character-by-character morphing
*/ */
import { import {
BatchFontStore,
type FontLoadRequestConfig, type FontLoadRequestConfig,
type UnifiedFont, type UnifiedFont,
appliedFontsManager, appliedFontsManager,
@@ -22,6 +21,7 @@ import {
getFontUrl, getFontUrl,
} from '$entities/Font'; } from '$entities/Font';
import { typographySettingsStore } from '$features/AdjustTypography/model'; import { typographySettingsStore } from '$features/AdjustTypography/model';
import { FontsByIdsStore } from '$features/FetchFontsByIds';
import { createPersistentStore } from '$shared/lib'; import { createPersistentStore } from '$shared/lib';
import { untrack } from 'svelte'; import { untrack } from 'svelte';
import { getPretextFontString } from '../../lib'; import { getPretextFontString } from '../../lib';
@@ -51,7 +51,7 @@ const storage = createPersistentStore<ComparisonState>('glyphdiff:comparison', {
/** /**
* Store for managing font comparison state. * 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 * the previous hand-rolled async fetch approach. Three reactive effects
* handle: (1) syncing batch results into fontA/fontB, (2) triggering the * handle: (1) syncing batch results into fontA/fontB, (2) triggering the
* CSS Font Loading API, and (3) falling back to default fonts when * 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 * TanStack Query-backed store for efficient batch font retrieval
*/ */
#batchStore: BatchFontStore; #fontsByIdsStore: FontsByIdsStore;
constructor() { constructor() {
// Synchronously seed the batch store with any IDs already in storage // Synchronously seed the batch store with any IDs already in storage
const { fontAId, fontBId } = storage.value; const { fontAId, fontBId } = storage.value;
this.#batchStore = new BatchFontStore(fontAId && fontBId ? [fontAId, fontBId] : []); this.#fontsByIdsStore = new FontsByIdsStore(fontAId && fontBId ? [fontAId, fontBId] : []);
$effect.root(() => { $effect.root(() => {
// Effect 1: Sync batch results → fontA / fontB // Effect 1: Sync batch results → fontA / fontB
$effect(() => { $effect(() => {
const fonts = this.#batchStore.fonts; const fonts = this.#fontsByIdsStore.fonts;
if (fonts.length === 0) { if (fonts.length === 0) {
return; return;
} }
@@ -157,7 +157,7 @@ export class ComparisonStore {
const id1 = fonts[0].id; const id1 = fonts[0].id;
const id2 = fonts[fonts.length - 1].id; const id2 = fonts[fonts.length - 1].id;
storage.value = { fontAId: id1, fontBId: id2 }; 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) * True if any font is currently being fetched or loaded (reactive)
*/ */
get isLoading() { get isLoading() {
return this.#batchStore.isLoading || !this.#fontsReady; return this.#fontsByIdsStore.isLoading || !this.#fontsReady;
} }
/** /**
@@ -325,7 +325,7 @@ export class ComparisonStore {
resetAll() { resetAll() {
this.#fontA = undefined; this.#fontA = undefined;
this.#fontB = undefined; this.#fontB = undefined;
this.#batchStore.setIds([]); this.#fontsByIdsStore.setIds([]);
storage.clear(); storage.clear();
typographySettingsStore.reset(); typographySettingsStore.reset();
} }
@@ -1,7 +1,7 @@
/** /**
* Unit tests for ComparisonStore (TanStack Query refactor) * 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. * 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 => { vi.mock('$entities/Font', async importOriginal => {
const actual = await importOriginal<typeof import('$entities/Font')>(); const actual = await importOriginal<typeof import('$entities/Font')>();
const { BatchFontStore } = await import(
'$entities/Font/model/store/batchFontStore/batchFontStore.svelte'
);
return { return {
...actual, ...actual,
BatchFontStore,
fontStore: { fonts: [] }, fontStore: { fonts: [] },
appliedFontsManager: { appliedFontsManager: {
touch: vi.fn(), 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 () => { it('should restore fontA and fontB from stored IDs', async () => {
mockStorage._value.fontAId = mockFontA.id; mockStorage._value.fontAId = mockFontA.id;
mockStorage._value.fontBId = mockFontB.id; mockStorage._value.fontBId = mockFontB.id;