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:
211
src/features/FetchFonts/model/services/fetchFontshareFonts.ts
Normal file
211
src/features/FetchFonts/model/services/fetchFontshareFonts.ts
Normal 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'],
|
||||
});
|
||||
}
|
||||
}
|
||||
213
src/features/FetchFonts/model/services/fetchGoogleFonts.ts
Normal file
213
src/features/FetchFonts/model/services/fetchGoogleFonts.ts
Normal 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'],
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user