/** * ============================================================================ * UNIFIED FONT STORE * ============================================================================ * * Single source of truth for font operations across all providers. * Combines fetching, filtering, caching, and managing fonts from Google Fonts and Fontshare. * * NOW INTEGRATED WITH TANSTACK QUERY via specific stores. * * OPTIMIZATIONS (P0/P1): * - Debounced search (300ms) to reduce re-renders * - Single-pass filter function for O(n) complexity * - Two derived values: filteredFonts (filtered) + sortedFilteredFonts (final) * - TanStack Query cancellation for stale requests */ import { debounce } from '$shared/lib/utils'; import { filterFonts, sortFonts, } from '../../lib/filterUtils'; import { createFontshareStore } from '../services/fetchFontshareFonts.svelte'; import { createGoogleFontsStore } from '../services/fetchGoogleFonts.svelte'; import type { UnifiedFont } from '../types/normalize'; import type { FetchFontsParams, FilterType, FontFilters, FontSort, } from './types'; /** * Creates a unified font store instance. */ export function createUnifiedFontStore() { // Instantiate specific stores const googleStore = createGoogleFontsStore(); const fontshareStore = createFontshareStore(); // Internal state for local filters (that apply to the combined list) let _filters = $state({ providers: [], categories: [], subsets: [], searchQuery: '', }); let _sort = $state({ field: 'name', direction: 'asc' }); // === Debounced Search === // Debounce search query to avoid excessive filtering during typing let _debouncedSearchQuery = $state(''); const setSearchQuery = debounce((query: string) => { _debouncedSearchQuery = query; }, 300); $effect(() => { setSearchQuery(_filters.searchQuery); }); // === Computed Values === /** * Combined fonts from all active stores */ const allFonts = $derived.by(() => { let fonts: UnifiedFont[] = []; // Google Fonts if (_filters.providers.length === 0 || _filters.providers.includes('google')) { fonts = [...fonts, ...googleStore.fonts]; } // Fontshare Fonts if (_filters.providers.length === 0 || _filters.providers.includes('fontshare')) { fonts = [...fonts, ...fontshareStore.fonts]; } return fonts; }); /** * Filtered fonts (before sort) - Derived Value #1 * Uses optimized single-pass filter function */ const filteredFonts = $derived.by(() => { const filtersForFiltering: FontFilters = { providers: _filters.providers, categories: _filters.categories, subsets: _filters.subsets, searchQuery: _debouncedSearchQuery, }; return filterFonts(allFonts, filtersForFiltering); }); /** * Sorted filtered fonts (final result) - Derived Value #2 * Fast path: skip sorting for default name ascending order */ const sortedFilteredFonts = $derived.by(() => { const filtered = filteredFonts; // Fast path: default sort (name ascending) - already sorted by filterFonts if (_sort.field === 'name' && _sort.direction === 'asc') { return filtered; } return sortFonts(filtered, _sort); }); // === Status Derivation === const isLoading = $derived(googleStore.isLoading || fontshareStore.isLoading); const isFetching = $derived(googleStore.isFetching || fontshareStore.isFetching); const error = $derived(googleStore.error?.message || fontshareStore.error?.message || null); // === Methods === /** * Fetch fonts from all active providers using TanStack Query * This now mostly just updates params or triggers refetch if needed. * * Includes cancellation of stale requests to improve performance. */ async function fetchFonts(params?: FetchFontsParams): Promise { // Update local filters if (params) { if (params.providers) _filters.providers = params.providers; if (params.categories) _filters.categories = params.categories; if (params.subsets) _filters.subsets = params.subsets; if (params.search !== undefined) _filters.searchQuery = params.search; if (params.sort) _sort = params.sort; } // Cancel existing queries before starting new ones (optimization) googleStore.cancel(); fontshareStore.cancel(); // Trigger fetches in underlying stores // We pass the filter params down to the stores so they can optimize server-side fetching if supported // For Google Fonts: googleStore.setParams({ category: _filters.categories.length === 1 ? _filters.categories[0] : undefined, subset: _filters.subsets.length === 1 ? _filters.subsets[0] : undefined, sort: (_sort.field === 'popularity' ? 'popularity' : _sort.field === 'date' ? 'date' : 'alpha') as any, }); // For Fontshare: // fontshareStore.setCategories(_filters.categories); // If supported } /** Update specific filter */ function setFilter(type: FilterType, values: string[]): void { if (type === 'searchQuery') { _filters.searchQuery = values[0] ?? ''; } else { _filters = { ..._filters, [type]: values as any, // Type cast for loose array matching }; } } /** Clear specific filter */ function clearFilter(type: FilterType): void { if (type === 'searchQuery') { _filters.searchQuery = ''; } else { _filters = { ..._filters, [type]: [], }; } } /** Clear all filters */ function clearFilters(): void { _filters = { providers: [], categories: [], subsets: [], searchQuery: '', }; } return { // Getters get fonts() { return allFonts; }, get filteredFonts() { return sortedFilteredFonts; }, get count() { return allFonts.length; }, get isLoading() { return isLoading; }, get isFetching() { return isFetching; }, get error() { return error; }, get filters() { return _filters; }, get searchQuery() { return _filters.searchQuery; }, get sort() { return _sort; }, get providers() { // Expose underlying stores for direct access if needed return { google: googleStore, fontshare: fontshareStore, }; }, // Setters set filters(value: FontFilters) { _filters = value; }, set searchQuery(value: string) { _filters.searchQuery = value; }, set sort(value: FontSort) { _sort = value; }, // Methods fetchFonts, setFilter, clearFilter, clearFilters, // Legacy support (no-op or adapted) addFont: () => {}, // Not supported in reactive query model addFonts: () => {}, removeFont: () => {}, getFontById: (id: string) => allFonts.find(f => f.id === id), clearCache: () => { googleStore.clearCache(); fontshareStore.clearCache(); }, }; } export type UnifiedFontStore = ReturnType; /** * Context key for dependency injection */ export const UNIFIED_FONT_STORE_KEY = Symbol('UNIFIED_FONT_STORE');