refactor(shared): rename fontCache to collectionCache

- Rename fontCache.ts to collectionCache.ts
- Rename FontCacheManager interface to CollectionCacheManager
- Make implementation fully generic (already was, just renamed interface)
- Update exports in shared/fetch/index.ts
- Fix getStats() to return derived store value for accurate statistics
- Add comprehensive test coverage for collection cache manager
  - 41 test cases covering all functionality
  - Tests for caching, deduplication, state tracking
  - Tests for statistics, reactivity, and edge cases

Closes task-1 of Phase 1 refactoring
This commit is contained in:
Ilia Mashkov
2026-01-06 14:38:55 +03:00
parent 7d2fe49e9c
commit 29d1cc0cdc
21 changed files with 3093 additions and 4 deletions

View File

@@ -0,0 +1,25 @@
/**
* Fetch fonts feature exports
*
* Exports service functions for fetching fonts from Google Fonts and Fontshare
*/
export {
cancelGoogleFontsQueries,
fetchGoogleFontsQuery,
getGoogleFontsQueryKey,
invalidateGoogleFonts,
prefetchGoogleFonts,
useGoogleFontsQuery,
} from './model/services/fetchGoogleFonts';
export type { GoogleFontsQueryParams } from './model/services/fetchGoogleFonts';
export {
cancelFontshareFontsQueries,
fetchFontshareFontsQuery,
getFontshareQueryKey,
invalidateFontshareFonts,
prefetchFontshareFonts,
useFontshareFontsQuery,
} from './model/services/fetchFontshareFonts';
export type { FontshareQueryParams } from './model/services/fetchFontshareFonts';

View File

@@ -0,0 +1,211 @@
/**
* Service for fetching Fontshare fonts
*
* Integrates with TanStack Query for caching, deduplication,
* and automatic refetching.
*/
import { fetchFontshareFonts } from '$entities/Font/api/fontshare';
import { normalizeFontshareFonts } from '$entities/Font/api/normalize';
import type { UnifiedFont } from '$entities/Font/api/normalize';
import type { QueryFunction } from '@tanstack/svelte-query';
import {
createQuery,
useQueryClient,
} from '@tanstack/svelte-query';
/**
* Fontshare query parameters
*/
export interface FontshareQueryParams {
/** Filter by categories (e.g., ["Sans", "Serif"]) */
categories?: string[];
/** Filter by tags (e.g., ["Branding", "Logos"]) */
tags?: string[];
/** Page number for pagination */
page?: number;
/** Number of items per page */
limit?: number;
/** Search query */
search?: string;
}
/**
* Query key factory for Fontshare
* Generates consistent query keys for cache management
*/
export function getFontshareQueryKey(
params: FontshareQueryParams,
): readonly unknown[] {
return ['fontshare', params];
}
/**
* Query function for fetching Fontshare fonts
* Handles caching, loading states, and errors
*/
export const fetchFontshareFontsQuery: QueryFunction<
UnifiedFont[],
readonly unknown[]
> = async ({ queryKey }) => {
const params = queryKey[1] as FontshareQueryParams;
try {
const response = await fetchFontshareFonts({
categories: params.categories,
tags: params.tags,
page: params.page,
limit: params.limit,
search: params.search,
});
const normalizedFonts = normalizeFontshareFonts(response.items);
return normalizedFonts;
} catch (error) {
// User-friendly error messages
if (error instanceof Error) {
if (error.message.includes('Failed to fetch')) {
throw new Error(
'Unable to connect to Fontshare. Please check your internet connection and try again.',
);
}
if (error.message.includes('404')) {
throw new Error('Font not found in Fontshare catalog.');
}
throw new Error(
'Failed to load fonts from Fontshare. Please try again later.',
);
}
throw new Error('An unexpected error occurred while fetching fonts.');
}
};
/**
* Create a Fontshare query hook
* Use this in Svelte components to fetch Fontshare fonts with caching
*
* @param params - Query parameters
* @returns Query result with data, loading state, and error
*
* @example
* ```svelte
* <script lang="ts">
* let { categories }: { categories?: string[] } = $props();
*
* const query = useFontshareFontsQuery({ categories });
*
* if ($query.isLoading) {
* return <LoadingSpinner />;
* }
*
* if ($query.error) {
* return <ErrorMessage message={$query.error.message} />;
* }
*
* const fonts = $query.data ?? [];
* </script>
*
* {#each fonts as font}
* <FontCard {font} />
* {/each}
* ```
*/
export function useFontshareFontsQuery(
params: FontshareQueryParams = {},
) {
const queryClient = useQueryClient();
const query = createQuery(() => ({
queryKey: getFontshareQueryKey(params),
queryFn: fetchFontshareFontsQuery,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
}));
return query;
}
/**
* Prefetch Fontshare fonts
* Fetch fonts in background without showing loading state
*
* @param params - Query parameters for prefetch
*
* @example
* ```ts
* // Prefetch fonts when user hovers over button
* function onMouseEnter() {
* prefetchFontshareFonts({ categories: ['Sans'] });
* }
* ```
*/
export async function prefetchFontshareFonts(
params: FontshareQueryParams = {},
): Promise<void> {
const queryClient = useQueryClient();
await queryClient.prefetchQuery({
queryKey: getFontshareQueryKey(params),
queryFn: fetchFontshareFontsQuery,
staleTime: 5 * 60 * 1000,
});
}
/**
* Invalidate Fontshare cache
* Forces refetch on next query
*
* @param params - Query parameters to invalidate (all if not provided)
*
* @example
* ```ts
* // Invalidate all Fontshare cache
* invalidateFontshareFonts();
*
* // Invalidate specific category cache
* invalidateFontshareFonts({ categories: ['Sans'] });
* ```
*/
export function invalidateFontshareFonts(
params?: FontshareQueryParams,
): void {
const queryClient = useQueryClient();
if (params) {
queryClient.invalidateQueries({
queryKey: getFontshareQueryKey(params),
});
} else {
queryClient.invalidateQueries({
queryKey: ['fontshare'],
});
}
}
/**
* Cancel Fontshare queries
* Abort in-flight requests
*
* @param params - Query parameters to cancel (all if not provided)
*
* @example
* ```ts
* // Cancel all Fontshare queries
* cancelFontshareFontsQueries();
* ```
*/
export function cancelFontshareFontsQueries(
params?: FontshareQueryParams,
): void {
const queryClient = useQueryClient();
if (params) {
queryClient.cancelQueries({
queryKey: getFontshareQueryKey(params),
});
} else {
queryClient.cancelQueries({
queryKey: ['fontshare'],
});
}
}

View File

@@ -0,0 +1,213 @@
/**
* Service for fetching Google Fonts
*
* Integrates with TanStack Query for caching, deduplication,
* and automatic refetching.
*
* Uses reactive query args pattern for Svelte 5 compatibility.
*/
import type {
FontCategory,
FontSubset,
} from '$entities/Font';
import { fetchGoogleFonts } from '$entities/Font/api/googleFonts';
import { normalizeGoogleFonts } from '$entities/Font/api/normalize';
import type { UnifiedFont } from '$entities/Font/api/normalize';
import type { QueryFunction } from '@tanstack/svelte-query';
import {
createQuery,
useQueryClient,
} from '@tanstack/svelte-query';
/**
* Google Fonts query parameters
*/
export interface GoogleFontsQueryParams {
/** Font category filter */
category?: FontCategory;
/** Character subset filter */
subset?: FontSubset;
/** Sort order */
sort?: 'popularity' | 'alpha' | 'date';
/** Search query (for specific font) */
search?: string;
/** Force refetch even if cached */
forceRefetch?: boolean;
}
/**
* Query key factory for Google Fonts
* Generates consistent query keys for cache management
*/
export function getGoogleFontsQueryKey(
params: GoogleFontsQueryParams,
): readonly unknown[] {
return ['googleFonts', params];
}
/**
* Query function for fetching Google Fonts
* Handles caching, loading states, and errors
*/
export const fetchGoogleFontsQuery: QueryFunction<
UnifiedFont[],
readonly unknown[]
> = async ({ queryKey }) => {
const params = queryKey[1] as GoogleFontsQueryParams;
try {
const response = await fetchGoogleFonts({
category: params.category,
subset: params.subset,
sort: params.sort,
});
const normalizedFonts = normalizeGoogleFonts(response.items);
return normalizedFonts;
} catch (error) {
// User-friendly error messages
if (error instanceof Error) {
if (error.message.includes('Failed to fetch')) {
throw new Error(
'Unable to connect to Google Fonts. Please check your internet connection and try again.',
);
}
if (error.message.includes('404')) {
throw new Error('Font not found in Google Fonts catalog.');
}
throw new Error(
'Failed to load fonts from Google Fonts. Please try again later.',
);
}
throw new Error('An unexpected error occurred while fetching fonts.');
}
};
/**
* Create a Google Fonts query hook
* Use this in Svelte components to fetch Google Fonts with caching
*
* @param params - Query parameters
* @returns Query result with data, loading state, and error
*
* @example
* ```svelte
* <script lang="ts">
* let { category }: { category?: FontCategory } = $props();
*
* const query = useGoogleFontsQuery({ category });
*
* if ($query.isLoading) {
* return <LoadingSpinner />;
* }
*
* if ($query.error) {
* return <ErrorMessage message={$query.error.message} />;
* }
*
* const fonts = $query.data ?? [];
* </script>
*
* {#each fonts as font}
* <FontCard {font} />
* {/each}
* ```
*/
export function useGoogleFontsQuery(params: GoogleFontsQueryParams = {}) {
const queryClient = useQueryClient();
const query = createQuery(() => ({
queryKey: getGoogleFontsQueryKey(params),
queryFn: fetchGoogleFontsQuery,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
}));
return query;
}
/**
* Prefetch Google Fonts
* Fetch fonts in background without showing loading state
*
* @param params - Query parameters for prefetch
*
* @example
* ```ts
* // Prefetch fonts when user hovers over button
* function onMouseEnter() {
* prefetchGoogleFonts({ category: 'sans-serif' });
* }
* ```
*/
export async function prefetchGoogleFonts(
params: GoogleFontsQueryParams = {},
): Promise<void> {
const queryClient = useQueryClient();
await queryClient.prefetchQuery({
queryKey: getGoogleFontsQueryKey(params),
queryFn: fetchGoogleFontsQuery,
staleTime: 5 * 60 * 1000,
});
}
/**
* Invalidate Google Fonts cache
* Forces refetch on next query
*
* @param params - Query parameters to invalidate (all if not provided)
*
* @example
* ```ts
* // Invalidate all Google Fonts cache
* invalidateGoogleFonts();
*
* // Invalidate specific category cache
* invalidateGoogleFonts({ category: 'sans-serif' });
* ```
*/
export function invalidateGoogleFonts(
params?: GoogleFontsQueryParams,
): void {
const queryClient = useQueryClient();
if (params) {
queryClient.invalidateQueries({
queryKey: getGoogleFontsQueryKey(params),
});
} else {
queryClient.invalidateQueries({
queryKey: ['googleFonts'],
});
}
}
/**
* Cancel Google Fonts queries
* Abort in-flight requests
*
* @param params - Query parameters to cancel (all if not provided)
*
* @example
* ```ts
* // Cancel all Google Fonts queries
* cancelGoogleFontsQueries();
* ```
*/
export function cancelGoogleFontsQueries(
params?: GoogleFontsQueryParams,
): void {
const queryClient = useQueryClient();
if (params) {
queryClient.cancelQueries({
queryKey: getGoogleFontsQueryKey(params),
});
} else {
queryClient.cancelQueries({
queryKey: ['googleFonts'],
});
}
}

View File

@@ -0,0 +1,76 @@
/**
* Fetch Fonts feature types
*
* Type definitions for font fetching feature
*/
import type {
FontCategory,
FontProvider,
FontSubset,
} from '$entities/Font';
import type { UnifiedFont } from '$entities/Font/api/normalize';
/**
* Combined query parameters for fetching from any provider
*/
export interface FetchFontsParams {
/** Font provider to fetch from */
provider?: FontProvider;
/** Category filter */
category?: FontCategory;
/** Subset filter */
subset?: FontSubset;
/** Search query */
search?: string;
/** Page number (for Fontshare) */
page?: number;
/** Limit (for Fontshare) */
limit?: number;
/** Force refetch even if cached */
forceRefetch?: boolean;
}
/**
* Font fetching result
*/
export interface FetchFontsResult {
/** Fetched fonts */
fonts: UnifiedFont[];
/** Total count (for pagination) */
total?: number;
/** Whether more fonts are available */
hasMore?: boolean;
/** Page number (for pagination) */
page?: number;
}
/**
* Font fetching error
*/
export interface FetchFontsError {
/** Error message */
message: string;
/** Provider that failed */
provider: FontProvider | 'all';
/** HTTP status code (if applicable) */
status?: number;
/** Original error */
originalError?: unknown;
}
/**
* Font fetching state
*/
export interface FetchFontsState {
/** Currently fetching */
isFetching: boolean;
/** Currently loading initial data */
isLoading: boolean;
/** Error state */
error: FetchFontsError | null;
/** Cached fonts */
fonts: UnifiedFont[];
/** Last fetch timestamp */
lastFetchedAt: number | null;
}