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