From a29b80efbbeb03f5da20672d0d54c72be5e75594 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Tue, 13 Jan 2026 20:02:20 +0300 Subject: [PATCH] feature: Create BaseFontStore class with Tanstack query logic and FontshareStore, GoogleFontsStore based on it --- .../Font/model/store/baseFontStore.svelte.ts | 156 ++++++++++++++++++ .../Font/model/store/fontshareStore.svelte.ts | 32 ++++ .../model/store/googleFontsStore.svelte.ts | 27 +++ 3 files changed, 215 insertions(+) create mode 100644 src/entities/Font/model/store/baseFontStore.svelte.ts create mode 100644 src/entities/Font/model/store/fontshareStore.svelte.ts create mode 100644 src/entities/Font/model/store/googleFontsStore.svelte.ts diff --git a/src/entities/Font/model/store/baseFontStore.svelte.ts b/src/entities/Font/model/store/baseFontStore.svelte.ts new file mode 100644 index 0000000..d9ffd12 --- /dev/null +++ b/src/entities/Font/model/store/baseFontStore.svelte.ts @@ -0,0 +1,156 @@ +import { queryClient } from '$shared/api/queryClient'; +import { + type QueryKey, + QueryObserver, + type QueryObserverOptions, + type QueryObserverResult, +} from '@tanstack/query-core'; +import type { UnifiedFont } from '../types'; + +/** */ +export abstract class BaseFontStore> { + // params = $state({} as TParams); + cleanup: () => void; + + #bindings = $state<(() => Partial)[]>([]); + #internalParams = $state({} as TParams); + + params = $derived.by(() => { + let merged = { ...this.#internalParams }; + + // Loop through every "Cable" plugged into the store + for (const getter of this.#bindings) { + merged = { ...merged, ...getter() }; + } + + return merged as TParams; + }); + + protected result = $state>({} as any); + protected observer: QueryObserver; + protected qc = queryClient; + + constructor(initialParams: TParams) { + this.#internalParams = initialParams; + + this.observer = new QueryObserver(this.qc, this.getOptions()); + + // Sync TanStack -> Svelte State + this.observer.subscribe(r => { + this.result = r; + }); + + // Sync Svelte State -> TanStack Options + this.cleanup = $effect.root(() => { + $effect(() => { + this.observer.setOptions(this.getOptions()); + }); + }); + } + + /** + * Mandatory: Child must define how to fetch data and what the key is. + */ + protected abstract getQueryKey(params: TParams): QueryKey; + protected abstract fetchFn(params: TParams): Promise; + + private getOptions(params = this.params): QueryObserverOptions { + return { + queryKey: this.getQueryKey(params), + queryFn: () => this.fetchFn(params), + staleTime: 5 * 60 * 1000, + }; + } + + // --- Common Getters --- + get fonts() { + return this.result.data ?? []; + } + get isLoading() { + return this.result.isLoading; + } + get isFetching() { + return this.result.isFetching; + } + get isError() { + return this.result.isError; + } + get isEmpty() { + return !this.isLoading && this.fonts.length === 0; + } + + // --- Common Actions --- + + addBinding(getter: () => Partial) { + this.#bindings.push(getter); + + return () => { + this.#bindings = this.#bindings.filter(b => b !== getter); + }; + } + + setParams(newParams: Partial) { + this.#internalParams = { ...this.params, ...newParams }; + } + /** + * Invalidate cache and refetch + */ + invalidate() { + this.qc.invalidateQueries({ queryKey: this.getQueryKey(this.params) }); + } + + destroy() { + this.cleanup(); + } + + /** + * Manually refetch + */ + async refetch() { + await this.observer.refetch(); + } + + /** + * Prefetch with different params (for hover states, pagination, etc.) + */ + 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, + ); + } +} diff --git a/src/entities/Font/model/store/fontshareStore.svelte.ts b/src/entities/Font/model/store/fontshareStore.svelte.ts new file mode 100644 index 0000000..cec798d --- /dev/null +++ b/src/entities/Font/model/store/fontshareStore.svelte.ts @@ -0,0 +1,32 @@ +import type { FontshareParams } from '../../api'; +import { fetchFontshareFontsQuery } from '../services'; +import type { UnifiedFont } from '../types'; +import { BaseFontStore } from './baseFontStore.svelte'; + +/** + * Fontshare store wrapping TanStack Query with runes + */ +export class FontshareStore extends BaseFontStore { + constructor(initialParams: FontshareParams = {}) { + super(initialParams); + } + + protected getQueryKey(params: FontshareParams) { + return ['fontshare', params] as const; + } + + protected async fetchFn(params: FontshareParams): Promise { + return fetchFontshareFontsQuery(params); + } + + // Provider-specific methods (shortcuts) + setSearch(search: string) { + this.setParams({ q: search } as any); + } +} + +export function createFontshareStore(params: FontshareParams = {}) { + return new FontshareStore(params); +} + +export const fontshareStore = new FontshareStore(); diff --git a/src/entities/Font/model/store/googleFontsStore.svelte.ts b/src/entities/Font/model/store/googleFontsStore.svelte.ts new file mode 100644 index 0000000..428c476 --- /dev/null +++ b/src/entities/Font/model/store/googleFontsStore.svelte.ts @@ -0,0 +1,27 @@ +import type { GoogleFontsParams } from '../../api'; +import { fetchGoogleFontsQuery } from '../services'; +import type { UnifiedFont } from '../types'; +import { BaseFontStore } from './baseFontStore.svelte'; + +/** + * Google Fonts store wrapping TanStack Query with runes + */ +export class GoogleFontsStore extends BaseFontStore { + constructor(initialParams: GoogleFontsParams = {}) { + super(initialParams); + } + + protected getQueryKey(params: GoogleFontsParams) { + return ['googleFonts', params] as const; + } + + protected async fetchFn(params: GoogleFontsParams): Promise { + return fetchGoogleFontsQuery(params); + } +} + +export function createFontshareStore(params: GoogleFontsParams = {}) { + return new GoogleFontsStore(params); +} + +export const googleFontsStore = new GoogleFontsStore();