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,338 @@
/**
* Font collection store
*
* Main font collection cache using Svelte stores.
* Integrates with TanStack Query for advanced caching and deduplication.
*
* Provides derived stores for filtered/sorted fonts.
*/
import type { UnifiedFont } from '$entities/Font/api/normalize';
import {
type CollectionCacheManager,
createCollectionCache,
} from '$shared/fetch/collectionCache';
import type {
Readable,
Writable,
} from 'svelte/store';
import {
derived,
get,
writable,
} from 'svelte/store';
import type {
FontCategory,
FontProvider,
} from '../types/font';
/**
* Font collection state
*/
export interface FontCollectionState {
/** All cached fonts */
fonts: Record<string, UnifiedFont>;
/** Active filters */
filters: FontCollectionFilters;
/** Sort configuration */
sort: FontCollectionSort;
}
/**
* Font collection filters
*/
export interface FontCollectionFilters {
/** Search query */
searchQuery?: string;
/** Filter by provider */
provider?: FontProvider;
/** Filter by category */
category?: FontCategory;
/** Filter by subsets */
subsets?: string[];
}
/**
* Font collection sort configuration
*/
export interface FontCollectionSort {
/** Sort field */
field: 'name' | 'popularity' | 'category';
/** Sort direction */
direction: 'asc' | 'desc';
}
/**
* Font collection store interface
*/
export interface FontCollectionStore {
/** Main state store */
state: Writable<FontCollectionState>;
/** All fonts as array */
fonts: Readable<UnifiedFont[]>;
/** Filtered fonts as array */
filteredFonts: Readable<UnifiedFont[]>;
/** Number of fonts in collection */
count: Readable<number>;
/** Loading state */
isLoading: Readable<boolean>;
/** Error state */
error: Readable<string | undefined>;
/** Add fonts to collection */
addFonts: (fonts: UnifiedFont[]) => void;
/** Add single font to collection */
addFont: (font: UnifiedFont) => void;
/** Remove font from collection */
removeFont: (fontId: string) => void;
/** Clear all fonts */
clear: () => void;
/** Update filters */
setFilters: (filters: Partial<FontCollectionFilters>) => void;
/** Clear filters */
clearFilters: () => void;
/** Update sort configuration */
setSort: (sort: FontCollectionSort) => void;
/** Get font by ID */
getFont: (fontId: string) => UnifiedFont | undefined;
/** Get fonts by provider */
getFontsByProvider: (provider: FontProvider) => UnifiedFont[];
/** Get fonts by category */
getFontsByCategory: (category: FontCategory) => UnifiedFont[];
}
/**
* Create font collection store
*
* @param initialState - Initial state for collection
* @returns Font collection store instance
*
* @example
* ```ts
* const fontCollection = createFontCollectionStore({
* fonts: {},
* filters: {},
* sort: { field: 'name', direction: 'asc' }
* });
*
* // Add fonts to collection
* fontCollection.addFonts([font1, font2]);
*
* // Use in component
* $fontCollection.filteredFonts
* ```
*/
export function createFontCollectionStore(
initialState?: Partial<FontCollectionState>,
): FontCollectionStore {
const cache = createCollectionCache<UnifiedFont>({
defaultTTL: 5 * 60 * 1000, // 5 minutes
maxSize: 1000,
});
const defaultState: FontCollectionState = {
fonts: {},
filters: {},
sort: { field: 'name', direction: 'asc' },
};
const state: Writable<FontCollectionState> = writable({
...defaultState,
...initialState,
});
const isLoading = writable(false);
const error = writable<string | undefined>();
// Derived store for fonts as array
const fonts = derived(state, $state => {
return Object.values($state.fonts);
});
// Derived store for filtered fonts
const filteredFonts = derived([state, fonts], ([$state, $fonts]) => {
let filtered = [...$fonts];
// Apply search filter
if ($state.filters.searchQuery) {
const query = $state.filters.searchQuery.toLowerCase();
filtered = filtered.filter(font => font.name.toLowerCase().includes(query));
}
// Apply provider filter
if ($state.filters.provider) {
filtered = filtered.filter(
font => font.provider === $state.filters.provider,
);
}
// Apply category filter
if ($state.filters.category) {
filtered = filtered.filter(
font => font.category === $state.filters.category,
);
}
// Apply subset filter
if ($state.filters.subsets?.length) {
filtered = filtered.filter(font =>
$state.filters.subsets!.some(subset => font.subsets.includes(subset as any))
);
}
// Apply sort
const { field, direction } = $state.sort;
const multiplier = direction === 'asc' ? 1 : -1;
filtered.sort((a, b) => {
let comparison = 0;
if (field === 'name') {
comparison = a.name.localeCompare(b.name);
} else if (field === 'popularity') {
const aPop = a.metadata.popularity ?? 0;
const bPop = b.metadata.popularity ?? 0;
comparison = aPop - bPop;
} else if (field === 'category') {
comparison = a.category.localeCompare(b.category);
}
return comparison * multiplier;
});
return filtered;
});
// Derived store for count
const count = derived(fonts, $fonts => $fonts.length);
return {
// Expose main state
state,
// Expose derived stores
fonts,
filteredFonts,
count,
isLoading,
error,
/**
* Add multiple fonts to collection
*/
addFonts: (newFonts: UnifiedFont[]) => {
state.update($state => {
const fontsMap = { ...$state.fonts };
for (const font of newFonts) {
fontsMap[font.id] = font;
cache.set(font.id, font);
}
return {
...$state,
fonts: fontsMap,
};
});
},
/**
* Add single font to collection
*/
addFont: (font: UnifiedFont) => {
state.update($state => ({
...$state,
fonts: {
...$state.fonts,
[font.id]: font,
},
}));
cache.set(font.id, font);
},
/**
* Remove font from collection
*/
removeFont: (fontId: string) => {
state.update($state => {
const { [fontId]: _, ...rest } = $state.fonts;
return {
...$state,
fonts: rest,
};
});
cache.remove(fontId);
},
/**
* Clear all fonts
*/
clear: () => {
state.set({
...get(state),
fonts: {},
});
cache.clear();
},
/**
* Update filters
*/
setFilters: (filters: Partial<FontCollectionFilters>) => {
state.update($state => ({
...$state,
filters: {
...$state.filters,
...filters,
},
}));
},
/**
* Clear filters
*/
clearFilters: () => {
state.update($state => ({
...$state,
filters: {},
}));
},
/**
* Update sort configuration
*/
setSort: (sort: FontCollectionSort) => {
state.update($state => ({
...$state,
sort,
}));
},
/**
* Get font by ID
*/
getFont: (fontId: string) => {
const currentState = get(state);
return currentState.fonts[fontId];
},
/**
* Get fonts by provider
*/
getFontsByProvider: (provider: FontProvider) => {
const currentState = get(state);
return Object.values(currentState.fonts).filter(
font => font.provider === provider,
);
},
/**
* Get fonts by category
*/
getFontsByCategory: (category: FontCategory) => {
const currentState = get(state);
return Object.values(currentState.fonts).filter(
font => font.category === category,
);
},
};
}

View File

@@ -0,0 +1,13 @@
/**
* Font collection store exports
*
* Exports font collection store types and factory function
*/
export { createFontCollectionStore } from './fontCollectionStore';
export type {
FontCollectionFilters,
FontCollectionSort,
FontCollectionState,
FontCollectionStore,
} from './fontCollectionStore';

View File

@@ -1,4 +1,4 @@
import type { CollectionApiModel } from '../../../shared/types/collection';
import type { CollectionApiModel } from '$shared/types/collection';
export const FONTSHARE_API_URL = 'https://api.fontshare.com/v2' as const;