From b3bc40b76c4029157cf194390767c7e73bdfafab Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Mon, 1 Jun 2026 18:49:47 +0300 Subject: [PATCH] refactor(font): replace fontLifecycleManager singleton with lazy accessor Convert the eager fontLifecycleManager singleton to getFontLifecycleManager() (+ __resetFontLifecycleManager for tests), so its AbortController/FontFace bookkeeping is set up on first use rather than at module load. Update consumers (FontVirtualList, FontList, SampleList) and resolve it once as a field in comparisonStore; the comparisonStore mock now exposes getFontLifecycleManager. --- .../fontLifecycleManager.svelte.ts | 15 ++++++++-- .../ui/FontVirtualList/FontVirtualList.svelte | 3 +- .../model/stores/comparisonStore.svelte.ts | 16 ++++++---- .../model/stores/comparisonStore.test.ts | 29 +++++++++---------- .../ui/FontList/FontList.svelte | 3 +- .../ui/SampleList/SampleList.svelte | 3 +- 6 files changed, 43 insertions(+), 26 deletions(-) diff --git a/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts b/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts index 9439a87..f159a82 100644 --- a/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts +++ b/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts @@ -419,7 +419,18 @@ export class FontLifecycleManager { } } +let _fontLifecycleManager: FontLifecycleManager | undefined; + /** - * Singleton instance — use throughout the application for unified font loading state. + * App-wide font lifecycle manager, created on first access. Lazy so its + * AbortController / FontFace bookkeeping isn't set up at module load. */ -export const fontLifecycleManager = new FontLifecycleManager(); +export function getFontLifecycleManager(): FontLifecycleManager { + return (_fontLifecycleManager ??= new FontLifecycleManager()); +} + +// test-only reset, so specs don't share loaded-font/eviction state +export function __resetFontLifecycleManager() { + _fontLifecycleManager?.destroy(); + _fontLifecycleManager = undefined; +} diff --git a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte index bb71dd8..4fdf6a5 100644 --- a/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte +++ b/src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte @@ -15,8 +15,8 @@ import { createFontLoadRequestContfig } from '../../lib/createFontLoadRequestCon import { type FontLoadRequestConfig, type UnifiedFont, - fontLifecycleManager, getFontCatalog, + getFontLifecycleManager, } from '../../model'; interface Props extends @@ -53,6 +53,7 @@ let { }: Props = $props(); const fontCatalog = getFontCatalog(); +const fontLifecycleManager = getFontLifecycleManager(); const isLoading = $derived(fontCatalog?.isLoading); const isFetching = $derived(fontCatalog.isFetching); diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts index 7abfa6c..4b84354 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts @@ -20,9 +20,10 @@ import { } from '$entities/Font'; import { type FontCatalogStore, + type FontLifecycleManager, FontsByIdsStore, - fontLifecycleManager, getFontCatalog, + getFontLifecycleManager, } from '$entities/Font/model'; import { type TypographySettingsStore, @@ -106,12 +107,15 @@ export class ComparisonStore { #typography: TypographySettingsStore; + #lifecycle: FontLifecycleManager; + constructor() { // Synchronously seed the batch store with any IDs already in storage const { fontAId, fontBId } = storage.value; this.#fontsByIdsStore = new FontsByIdsStore(fontAId && fontBId ? [fontAId, fontBId] : []); this.#fontCatalog = getFontCatalog(); this.#typography = getTypographySettingsStore(); + this.#lifecycle = getFontLifecycleManager(); $effect.root(() => { // Sync batch results → fontA / fontB @@ -161,7 +165,7 @@ export class ComparisonStore { }); if (configs.length > 0) { - fontLifecycleManager.touch(configs); + this.#lifecycle.touch(configs); this.#checkFontsLoaded(); } }); @@ -203,17 +207,17 @@ export class ComparisonStore { const fb = this.#fontB; const w = this.#typography.weight; if (fa) { - fontLifecycleManager.pin(fa.id, w, fa.features?.isVariable); + this.#lifecycle.pin(fa.id, w, fa.features?.isVariable); } if (fb) { - fontLifecycleManager.pin(fb.id, w, fb.features?.isVariable); + this.#lifecycle.pin(fb.id, w, fb.features?.isVariable); } return () => { if (fa) { - fontLifecycleManager.unpin(fa.id, w, fa.features?.isVariable); + this.#lifecycle.unpin(fa.id, w, fa.features?.isVariable); } if (fb) { - fontLifecycleManager.unpin(fb.id, w, fb.features?.isVariable); + this.#lifecycle.unpin(fb.id, w, fb.features?.isVariable); } }; }); diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts index 495aa70..323e0b3 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.test.ts @@ -63,6 +63,14 @@ vi.mock('$entities/Font', async importOriginal => { }; }); +const mockLifecycle = vi.hoisted(() => ({ + touch: vi.fn(), + pin: vi.fn(), + unpin: vi.fn(), + getFontStatus: vi.fn(), + ready: vi.fn(() => Promise.resolve()), +})); + // Stores moved behind the model segment; mock them there. FontsByIdsStore is // intentionally left real (spread from actual) so $state reactivity works. vi.mock('$entities/Font/model', async importOriginal => { @@ -70,13 +78,7 @@ vi.mock('$entities/Font/model', async importOriginal => { return { ...actual, getFontCatalog: () => mockFontCatalog, - fontLifecycleManager: { - touch: vi.fn(), - pin: vi.fn(), - unpin: vi.fn(), - getFontStatus: vi.fn(), - ready: vi.fn(() => Promise.resolve()), - }, + getFontLifecycleManager: () => mockLifecycle, }; }); @@ -100,10 +102,7 @@ vi.mock('$features/AdjustTypography/model', () => ({ })); import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts'; -import { - fontLifecycleManager, - getFontCatalog, -} from '$entities/Font/model'; +import { getFontCatalog } from '$entities/Font/model'; import { __resetFontCatalog } from '$entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte'; import { ComparisonStore } from './comparisonStore.svelte'; @@ -273,12 +272,12 @@ describe('ComparisonStore', () => { new ComparisonStore(); await vi.waitFor(() => { - expect(fontLifecycleManager.pin).toHaveBeenCalledWith( + expect(mockLifecycle.pin).toHaveBeenCalledWith( mockFontA.id, 400, mockFontA.features?.isVariable, ); - expect(fontLifecycleManager.pin).toHaveBeenCalledWith( + expect(mockLifecycle.pin).toHaveBeenCalledWith( mockFontB.id, 400, mockFontB.features?.isVariable, @@ -299,12 +298,12 @@ describe('ComparisonStore', () => { store.fontA = mockFontC; await vi.waitFor(() => { - expect(fontLifecycleManager.unpin).toHaveBeenCalledWith( + expect(mockLifecycle.unpin).toHaveBeenCalledWith( mockFontA.id, 400, mockFontA.features?.isVariable, ); - expect(fontLifecycleManager.pin).toHaveBeenCalledWith( + expect(mockLifecycle.pin).toHaveBeenCalledWith( mockFontC.id, 400, mockFontC.features?.isVariable, diff --git a/src/widgets/ComparisonView/ui/FontList/FontList.svelte b/src/widgets/ComparisonView/ui/FontList/FontList.svelte index cc8de36..817b863 100644 --- a/src/widgets/ComparisonView/ui/FontList/FontList.svelte +++ b/src/widgets/ComparisonView/ui/FontList/FontList.svelte @@ -11,8 +11,8 @@ import { VIRTUAL_INDEX_NOT_LOADED, } from '$entities/Font'; import { - fontLifecycleManager, getFontCatalog, + getFontLifecycleManager, } from '$entities/Font/model'; import { getSkeletonWidth } from '$shared/lib/utils'; import { @@ -33,6 +33,7 @@ import { const fontCatalog = getFontCatalog(); const comparisonStore = getComparisonStore(); +const fontLifecycleManager = getFontLifecycleManager(); const fonts = $derived(fontCatalog.fonts); const side = $derived(comparisonStore.side); diff --git a/src/widgets/SampleList/ui/SampleList/SampleList.svelte b/src/widgets/SampleList/ui/SampleList/SampleList.svelte index 20f7a21..b7b0162 100644 --- a/src/widgets/SampleList/ui/SampleList/SampleList.svelte +++ b/src/widgets/SampleList/ui/SampleList/SampleList.svelte @@ -10,8 +10,8 @@ import { createFontRowSizeResolver, } from '$entities/Font'; import { - fontLifecycleManager, getFontCatalog, + getFontLifecycleManager, } from '$entities/Font/model'; import { TypographyMenu, @@ -24,6 +24,7 @@ import { layoutManager } from '../../model'; const fontCatalog = getFontCatalog(); const typographySettingsStore = getTypographySettingsStore(); +const fontLifecycleManager = getFontLifecycleManager(); // FontSampler chrome heights — derived from Tailwind classes in FontSampler.svelte. // Header: py-3 (12+12px padding) + ~32px content row ≈ 56px.