From 10603d18bf87dd68937d7e00d4c5dd9ae05025ba Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 1 Jun 2026 17:24:55 +0300 Subject: [PATCH] refactor(font): replace fontCatalogStore singleton with lazy getFontCatalog Swap the eagerly-constructed fontCatalogStore singleton for a lazy getFontCatalog() accessor (plus __resetFontCatalog for tests), so the InfiniteQueryObserver is created on first use rather than at module load. Update the model barrels and all consumers (FontVirtualList, SampleList, SampleListSection) to the accessor. Also extract createFontLoadRequestConfig from FontVirtualList: it resolves a font's URL for a weight and returns a 0-or-1 element array, letting callers flatMap over a list to build load requests and drop unresolvable fonts in one pass. --- .../createFontLoadRequestContfig.test.ts | 111 ++++++++++++++++++ .../createFontLoadRequestContfig.ts | 33 ++++++ src/entities/Font/model/index.ts | 3 + .../fontCatalogStore.svelte.ts | 12 +- src/entities/Font/model/store/index.ts | 8 +- .../ui/FontVirtualList/FontVirtualList.svelte | 63 +++++----- .../ui/SampleList/SampleList.svelte | 16 ++- .../SampleListSection.svelte | 7 +- 8 files changed, 204 insertions(+), 49 deletions(-) create mode 100644 src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.test.ts create mode 100644 src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.ts diff --git a/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.test.ts b/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.test.ts new file mode 100644 index 0000000..ef196d4 --- /dev/null +++ b/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.test.ts @@ -0,0 +1,111 @@ +import { + describe, + expect, + it, +} from 'vitest'; +import type { UnifiedFont } from '../../model/types'; +import { createFontLoadRequestContfig } from './createFontLoadRequestContfig'; + +/** + * Minimal UnifiedFont mock — override only the fields a case exercises. + */ +function createMockFont(overrides: Partial = {}): UnifiedFont { + const baseFont: UnifiedFont = { + id: 'test-font', + name: 'Test Font', + provider: 'google', + category: 'sans-serif', + subsets: ['latin'], + variants: [], + styles: {}, + metadata: { + cachedAt: Date.now(), + }, + features: { + isVariable: false, + tags: [], + }, + }; + + return { ...baseFont, ...overrides }; +} + +describe('createFontLoadRequestContfig', () => { + it('builds a single-element config when a URL resolves', () => { + const font = createMockFont({ + id: 'roboto', + name: 'Roboto', + styles: { variants: { '400': 'https://example.com/roboto-400.woff2' } }, + }); + + const result = createFontLoadRequestContfig(font, 400); + + expect(result).toEqual([ + { + id: 'roboto', + name: 'Roboto', + weight: 400, + url: 'https://example.com/roboto-400.woff2', + isVariable: false, + }, + ]); + }); + + it('returns an empty array when no URL resolves (flatMap drops the font)', () => { + const font = createMockFont({ styles: {} }); + + expect(createFontLoadRequestContfig(font, 400)).toEqual([]); + }); + + it('forwards isVariable from font features', () => { + const font = createMockFont({ + features: { isVariable: true, tags: [] }, + styles: { variants: { '700': 'https://example.com/inter-vf.woff2' } }, + }); + + const [config] = createFontLoadRequestContfig(font, 700); + + expect(config.isVariable).toBe(true); + }); + + it('sets isVariable to undefined when features is absent', () => { + // features is non-optional on UnifiedFont, but upstream data can be partial — + // the optional chain must not throw, and isVariable stays undefined. + const font = createMockFont({ + styles: { variants: { '400': 'https://example.com/font.woff2' } }, + }); + // @ts-expect-error — deliberately drop the guaranteed field to exercise the optional chain + font.features = undefined; + + const [config] = createFontLoadRequestContfig(font, 400); + + expect(config.isVariable).toBeUndefined(); + }); + + it('uses the resolved fallback URL, not just exact matches', () => { + // getFontUrl falls back to styles.regular when the exact weight is missing; + // the config must carry whatever URL actually resolved. + const font = createMockFont({ + styles: { regular: 'https://example.com/font-regular.woff2' }, + }); + + const [config] = createFontLoadRequestContfig(font, 900); + + expect(config.url).toBe('https://example.com/font-regular.woff2'); + expect(config.weight).toBe(900); + }); + + it('carries the requested weight even when the URL is a shared fallback', () => { + const font = createMockFont({ + styles: { variants: { '400': 'https://example.com/shared.woff2' } }, + }); + + expect(createFontLoadRequestContfig(font, 700)[0].weight).toBe(700); + }); + + it('propagates the invalid-weight error from getFontUrl', () => { + const font = createMockFont(); + + expect(() => createFontLoadRequestContfig(font, 450)).toThrow('Invalid weight: 450'); + }); +}); diff --git a/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.ts b/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.ts new file mode 100644 index 0000000..b0aad4c --- /dev/null +++ b/src/entities/Font/lib/createFontLoadRequestContfig/createFontLoadRequestContfig.ts @@ -0,0 +1,33 @@ +import type { + FontLoadRequestConfig, + UnifiedFont, +} from '../../model'; +import { getFontUrl } from '../getFontUrl/getFontUrl'; + +/** + * Build the font-lifecycle load request for a single font at a given weight. + * + * Returns a 0-or-1 element array rather than `FontLoadRequestConfig | undefined` + * so call sites can `flatMap` over a font list — resolve the URL and drop fonts + * that have none in a single pass, with no separate filter step. An empty array + * means the font has no loadable asset for this weight (or its fallbacks) and is + * silently skipped. + * + * `isVariable` is forwarded from the font's features so the lifecycle manager can + * dedupe variable fonts per ID (they load once regardless of weight) while still + * loading static fonts per weight. + * + * @param font - Unified font to load + * @param weight - Numeric weight (100-900) + * @returns Single-element config array, or `[]` when no URL resolves + * @throws Error when weight is outside the valid 100-900 range (propagated from `getFontUrl`) + */ +export function createFontLoadRequestContfig(font: UnifiedFont, weight: number): FontLoadRequestConfig[] { + const url = getFontUrl(font, weight); + + if (!url) { + return []; + } + + return [{ id: font.id, name: font.name, weight, url, isVariable: font.features?.isVariable }]; +} diff --git a/src/entities/Font/model/index.ts b/src/entities/Font/model/index.ts index 8749b29..36cbda2 100644 --- a/src/entities/Font/model/index.ts +++ b/src/entities/Font/model/index.ts @@ -1,3 +1,6 @@ export * from './const/const'; + +export { getFontCatalog } from './store'; + export * from './store'; export * from './types'; diff --git a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts index c6d7510..72b3d56 100644 --- a/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts +++ b/src/entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte.ts @@ -483,8 +483,14 @@ export class FontCatalogStore { } } -export function createFontCatalogStore(params: FontStoreParams = {}): FontCatalogStore { - return new FontCatalogStore(params); +let _catalog: FontCatalogStore | undefined; + +export function getFontCatalog(): FontCatalogStore { + return (_catalog ??= new FontCatalogStore({ limit: 50 })); } -export const fontCatalogStore = new FontCatalogStore({ limit: 50 }); +// test-only reset, so specs don't share a live observer +export function __resetFontCatalog() { + _catalog?.destroy(); + _catalog = undefined; +} diff --git a/src/entities/Font/model/store/index.ts b/src/entities/Font/model/store/index.ts index 52dda36..d0d0024 100644 --- a/src/entities/Font/model/store/index.ts +++ b/src/entities/Font/model/store/index.ts @@ -2,11 +2,9 @@ export * from './fontLifecycleManager/fontLifecycleManager.svelte'; // Paginated catalog -export { - createFontCatalogStore, - FontCatalogStore, - fontCatalogStore, -} from './fontCatalogStore/fontCatalogStore.svelte'; +export { getFontCatalog } from './fontCatalogStore/fontCatalogStore.svelte'; + +export type { FontCatalogStore } from './fontCatalogStore/fontCatalogStore.svelte'; // Batch fetch by IDs (detail-cache seeding) export { FontsByIdsStore } from './fontsByIdsStore/fontsByIdsStore.svelte'; diff --git a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte index 0d21abe..bb71dd8 100644 --- a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte +++ b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte @@ -5,21 +5,18 @@ --> @@ -36,7 +39,7 @@ const responsive = getContext('responsive'); id="sample_set" title="Sample Set" headerTitle="visual_output" - headerSubtitle="items_total: {fontCatalogStore.pagination.total ?? 0}" + headerSubtitle="items_total: {total ?? 0}" headerAction={registerAction} > {#snippet headerContent()}