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,
fetchProxyFontById,
fetchProxyFonts,
seedFontCache,
} from './proxy/proxyFonts';
export type {
ProxyFontsParams,
-3
View File
@@ -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,
+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 {
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<UnifiedFont[]> {
}
/**
* 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<UnifiedFont[]> {
export class FontsByIdsStore extends BaseQueryStore<UnifiedFont[]> {
constructor(initialIds: string[] = []) {
super({
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 { 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']);
@@ -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<ComparisonState>('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();
}
@@ -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<typeof import('$entities/Font')>();
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;