import { queryClient } from '$shared/api/queryClient'; import { type QueryKey, QueryObserver, type QueryObserverOptions, type QueryObserverResult, } from '@tanstack/query-core'; import type { UnifiedFont } from '../types'; /** * Base class for font stores using TanStack Query * * Provides reactive font data fetching with caching, automatic refetching, * and parameter binding. Extended by UnifiedFontStore for provider-agnostic * font fetching. * * @template TParams - Type of query parameters */ export abstract class BaseFontStore> { /** * Cleanup function for effects * Call destroy() to remove effects and prevent memory leaks */ cleanup: () => void; /** Reactive parameter bindings from external sources */ #bindings = $state<(() => Partial)[]>([]); /** Internal parameter state */ #internalParams = $state({} as TParams); /** * Merged params from internal state and all bindings * Automatically updates when bindings or internal params change */ params = $derived.by(() => { let merged = { ...this.#internalParams }; // Merge all binding results into params for (const getter of this.#bindings) { const bindingResult = getter(); merged = { ...merged, ...bindingResult }; } return merged as TParams; }); /** TanStack Query result state */ protected result = $state>({} as any); /** TanStack Query observer instance */ protected observer: QueryObserver; /** Shared query client */ protected qc = queryClient; /** * Creates a new base font store * @param initialParams - Initial query parameters */ constructor(initialParams: TParams) { this.#internalParams = initialParams; this.observer = new QueryObserver(this.qc, this.getOptions()); // Sync TanStack Query state -> Svelte state this.observer.subscribe(r => { this.result = r; }); // Sync Svelte state changes -> TanStack Query options this.cleanup = $effect.root(() => { $effect(() => { this.observer.setOptions(this.getOptions()); }); }); } /** * Must be implemented by child class * Returns the query key for TanStack Query caching */ protected abstract getQueryKey(params: TParams): QueryKey; /** * Must be implemented by child class * Fetches font data from API */ protected abstract fetchFn(params: TParams): Promise; /** * Gets TanStack Query options * @param params - Query parameters (defaults to current params) */ protected getOptions(params = this.params): QueryObserverOptions { return { queryKey: this.getQueryKey(params), queryFn: () => this.fetchFn(params), staleTime: 5 * 60 * 1000, gcTime: 10 * 60 * 1000, }; } /** Array of fonts (empty array if loading/error) */ get fonts() { return this.result.data ?? []; } /** Whether currently fetching initial data */ get isLoading() { return this.result.isLoading; } /** Whether any fetch is in progress (including refetches) */ get isFetching() { return this.result.isFetching; } /** Whether last fetch resulted in an error */ get isError() { return this.result.isError; } /** Whether no fonts are loaded (not loading and empty array) */ get isEmpty() { return !this.isLoading && this.fonts.length === 0; } /** * Add a reactive parameter binding * @param getter - Function that returns partial params to merge * @returns Unbind function to remove the binding */ addBinding(getter: () => Partial) { this.#bindings.push(getter); return () => { this.#bindings = this.#bindings.filter(b => b !== getter); }; } /** * Update query parameters * @param newParams - Partial params to merge with existing */ setParams(newParams: Partial) { this.#internalParams = { ...this.params, ...newParams }; } /** * Invalidate cache and refetch */ invalidate() { this.qc.invalidateQueries({ queryKey: this.getQueryKey(this.params) }); } /** * Clean up effects and observers */ destroy() { this.cleanup(); } /** * Manually trigger a refetch */ async refetch() { await this.observer.refetch(); } /** * Prefetch data with different parameters */ async prefetch(params: TParams) { await this.qc.prefetchQuery(this.getOptions(params)); } /** * Cancel ongoing queries */ cancel() { this.qc.cancelQueries({ queryKey: this.getQueryKey(this.params), }); } /** * Clear cache for current params */ clearCache() { this.qc.removeQueries({ queryKey: this.getQueryKey(this.params), }); } /** * Get cached data without triggering fetch */ getCachedData() { return this.qc.getQueryData( this.getQueryKey(this.params), ); } /** * Set data manually (optimistic updates) */ setQueryData(updater: (old: UnifiedFont[] | undefined) => UnifiedFont[]) { this.qc.setQueryData( this.getQueryKey(this.params), updater, ); } }