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:
338
src/entities/Font/model/stores/fontCollectionStore.ts
Normal file
338
src/entities/Font/model/stores/fontCollectionStore.ts
Normal 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,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
13
src/entities/Font/model/stores/index.ts
Normal file
13
src/entities/Font/model/stores/index.ts
Normal 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';
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user