feat: implement P0/P1 performance and code quality optimizations

P0 Performance Optimizations:
- Add debounced search (300ms) to reduce re-renders during typing
- Implement single-pass filter function for O(n) complexity
- Add TanStack Query cancellation before new requests

P1 Code Quality Optimizations:
- Add runtime type guards for filter validation
- Implement two derived values (filteredFonts + sortedFilteredFonts)
- Remove all 'as any[]' casts from filter bridge
- Add fast-path for default sorting (skip unnecessary operations)

New Utilities:
- debounce utility with 4 tests (all pass)
- filterUtils with 15 tests (all pass)
- typeGuards with 20 tests (all pass)
- Total: 39 new tests

Modified Files:
- unifiedFontStore.svelte.ts: Add debouncing, use filter/sort utilities
- filterBridge.svelte.ts: Type-safe validation with type guards
- unifiedFontStore.test.ts: Fix pre-existing bugs (missing async, duplicate imports)

Code Quality:
- 0 linting warnings/errors (oxlint)
- FSD compliant architecture (entity lib layer)
- Backward compatible store API
This commit is contained in:
Ilia Mashkov
2026-01-11 14:49:21 +03:00
parent 77de829b04
commit d81af0a77b
13 changed files with 1448 additions and 0 deletions

View File

@@ -0,0 +1,297 @@
import {
describe,
expect,
it,
} from 'vitest';
import type { UnifiedFont } from '../model/types/normalize';
import {
filterFonts,
sortFonts,
} from './filterUtils';
const createMockFont = (overrides: Partial<UnifiedFont> = {}): UnifiedFont => ({
id: 'test-1',
name: 'Test Font',
provider: 'google',
category: 'sans-serif',
subsets: ['latin'],
variants: ['400'],
styles: {
regular: 'https://example.com/font.woff2',
},
metadata: {
cachedAt: Date.now(),
popularity: 100,
},
features: {},
...overrides,
});
describe('filterUtils', () => {
describe('filterFonts', () => {
it('should return all fonts when no filters are applied', () => {
const fonts = [
createMockFont({ id: '1', name: 'Arial' }),
createMockFont({ id: '2', name: 'Times New Roman' }),
];
const result = filterFonts(fonts, {
providers: [],
categories: [],
subsets: [],
searchQuery: '',
});
expect(result).toEqual(fonts);
});
it('should filter by provider', () => {
const fonts = [
createMockFont({ id: '1', provider: 'google' }),
createMockFont({ id: '2', provider: 'fontshare' }),
];
const result = filterFonts(fonts, {
providers: ['google'],
categories: [],
subsets: [],
searchQuery: '',
});
expect(result).toHaveLength(1);
expect(result[0].provider).toBe('google');
});
it('should filter by category', () => {
const fonts = [
createMockFont({ id: '1', category: 'sans-serif' }),
createMockFont({ id: '2', category: 'serif' }),
];
const result = filterFonts(fonts, {
providers: [],
categories: ['sans-serif'],
subsets: [],
searchQuery: '',
});
expect(result).toHaveLength(1);
expect(result[0].category).toBe('sans-serif');
});
it('should filter by subsets', () => {
const fonts = [
createMockFont({ id: '1', subsets: ['latin'] }),
createMockFont({ id: '2', subsets: ['cyrillic'] }),
];
const result = filterFonts(fonts, {
providers: [],
categories: [],
subsets: ['latin'],
searchQuery: '',
});
expect(result).toHaveLength(1);
expect(result[0].subsets).toContain('latin');
});
it('should filter by search query (case-insensitive)', () => {
const fonts = [
createMockFont({ id: '1', name: 'Roboto' }),
createMockFont({ id: '2', name: 'Open Sans' }),
];
const result = filterFonts(fonts, {
providers: [],
categories: [],
subsets: [],
searchQuery: 'ROBO',
});
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Roboto');
});
it('should apply multiple filters simultaneously', () => {
const fonts = [
createMockFont({
id: '1',
name: 'Roboto',
provider: 'google',
category: 'sans-serif',
subsets: ['latin'],
}),
createMockFont({
id: '2',
name: 'Playfair',
provider: 'google',
category: 'serif',
subsets: ['latin'],
}),
createMockFont({
id: '3',
name: 'Lato',
provider: 'fontshare',
category: 'sans-serif',
subsets: ['latin'],
}),
];
const result = filterFonts(fonts, {
providers: ['google'],
categories: ['sans-serif'],
subsets: [],
searchQuery: '',
});
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Roboto');
});
it('should handle empty result set', () => {
const fonts = [
createMockFont({ id: '1', name: 'Roboto' }),
];
const result = filterFonts(fonts, {
providers: ['fontshare'],
categories: [],
subsets: [],
searchQuery: '',
});
expect(result).toHaveLength(0);
});
it('should be case-insensitive for search', () => {
const fonts = [
createMockFont({ id: '1', name: 'Roboto' }),
createMockFont({ id: '2', name: 'roboto-condensed' }),
];
const result = filterFonts(fonts, {
providers: [],
categories: [],
subsets: [],
searchQuery: 'ROBO',
});
expect(result).toHaveLength(2);
});
});
describe('sortFonts', () => {
it('should sort by name ascending', () => {
const fonts = [
createMockFont({ id: '1', name: 'Zebra' }),
createMockFont({ id: '2', name: 'Apple' }),
createMockFont({ id: '3', name: 'Middle' }),
];
const result = sortFonts(fonts, { field: 'name', direction: 'asc' });
expect(result[0].name).toBe('Apple');
expect(result[1].name).toBe('Middle');
expect(result[2].name).toBe('Zebra');
});
it('should sort by name descending', () => {
const fonts = [
createMockFont({ id: '1', name: 'Zebra' }),
createMockFont({ id: '2', name: 'Apple' }),
];
const result = sortFonts(fonts, { field: 'name', direction: 'desc' });
expect(result[0].name).toBe('Zebra');
expect(result[1].name).toBe('Apple');
});
it('should sort by category', () => {
const fonts = [
createMockFont({ id: '1', category: 'serif' }),
createMockFont({ id: '2', category: 'sans-serif' }),
createMockFont({ id: '3', category: 'display' }),
];
const result = sortFonts(fonts, { field: 'category', direction: 'asc' });
expect(result[0].category).toBe('display');
expect(result[1].category).toBe('sans-serif');
expect(result[2].category).toBe('serif');
});
it('should sort by popularity (most popular first for asc)', () => {
const fonts = [
createMockFont({ id: '1', metadata: { cachedAt: Date.now(), popularity: 10 } }),
createMockFont({ id: '2', metadata: { cachedAt: Date.now(), popularity: 100 } }),
createMockFont({ id: '3', metadata: { cachedAt: Date.now(), popularity: 50 } }),
];
const result = sortFonts(fonts, { field: 'popularity', direction: 'asc' });
// Higher popularity comes first
expect(result[0].metadata.popularity).toBe(100);
expect(result[1].metadata.popularity).toBe(50);
expect(result[2].metadata.popularity).toBe(10);
});
it('should sort by date (most recent first for asc)', () => {
const now = Date.now();
const fonts = [
createMockFont({
id: '1',
metadata: { cachedAt: now - 10000, popularity: 0 },
}),
createMockFont({
id: '2',
metadata: { cachedAt: now, popularity: 0 },
}),
createMockFont({
id: '3',
metadata: { cachedAt: now - 5000, popularity: 0 },
}),
];
const result = sortFonts(fonts, { field: 'date', direction: 'asc' });
// Most recent comes first
expect(result[0].metadata.cachedAt).toBe(now);
expect(result[1].metadata.cachedAt).toBe(now - 5000);
expect(result[2].metadata.cachedAt).toBe(now - 10000);
});
it('should handle undefined popularity values', () => {
const fonts = [
createMockFont({
id: '1',
metadata: { cachedAt: Date.now(), popularity: 50 },
}),
createMockFont({
id: '2',
metadata: { cachedAt: Date.now() },
}),
];
const result = sortFonts(fonts, { field: 'popularity', direction: 'asc' });
// Undefined should be treated as 0
expect(result[0].metadata.popularity).toBe(50);
expect(result[1].metadata.popularity).toBe(undefined);
});
it('should return a new array without modifying original', () => {
const fonts = [
createMockFont({ id: '1', name: 'Zebra' }),
createMockFont({ id: '2', name: 'Apple' }),
];
const result = sortFonts(fonts, { field: 'name', direction: 'asc' });
expect(result).not.toBe(fonts);
expect(result[0].name).toBe('Apple');
expect(fonts[0].name).toBe('Zebra'); // Original unchanged
});
});
});

View File

@@ -0,0 +1,100 @@
/**
* ============================================================================
* FONT FILTER UTILITIES
* ============================================================================
*
* Optimized utilities for filtering and sorting fonts in a single pass.
* These utilities are entity-specific and located in the Font entity layer
* following FSD (Feature-Sliced Design) principles.
*/
import type {
FontFilters,
FontSort,
} from '../model/store/types';
import type { UnifiedFont } from '../model/types/normalize';
/**
* Single-pass filter function for fonts
*
* Applies all filters in a single iteration for O(n) complexity.
* This is more efficient than chaining multiple filter() calls which
* would create intermediate arrays.
*
* @param fonts - The fonts to filter
* @param filters - The filter criteria
* @returns Filtered fonts
*/
export function filterFonts(
fonts: UnifiedFont[],
filters: FontFilters,
): UnifiedFont[] {
const hasSearch = !!filters.searchQuery;
const hasProviders = filters.providers.length > 0;
const hasCategories = filters.categories.length > 0;
const hasSubsets = filters.subsets.length > 0;
// Fast path: no filters
if (!hasSearch && !hasProviders && !hasCategories && !hasSubsets) {
return fonts;
}
const searchQuery = hasSearch ? filters.searchQuery.toLowerCase() : '';
// Single-pass filter
return fonts.filter(font => {
// Provider filter
if (hasProviders && !filters.providers.includes(font.provider)) {
return false;
}
// Category filter
if (hasCategories && !filters.categories.includes(font.category)) {
return false;
}
// Subset filter
if (hasSubsets && !filters.subsets.some(s => font.subsets.includes(s))) {
return false;
}
// Search filter
if (hasSearch) {
const nameMatch = font.name.toLowerCase().includes(searchQuery);
const familyMatch = font.name.toLowerCase().includes(searchQuery);
if (!nameMatch && !familyMatch) {
return false;
}
}
return true;
});
}
/**
* Sort fonts by specified field and direction
*
* @param fonts - The fonts to sort
* @param sort - The sort configuration
* @returns Sorted fonts
*/
export function sortFonts(fonts: UnifiedFont[], sort: FontSort): UnifiedFont[] {
const { field, direction } = sort;
const multiplier = direction === 'asc' ? 1 : -1;
return [...fonts].sort((a, b) => {
switch (field) {
case 'name':
return a.name.localeCompare(b.name) * multiplier;
case 'category':
return a.category.localeCompare(b.category) * multiplier;
case 'popularity':
return ((b.metadata.popularity ?? 0) - (a.metadata.popularity ?? 0)) * multiplier;
case 'date':
// Sort by cachedAt timestamp as a proxy for date
return ((b.metadata.cachedAt ?? 0) - (a.metadata.cachedAt ?? 0)) * multiplier;
default:
return 0;
}
});
}

View File

@@ -2,3 +2,17 @@ export {
createFontCollection,
type FontCollectionStore,
} from './helpers/createFontCollection.svelte';
export {
filterFonts,
sortFonts,
} from './filterUtils';
export {
filterValidValues,
isValidFontCategory,
isValidFontFilters,
isValidFontProvider,
isValidFontSubset,
validateFilterValues,
} from './typeGuards';

View File

@@ -0,0 +1,194 @@
import {
describe,
expect,
it,
} from 'vitest';
import type {
FontCategory,
FontProvider,
FontSubset,
} from '../model/types/common';
import {
filterValidValues,
isValidFontCategory,
isValidFontFilters,
isValidFontProvider,
isValidFontSubset,
validateFilterValues,
} from './typeGuards';
describe('typeGuards', () => {
describe('isValidFontProvider', () => {
it('should return true for valid providers', () => {
expect(isValidFontProvider('google')).toBe(true);
expect(isValidFontProvider('fontshare')).toBe(true);
});
it('should return false for invalid providers', () => {
expect(isValidFontProvider('invalid')).toBe(false);
expect(isValidFontProvider('Google')).toBe(false); // Case-sensitive
expect(isValidFontProvider('')).toBe(false);
});
});
describe('isValidFontCategory', () => {
it('should return true for valid categories', () => {
expect(isValidFontCategory('serif')).toBe(true);
expect(isValidFontCategory('sans-serif')).toBe(true);
expect(isValidFontCategory('display')).toBe(true);
expect(isValidFontCategory('handwriting')).toBe(true);
expect(isValidFontCategory('monospace')).toBe(true);
expect(isValidFontCategory('script')).toBe(true);
expect(isValidFontCategory('slab')).toBe(true);
});
it('should return false for invalid categories', () => {
expect(isValidFontCategory('invalid')).toBe(false);
expect(isValidFontCategory('Serif')).toBe(false); // Case-sensitive
expect(isValidFontCategory('')).toBe(false);
});
});
describe('isValidFontSubset', () => {
it('should return true for valid subsets', () => {
expect(isValidFontSubset('latin')).toBe(true);
expect(isValidFontSubset('latin-ext')).toBe(true);
expect(isValidFontSubset('cyrillic')).toBe(true);
expect(isValidFontSubset('greek')).toBe(true);
expect(isValidFontSubset('arabic')).toBe(true);
expect(isValidFontSubset('devanagari')).toBe(true);
});
it('should return false for invalid subsets', () => {
expect(isValidFontSubset('invalid')).toBe(false);
expect(isValidFontSubset('Latin')).toBe(false); // Case-sensitive
expect(isValidFontSubset('')).toBe(false);
});
});
describe('validateFilterValues', () => {
it('should return true when all values are valid providers', () => {
const values = ['google', 'fontshare'];
expect(validateFilterValues(values, isValidFontProvider)).toBe(true);
});
it('should return true when all values are valid categories', () => {
const values = ['serif', 'sans-serif', 'display'];
expect(validateFilterValues(values, isValidFontCategory)).toBe(true);
});
it('should return false when any value is invalid', () => {
const values = ['serif', 'invalid-category'];
expect(validateFilterValues(values, isValidFontCategory)).toBe(false);
});
it('should return true for empty arrays', () => {
const values: string[] = [];
expect(validateFilterValues(values, isValidFontProvider)).toBe(true);
});
});
describe('filterValidValues', () => {
it('should filter out invalid values', () => {
const values = ['google', 'invalid', 'fontshare', 'another-invalid'];
const result = filterValidValues(values, isValidFontProvider);
expect(result).toEqual(['google', 'fontshare']);
});
it('should return empty array when no values are valid', () => {
const values = ['invalid1', 'invalid2'];
const result = filterValidValues(values, isValidFontProvider);
expect(result).toEqual([]);
});
it('should return all values when all are valid', () => {
const values = ['serif', 'sans-serif'];
const result = filterValidValues(values, isValidFontCategory);
expect(result).toEqual(['serif', 'sans-serif']);
});
it('should return empty array for empty input', () => {
const result = filterValidValues([], isValidFontProvider);
expect(result).toEqual([]);
});
});
describe('isValidFontFilters', () => {
it('should return true for valid filter object', () => {
const filters = {
providers: ['google', 'fontshare'],
categories: ['serif', 'sans-serif'],
subsets: ['latin'],
};
expect(isValidFontFilters(filters)).toBe(true);
});
it('should return false when providers are invalid', () => {
const filters = {
providers: ['invalid'],
categories: ['serif'],
subsets: ['latin'],
};
expect(isValidFontFilters(filters)).toBe(false);
});
it('should return false when categories are invalid', () => {
const filters = {
providers: ['google'],
categories: ['invalid'],
subsets: ['latin'],
};
expect(isValidFontFilters(filters)).toBe(false);
});
it('should return false when subsets are invalid', () => {
const filters = {
providers: ['google'],
categories: ['serif'],
subsets: ['invalid'],
};
expect(isValidFontFilters(filters)).toBe(false);
});
it('should return true for empty arrays', () => {
const filters = {
providers: [],
categories: [],
subsets: [],
};
expect(isValidFontFilters(filters)).toBe(true);
});
it('should type narrow correctly for TypeScript', () => {
const filters: {
providers: string[];
categories: string[];
subsets: string[];
} = {
providers: ['google', 'fontshare'],
categories: ['serif'],
subsets: ['latin'],
};
if (isValidFontFilters(filters)) {
// TypeScript should know these are now typed arrays
const provider: FontProvider = filters.providers[0]!;
const category: FontCategory = filters.categories[0]!;
const subset: FontSubset = filters.subsets[0]!;
expect(provider).toBe('google');
expect(category).toBe('serif');
expect(subset).toBe('latin');
}
});
});
});

View File

@@ -0,0 +1,100 @@
/**
* ============================================================================
* FONT TYPE GUARDS
* ============================================================================
*
* Runtime type validation utilities for font-related types.
* These type guards validate filter values to ensure type safety
* when bridging between UI components and the store.
*/
import {
FONT_CATEGORIES,
FONT_PROVIDERS,
FONT_SUBSETS,
} from '$features/FilterFonts/model/const/const';
import type {
FontCategory,
FontProvider,
FontSubset,
} from '../model/types/common';
/**
* Type guard for font providers
*
* @param value - The value to validate
* @returns True if the value is a valid FontProvider
*/
export function isValidFontProvider(value: string): value is FontProvider {
return FONT_PROVIDERS.some(p => p.value === value);
}
/**
* Type guard for font categories
*
* @param value - The value to validate
* @returns True if the value is a valid FontCategory
*/
export function isValidFontCategory(value: string): value is FontCategory {
return FONT_CATEGORIES.some(c => c.value === value);
}
/**
* Type guard for font subsets
*
* @param value - The value to validate
* @returns True if the value is a valid FontSubset
*/
export function isValidFontSubset(value: string): value is FontSubset {
return FONT_SUBSETS.some(s => s.value === value);
}
/**
* Validate array of filter values using a type guard
*
* @param values - The array of values to validate
* @param validator - The type guard function to use for validation
* @returns True if all values pass the type guard
*/
export function validateFilterValues<T extends string>(
values: string[],
validator: (value: string) => value is T,
): values is T[] {
return values.every(validator);
}
/**
* Validate and filter an array of values, keeping only valid ones
*
* @param values - The array of values to filter
* @param validator - The type guard function to use for validation
* @returns Array containing only valid values
*/
export function filterValidValues<T extends string>(
values: string[],
validator: (value: string) => value is T,
): T[] {
return values.filter(validator) as T[];
}
/**
* Type guard for FontFilters object
*
* @param filters - The filters object to validate
* @returns True if all filter values are valid
*/
export function isValidFontFilters(filters: {
providers: string[];
categories: string[];
subsets: string[];
}): filters is {
providers: FontProvider[];
categories: FontCategory[];
subsets: FontSubset[];
} {
return (
validateFilterValues(filters.providers, isValidFontProvider)
&& validateFilterValues(filters.categories, isValidFontCategory)
&& validateFilterValues(filters.subsets, isValidFontSubset)
);
}