refactor(Font): consolidate API layer and update type structure

This commit is contained in:
Ilia Mashkov
2026-03-02 22:18:21 +03:00
parent ba186d00a1
commit af4137f47f
17 changed files with 325 additions and 558 deletions

View File

@@ -7,41 +7,64 @@ import {
} 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<TParams extends Record<string, any>> {
/**
* Cleanup function for effects
* Call destroy() to remove effects and prevent memory leaks
*/
cleanup: () => void;
/** Reactive parameter bindings from external sources */
#bindings = $state<(() => Partial<TParams>)[]>([]);
/** Internal parameter state */
#internalParams = $state<TParams>({} 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 };
// Loop through every "Cable" plugged into the store
// Loop through every "Cable" plugged into the store
// 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<QueryObserverResult<UnifiedFont[], Error>>({} as any);
/** TanStack Query observer instance */
protected observer: QueryObserver<UnifiedFont[], Error>;
/** 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 -> Svelte State
// Sync TanStack Query state -> Svelte state
this.observer.subscribe(r => {
this.result = r;
});
// Sync Svelte State -> TanStack Options
// Sync Svelte state changes -> TanStack Query options
this.cleanup = $effect.root(() => {
$effect(() => {
this.observer.setOptions(this.getOptions());
@@ -50,11 +73,21 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
}
/**
* Mandatory: Child must define how to fetch data and what the key is.
* 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<UnifiedFont[]>;
/**
* Gets TanStack Query options
* @param params - Query parameters (defaults to current params)
*/
protected getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
return {
queryKey: this.getQueryKey(params),
@@ -64,25 +97,36 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
};
}
// --- Common Getters ---
/** 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;
}
// --- Common Actions ---
/**
* Add a reactive parameter binding
* @param getter - Function that returns partial params to merge
* @returns Unbind function to remove the binding
*/
addBinding(getter: () => Partial<TParams>) {
this.#bindings.push(getter);
@@ -91,9 +135,14 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
};
}
/**
* Update query parameters
* @param newParams - Partial params to merge with existing
*/
setParams(newParams: Partial<TParams>) {
this.#internalParams = { ...this.params, ...newParams };
}
/**
* Invalidate cache and refetch
*/
@@ -101,19 +150,22 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
this.qc.invalidateQueries({ queryKey: this.getQueryKey(this.params) });
}
/**
* Clean up effects and observers
*/
destroy() {
this.cleanup();
}
/**
* Manually refetch
* Manually trigger a refetch
*/
async refetch() {
await this.observer.refetch();
}
/**
* Prefetch with different params (for hover states, pagination, etc.)
* Prefetch data with different parameters
*/
async prefetch(params: TParams) {
await this.qc.prefetchQuery(this.getOptions(params));

View File

@@ -1,43 +0,0 @@
/**
* ============================================================================
* UNIFIED FONT STORE TYPES
* ============================================================================
*
* Type definitions for the unified font store infrastructure.
* Provides types for filters, sorting, and fetch parameters.
*/
import type {
FontshareParams,
GoogleFontsParams,
} from '$entities/Font/api';
import type {
FontCategory,
FontProvider,
FontSubset,
} from '$entities/Font/model/types/common';
/**
* Sort configuration
*/
export interface FontSort {
field: 'name' | 'popularity' | 'category' | 'date';
direction: 'asc' | 'desc';
}
/**
* Fetch params for unified API
*/
export interface FetchFontsParams {
providers?: FontProvider[];
categories?: FontCategory[];
subsets?: FontSubset[];
search?: string;
sort?: FontSort;
forceRefetch?: boolean;
}
/**
* Provider-specific params union
*/
export type ProviderParams = GoogleFontsParams | FontshareParams;

View File

@@ -43,7 +43,7 @@ import { BaseFontStore } from './baseFontStore.svelte';
* });
*
* // Update parameters
* store.setCategory('serif');
* store.setCategories(['serif']);
* store.nextPage();
* ```
*/
@@ -108,16 +108,20 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
this.#filterCleanup = $effect.root(() => {
$effect(() => {
const filterParams = JSON.stringify({
provider: this.params.provider,
category: this.params.category,
subset: this.params.subset,
providers: this.params.providers,
categories: this.params.categories,
subsets: this.params.subsets,
q: this.params.q,
});
// If filters changed, reset offset to 0
// If filters changed, reset offset and invalidate cache
if (filterParams !== this.#previousFilterParams) {
if (this.#previousFilterParams && this.params.offset !== 0) {
this.setParams({ offset: 0 });
if (this.#previousFilterParams) {
if (this.params.offset !== 0) {
this.setParams({ offset: 0 });
}
this.#accumulatedFonts = [];
this.invalidate();
}
this.#previousFilterParams = filterParams;
}
@@ -170,7 +174,7 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
}
protected getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
const hasFilters = !!(params.q || params.provider || params.category || params.subset);
const hasFilters = !!(params.q || params.providers || params.categories || params.subsets);
return {
queryKey: this.getQueryKey(params),
queryFn: () => this.fetchFn(params),
@@ -221,8 +225,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
return response.fonts;
}
// --- Getters (proxied from BaseFontStore) ---
/**
* Get all accumulated fonts (for infinite scroll)
*/
@@ -258,27 +260,25 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
return !this.isLoading && this.fonts.length === 0;
}
// --- Provider-specific shortcuts ---
/**
* Set provider filter
* Set providers filter
*/
setProvider(provider: 'google' | 'fontshare' | undefined) {
this.setParams({ provider });
setProviders(providers: ProxyFontsParams['providers']) {
this.setParams({ providers });
}
/**
* Set category filter
* Set categories filter
*/
setCategory(category: ProxyFontsParams['category']) {
this.setParams({ category });
setCategories(categories: ProxyFontsParams['categories']) {
this.setParams({ categories });
}
/**
* Set subset filter
* Set subsets filter
*/
setSubset(subset: ProxyFontsParams['subset']) {
this.setParams({ subset });
setSubsets(subsets: ProxyFontsParams['subsets']) {
this.setParams({ subsets });
}
/**
@@ -295,8 +295,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
this.setParams({ sort });
}
// --- Pagination methods ---
/**
* Go to next page
*/
@@ -337,8 +335,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
this.setParams({ limit });
}
// --- Category shortcuts (for convenience) ---
get sansSerifFonts() {
return this.fonts.filter(f => f.category === 'sans-serif');
}