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.
This commit is contained in:
Ilia Mashkov
2026-06-01 17:24:55 +03:00
parent 39d1ce4c37
commit 10603d18bf
8 changed files with 204 additions and 49 deletions
@@ -5,21 +5,18 @@
-->
<script lang="ts">
import { debounce } from '$shared/lib/utils';
import {
Skeleton,
VirtualList,
} from '$shared/ui';
import { VirtualList } from '$shared/ui';
import type {
ComponentProps,
Snippet,
} from 'svelte';
import { fade } from 'svelte/transition';
import { getFontUrl } from '../../lib';
import { createFontLoadRequestContfig } from '../../lib/createFontLoadRequestContfig/createFontLoadRequestContfig';
import {
type FontLoadRequestConfig,
type UnifiedFont,
fontCatalogStore,
fontLifecycleManager,
getFontCatalog,
} from '../../model';
interface Props extends
@@ -55,17 +52,27 @@ let {
...rest
}: Props = $props();
const isLoading = $derived(
fontCatalogStore.isFetching || fontCatalogStore.isLoading,
);
const fontCatalog = getFontCatalog();
const isLoading = $derived<boolean>(fontCatalog?.isLoading);
const isFetching = $derived<boolean>(fontCatalog.isFetching);
const hasMore = $derived<boolean>(fontCatalog?.pagination?.hasMore);
const fonts = $derived<UnifiedFont[]>(fontCatalog.fonts);
const total = $derived<number>(fontCatalog?.pagination.total);
let visibleFonts = $state<UnifiedFont[]>([]);
let isCatchingUp = $state(false);
let isCatchingUp = $state<boolean>(false);
const showInitialSkeleton = $derived(!!skeleton && isLoading && fontCatalogStore.fonts.length === 0);
const showCatchupSkeleton = $derived(!!skeleton && isCatchingUp);
const showInitialSkeleton = $derived.by(() => (
!!skeleton && (isLoading || isFetching) && fontCatalog.fonts.length === 0
));
const showCatchupSkeleton = $derived.by(() => (
!!skeleton && isCatchingUp
));
// Settled query with no matches — empty state replaces the (otherwise blank) list.
const showEmpty = $derived(!!empty && !isLoading && !isCatchingUp && fontCatalogStore.fonts.length === 0);
const showEmpty = $derived.by(() => (
!!empty && !(isLoading || isFetching) && !isCatchingUp && fontCatalog.fonts.length === 0
));
function handleInternalVisibleChange(items: UnifiedFont[]) {
visibleFonts = items;
@@ -79,12 +86,12 @@ function handleInternalVisibleChange(items: UnifiedFont[]) {
* font files for thousands of intermediate fonts.
*/
async function handleJump(targetIndex: number) {
if (isCatchingUp || !fontCatalogStore.pagination.hasMore) {
if (isCatchingUp || !hasMore) {
return;
}
isCatchingUp = true;
try {
await fontCatalogStore.fetchAllPagesTo(targetIndex);
await fontCatalog.fetchAllPagesTo(targetIndex);
} finally {
isCatchingUp = false;
}
@@ -105,13 +112,7 @@ $effect(() => {
if (isCatchingUp) {
return;
}
const configs: FontLoadRequestConfig[] = visibleFonts.flatMap(item => {
const url = getFontUrl(item, weight);
if (!url) {
return [];
}
return [{ id: item.id, name: item.name, weight, url, isVariable: item.features?.isVariable }];
});
const configs = visibleFonts.flatMap(item => createFontLoadRequestContfig(item, weight));
if (configs.length > 0) {
debouncedTouch(configs);
}
@@ -137,13 +138,11 @@ $effect(() => {
* Load more fonts by moving to the next page
*/
function loadMore() {
if (
!fontCatalogStore.pagination.hasMore
|| fontCatalogStore.isFetching
) {
if (!hasMore || isFetching) {
return;
}
fontCatalogStore.nextPage();
fontCatalog.nextPage();
}
/**
@@ -153,12 +152,10 @@ function loadMore() {
* of the loaded items. Only fetches if there are more pages available.
*/
function handleNearBottom(_lastVisibleIndex: number) {
const { hasMore } = fontCatalogStore.pagination;
// VirtualList already checks if we're near the bottom of loaded items.
// Guard isCatchingUp: fetchAllPagesTo bypasses TQ so isFetching stays false
// during batch catch-up, which would otherwise let nextPage() race with it.
if (hasMore && !fontCatalogStore.isFetching && !isCatchingUp) {
if (hasMore && !isFetching && !isCatchingUp) {
loadMore();
}
}
@@ -177,9 +174,9 @@ function handleNearBottom(_lastVisibleIndex: number) {
{:else}
<!-- VirtualList persists during pagination - no destruction/recreation -->
<VirtualList
items={fontCatalogStore.fonts}
total={fontCatalogStore.pagination.total}
isLoading={isLoading || isCatchingUp}
items={fonts}
{total}
isLoading={isLoading || isFetching || isCatchingUp}
onVisibleItemsChange={handleInternalVisibleChange}
onNearBottom={handleNearBottom}
onJump={handleJump}