feature/fetch-fonts #14

Merged
ilia merged 76 commits from feature/fetch-fonts into main 2026-01-14 11:01:44 +00:00
Showing only changes of commit 955cc66916 - Show all commits

View File

@@ -1,263 +1,29 @@
/**
* ============================================================================
* 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 { import {
filterFonts, type Filter,
sortFonts, type FilterModel,
} from '../../lib/filterUtils'; createFilter,
import { createFontshareStore } from '../services/fetchFontshareFonts.svelte'; } from '$shared/lib';
import { createGoogleFontsStore } from '../services/fetchGoogleFonts.svelte'; import { SvelteMap } from 'svelte/reactivity';
import type { UnifiedFont } from '../types/normalize'; import type { FontProvider } from '../types';
import type { import type { CheckboxFilter } from '../types/common';
FetchFontsParams, import type { BaseFontStore } from './baseFontStore.svelte';
FilterType, import { createFontshareStore } from './fontshareStore.svelte';
FontFilters, import type { ProviderParams } from './types';
FontSort,
} from './types';
/** export class UnitedFontStore {
* Creates a unified font store instance. private sources: Partial<Record<FontProvider, BaseFontStore<ProviderParams>>>;
*/
export function createUnifiedFontStore() {
// Instantiate specific stores
const googleStore = createGoogleFontsStore();
const fontshareStore = createFontshareStore();
// Internal state for local filters (that apply to the combined list) filters: SvelteMap<CheckboxFilter, Filter>;
let _filters = $state<FontFilters>({ queryValue = $state('');
providers: [],
categories: [],
subsets: [],
searchQuery: '',
});
let _sort = $state<FontSort>({ field: 'name', direction: 'asc' }); constructor(initialConfig: Partial<Record<FontProvider, ProviderParams>> = {}) {
this.sources = {
// === Debounced Search === fontshare: createFontshareStore(initialConfig?.fontshare),
// 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); this.filters = new SvelteMap();
});
/**
* 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<void> {
// 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 */ get fonts() {
function setFilter(type: FilterType, values: string[]): void { return Object.values(this.sources).map(store => store.fonts).flat();
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<typeof createUnifiedFontStore>;
/**
* Context key for dependency injection
*/
export const UNIFIED_FONT_STORE_KEY = Symbol('UNIFIED_FONT_STORE');