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.
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import { createFontLoadRequestContfig } from '../../lib/createFontLoadRequestCon
|
|||||||
import {
|
import {
|
||||||
type FontLoadRequestConfig,
|
type FontLoadRequestConfig,
|
||||||
type UnifiedFont,
|
type UnifiedFont,
|
||||||
fontLifecycleManager,
|
|
||||||
getFontCatalog,
|
getFontCatalog,
|
||||||
|
getFontLifecycleManager,
|
||||||
} from '../../model';
|
} from '../../model';
|
||||||
|
|
||||||
interface Props extends
|
interface Props extends
|
||||||
@@ -53,6 +53,7 @@ let {
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const fontCatalog = getFontCatalog();
|
const fontCatalog = getFontCatalog();
|
||||||
|
const fontLifecycleManager = getFontLifecycleManager();
|
||||||
|
|
||||||
const isLoading = $derived<boolean>(fontCatalog?.isLoading);
|
const isLoading = $derived<boolean>(fontCatalog?.isLoading);
|
||||||
const isFetching = $derived<boolean>(fontCatalog.isFetching);
|
const isFetching = $derived<boolean>(fontCatalog.isFetching);
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ import {
|
|||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import {
|
import {
|
||||||
type FontCatalogStore,
|
type FontCatalogStore,
|
||||||
|
type FontLifecycleManager,
|
||||||
FontsByIdsStore,
|
FontsByIdsStore,
|
||||||
fontLifecycleManager,
|
|
||||||
getFontCatalog,
|
getFontCatalog,
|
||||||
|
getFontLifecycleManager,
|
||||||
} from '$entities/Font/model';
|
} from '$entities/Font/model';
|
||||||
import {
|
import {
|
||||||
type TypographySettingsStore,
|
type TypographySettingsStore,
|
||||||
@@ -106,12 +107,15 @@ export class ComparisonStore {
|
|||||||
|
|
||||||
#typography: TypographySettingsStore;
|
#typography: TypographySettingsStore;
|
||||||
|
|
||||||
|
#lifecycle: FontLifecycleManager;
|
||||||
|
|
||||||
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.#fontsByIdsStore = new FontsByIdsStore(fontAId && fontBId ? [fontAId, fontBId] : []);
|
this.#fontsByIdsStore = new FontsByIdsStore(fontAId && fontBId ? [fontAId, fontBId] : []);
|
||||||
this.#fontCatalog = getFontCatalog();
|
this.#fontCatalog = getFontCatalog();
|
||||||
this.#typography = getTypographySettingsStore();
|
this.#typography = getTypographySettingsStore();
|
||||||
|
this.#lifecycle = getFontLifecycleManager();
|
||||||
|
|
||||||
$effect.root(() => {
|
$effect.root(() => {
|
||||||
// Sync batch results → fontA / fontB
|
// Sync batch results → fontA / fontB
|
||||||
@@ -161,7 +165,7 @@ export class ComparisonStore {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (configs.length > 0) {
|
if (configs.length > 0) {
|
||||||
fontLifecycleManager.touch(configs);
|
this.#lifecycle.touch(configs);
|
||||||
this.#checkFontsLoaded();
|
this.#checkFontsLoaded();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -203,17 +207,17 @@ export class ComparisonStore {
|
|||||||
const fb = this.#fontB;
|
const fb = this.#fontB;
|
||||||
const w = this.#typography.weight;
|
const w = this.#typography.weight;
|
||||||
if (fa) {
|
if (fa) {
|
||||||
fontLifecycleManager.pin(fa.id, w, fa.features?.isVariable);
|
this.#lifecycle.pin(fa.id, w, fa.features?.isVariable);
|
||||||
}
|
}
|
||||||
if (fb) {
|
if (fb) {
|
||||||
fontLifecycleManager.pin(fb.id, w, fb.features?.isVariable);
|
this.#lifecycle.pin(fb.id, w, fb.features?.isVariable);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (fa) {
|
if (fa) {
|
||||||
fontLifecycleManager.unpin(fa.id, w, fa.features?.isVariable);
|
this.#lifecycle.unpin(fa.id, w, fa.features?.isVariable);
|
||||||
}
|
}
|
||||||
if (fb) {
|
if (fb) {
|
||||||
fontLifecycleManager.unpin(fb.id, w, fb.features?.isVariable);
|
this.#lifecycle.unpin(fb.id, w, fb.features?.isVariable);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
// Stores moved behind the model segment; mock them there. FontsByIdsStore is
|
||||||
// intentionally left real (spread from actual) so $state reactivity works.
|
// intentionally left real (spread from actual) so $state reactivity works.
|
||||||
vi.mock('$entities/Font/model', async importOriginal => {
|
vi.mock('$entities/Font/model', async importOriginal => {
|
||||||
@@ -70,13 +78,7 @@ vi.mock('$entities/Font/model', async importOriginal => {
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
getFontCatalog: () => mockFontCatalog,
|
getFontCatalog: () => mockFontCatalog,
|
||||||
fontLifecycleManager: {
|
getFontLifecycleManager: () => mockLifecycle,
|
||||||
touch: vi.fn(),
|
|
||||||
pin: vi.fn(),
|
|
||||||
unpin: vi.fn(),
|
|
||||||
getFontStatus: vi.fn(),
|
|
||||||
ready: vi.fn(() => Promise.resolve()),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,10 +102,7 @@ vi.mock('$features/AdjustTypography/model', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts';
|
import * as proxyFonts from '$entities/Font/api/proxy/proxyFonts';
|
||||||
import {
|
import { getFontCatalog } from '$entities/Font/model';
|
||||||
fontLifecycleManager,
|
|
||||||
getFontCatalog,
|
|
||||||
} from '$entities/Font/model';
|
|
||||||
import { __resetFontCatalog } from '$entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte';
|
import { __resetFontCatalog } from '$entities/Font/model/store/fontCatalogStore/fontCatalogStore.svelte';
|
||||||
import { ComparisonStore } from './comparisonStore.svelte';
|
import { ComparisonStore } from './comparisonStore.svelte';
|
||||||
|
|
||||||
@@ -273,12 +272,12 @@ describe('ComparisonStore', () => {
|
|||||||
new ComparisonStore();
|
new ComparisonStore();
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(fontLifecycleManager.pin).toHaveBeenCalledWith(
|
expect(mockLifecycle.pin).toHaveBeenCalledWith(
|
||||||
mockFontA.id,
|
mockFontA.id,
|
||||||
400,
|
400,
|
||||||
mockFontA.features?.isVariable,
|
mockFontA.features?.isVariable,
|
||||||
);
|
);
|
||||||
expect(fontLifecycleManager.pin).toHaveBeenCalledWith(
|
expect(mockLifecycle.pin).toHaveBeenCalledWith(
|
||||||
mockFontB.id,
|
mockFontB.id,
|
||||||
400,
|
400,
|
||||||
mockFontB.features?.isVariable,
|
mockFontB.features?.isVariable,
|
||||||
@@ -299,12 +298,12 @@ describe('ComparisonStore', () => {
|
|||||||
store.fontA = mockFontC;
|
store.fontA = mockFontC;
|
||||||
|
|
||||||
await vi.waitFor(() => {
|
await vi.waitFor(() => {
|
||||||
expect(fontLifecycleManager.unpin).toHaveBeenCalledWith(
|
expect(mockLifecycle.unpin).toHaveBeenCalledWith(
|
||||||
mockFontA.id,
|
mockFontA.id,
|
||||||
400,
|
400,
|
||||||
mockFontA.features?.isVariable,
|
mockFontA.features?.isVariable,
|
||||||
);
|
);
|
||||||
expect(fontLifecycleManager.pin).toHaveBeenCalledWith(
|
expect(mockLifecycle.pin).toHaveBeenCalledWith(
|
||||||
mockFontC.id,
|
mockFontC.id,
|
||||||
400,
|
400,
|
||||||
mockFontC.features?.isVariable,
|
mockFontC.features?.isVariable,
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
VIRTUAL_INDEX_NOT_LOADED,
|
VIRTUAL_INDEX_NOT_LOADED,
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import {
|
import {
|
||||||
fontLifecycleManager,
|
|
||||||
getFontCatalog,
|
getFontCatalog,
|
||||||
|
getFontLifecycleManager,
|
||||||
} from '$entities/Font/model';
|
} from '$entities/Font/model';
|
||||||
import { getSkeletonWidth } from '$shared/lib/utils';
|
import { getSkeletonWidth } from '$shared/lib/utils';
|
||||||
import {
|
import {
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
|
|
||||||
const fontCatalog = getFontCatalog();
|
const fontCatalog = getFontCatalog();
|
||||||
const comparisonStore = getComparisonStore();
|
const comparisonStore = getComparisonStore();
|
||||||
|
const fontLifecycleManager = getFontLifecycleManager();
|
||||||
|
|
||||||
const fonts = $derived<UnifiedFont[]>(fontCatalog.fonts);
|
const fonts = $derived<UnifiedFont[]>(fontCatalog.fonts);
|
||||||
const side = $derived<Side>(comparisonStore.side);
|
const side = $derived<Side>(comparisonStore.side);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
createFontRowSizeResolver,
|
createFontRowSizeResolver,
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
import {
|
import {
|
||||||
fontLifecycleManager,
|
|
||||||
getFontCatalog,
|
getFontCatalog,
|
||||||
|
getFontLifecycleManager,
|
||||||
} from '$entities/Font/model';
|
} from '$entities/Font/model';
|
||||||
import {
|
import {
|
||||||
TypographyMenu,
|
TypographyMenu,
|
||||||
@@ -24,6 +24,7 @@ import { layoutManager } from '../../model';
|
|||||||
|
|
||||||
const fontCatalog = getFontCatalog();
|
const fontCatalog = getFontCatalog();
|
||||||
const typographySettingsStore = getTypographySettingsStore();
|
const typographySettingsStore = getTypographySettingsStore();
|
||||||
|
const fontLifecycleManager = getFontLifecycleManager();
|
||||||
|
|
||||||
// FontSampler chrome heights — derived from Tailwind classes in FontSampler.svelte.
|
// FontSampler chrome heights — derived from Tailwind classes in FontSampler.svelte.
|
||||||
// Header: py-3 (12+12px padding) + ~32px content row ≈ 56px.
|
// Header: py-3 (12+12px padding) + ~32px content row ≈ 56px.
|
||||||
|
|||||||
Reference in New Issue
Block a user