Chore/architecture refactoring #42
@@ -9,6 +9,7 @@ export {
|
||||
fetchFontsByIds,
|
||||
fetchProxyFontById,
|
||||
fetchProxyFonts,
|
||||
seedFontCache,
|
||||
} from './proxy/proxyFonts';
|
||||
export type {
|
||||
ProxyFontsParams,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { FontsByIdsStore } from './model';
|
||||
@@ -0,0 +1 @@
|
||||
export { FontsByIdsStore } from './store/fontsByIdsStore/fontsByIdsStore.svelte';
|
||||
+8
-9
@@ -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),
|
||||
+15
-15
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user