chore: follow the general comments style
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
/** @vitest-environment jsdom */
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { AppliedFontsManager } from './appliedFontsStore.svelte';
|
||||
import { FontFetchError } from './errors';
|
||||
import { FontEvictionPolicy } from './utils/fontEvictionPolicy/FontEvictionPolicy';
|
||||
|
||||
// ── Fake collaborators ────────────────────────────────────────────────────────
|
||||
|
||||
class FakeBufferCache {
|
||||
async get(_url: string): Promise<ArrayBuffer> {
|
||||
return new ArrayBuffer(8);
|
||||
@@ -13,7 +13,9 @@ class FakeBufferCache {
|
||||
clear(): void {}
|
||||
}
|
||||
|
||||
/** Throws {@link FontFetchError} on every `get()` — simulates network/HTTP failure. */
|
||||
/**
|
||||
* Throws {@link FontFetchError} on every `get()` — simulates network/HTTP failure.
|
||||
*/
|
||||
class FailingBufferCache {
|
||||
async get(url: string): Promise<never> {
|
||||
throw new FontFetchError(url, new Error('network error'), 500);
|
||||
@@ -22,8 +24,6 @@ class FailingBufferCache {
|
||||
clear(): void {}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const makeConfig = (id: string, overrides: Partial<{ weight: number; isVariable: boolean }> = {}) => ({
|
||||
id,
|
||||
name: id,
|
||||
@@ -32,8 +32,6 @@ const makeConfig = (id: string, overrides: Partial<{ weight: number; isVariable:
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// ── Suite ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('AppliedFontsManager', () => {
|
||||
let manager: AppliedFontsManager;
|
||||
let eviction: FontEvictionPolicy;
|
||||
@@ -66,8 +64,6 @@ describe('AppliedFontsManager', () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
// ── touch() ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('touch()', () => {
|
||||
it('queues and loads a new font', async () => {
|
||||
manager.touch([makeConfig('roboto')]);
|
||||
@@ -131,8 +127,6 @@ describe('AppliedFontsManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── queue processing ──────────────────────────────────────────────────────
|
||||
|
||||
describe('queue processing', () => {
|
||||
it('filters non-critical weights in data-saver mode', async () => {
|
||||
(navigator as any).connection = { saveData: true };
|
||||
@@ -163,8 +157,6 @@ describe('AppliedFontsManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase 1: fetch ────────────────────────────────────────────────────────
|
||||
|
||||
describe('Phase 1 — fetch', () => {
|
||||
it('sets status to error on fetch failure', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
@@ -209,8 +201,6 @@ describe('AppliedFontsManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Phase 2: parse ────────────────────────────────────────────────────────
|
||||
|
||||
describe('Phase 2 — parse', () => {
|
||||
it('sets status to error on parse failure', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
@@ -241,8 +231,6 @@ describe('AppliedFontsManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── #purgeUnused ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('#purgeUnused', () => {
|
||||
it('evicts fonts after TTL expires', async () => {
|
||||
manager.touch([makeConfig('ephemeral')]);
|
||||
@@ -300,8 +288,6 @@ describe('AppliedFontsManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── destroy() ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe('destroy()', () => {
|
||||
it('clears all statuses', async () => {
|
||||
manager.touch([makeConfig('roboto')]);
|
||||
|
||||
@@ -156,7 +156,9 @@ export class AppliedFontsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if data-saver mode is enabled (defers non-critical weights). */
|
||||
/**
|
||||
* Returns true if data-saver mode is enabled (defers non-critical weights).
|
||||
*/
|
||||
#shouldDeferNonCritical(): boolean {
|
||||
return (navigator as any).connection?.saveData === true;
|
||||
}
|
||||
@@ -188,13 +190,11 @@ export class AppliedFontsManager {
|
||||
const concurrency = getEffectiveConcurrency();
|
||||
const buffers = new Map<string, ArrayBuffer>();
|
||||
|
||||
// ==================== PHASE 1: Concurrent Fetching ====================
|
||||
// Fetch multiple font files in parallel since network I/O is non-blocking
|
||||
for (let i = 0; i < entries.length; i += concurrency) {
|
||||
await this.#fetchChunk(entries.slice(i, i + concurrency), buffers);
|
||||
}
|
||||
|
||||
// ==================== PHASE 2: Sequential Parsing ====================
|
||||
// Parse buffers one at a time with periodic yields to avoid blocking UI
|
||||
const hasInputPending = !!(navigator as any).scheduling?.isInputPending;
|
||||
let lastYield = performance.now();
|
||||
@@ -279,7 +279,9 @@ export class AppliedFontsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes fonts unused within TTL (LRU-style cleanup). Runs every PURGE_INTERVAL. Pinned fonts are never evicted. */
|
||||
/**
|
||||
* Removes fonts unused within TTL (LRU-style cleanup). Runs every PURGE_INTERVAL. Pinned fonts are never evicted.
|
||||
*/
|
||||
#purgeUnused() {
|
||||
const now = Date.now();
|
||||
// Iterate through all tracked font keys
|
||||
@@ -307,7 +309,9 @@ export class AppliedFontsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns current loading status for a font, or undefined if never requested. */
|
||||
/**
|
||||
* Returns current loading status for a font, or undefined if never requested.
|
||||
*/
|
||||
getFontStatus(id: string, weight: number, isVariable = false) {
|
||||
try {
|
||||
return this.statuses.get(generateFontKey({ id, weight, isVariable }));
|
||||
@@ -316,17 +320,23 @@ export class AppliedFontsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Pins a font so it is never evicted by #purgeUnused(), regardless of TTL. */
|
||||
/**
|
||||
* Pins a font so it is never evicted by #purgeUnused(), regardless of TTL.
|
||||
*/
|
||||
pin(id: string, weight: number, isVariable = false): void {
|
||||
this.#eviction.pin(generateFontKey({ id, weight, isVariable }));
|
||||
}
|
||||
|
||||
/** Unpins a font, allowing it to be evicted by #purgeUnused() once its TTL expires. */
|
||||
/**
|
||||
* Unpins a font, allowing it to be evicted by #purgeUnused() once its TTL expires.
|
||||
*/
|
||||
unpin(id: string, weight: number, isVariable = false): void {
|
||||
this.#eviction.unpin(generateFontKey({ id, weight, isVariable }));
|
||||
}
|
||||
|
||||
/** Waits for all fonts to finish loading using document.fonts.ready. */
|
||||
/**
|
||||
* Waits for all fonts to finish loading using document.fonts.ready.
|
||||
*/
|
||||
async ready(): Promise<void> {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
@@ -336,7 +346,9 @@ export class AppliedFontsManager {
|
||||
} catch { /* document unloaded */ }
|
||||
}
|
||||
|
||||
/** Aborts all operations, removes fonts from document, and clears state. Manager cannot be reused after. */
|
||||
/**
|
||||
* Aborts all operations, removes fonts from document, and clears state. Manager cannot be reused after.
|
||||
*/
|
||||
destroy() {
|
||||
// Abort all in-flight network requests
|
||||
this.#abortController.abort();
|
||||
@@ -375,5 +387,7 @@ export class AppliedFontsManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton instance — use throughout the application for unified font loading state. */
|
||||
/**
|
||||
* Singleton instance — use throughout the application for unified font loading state.
|
||||
*/
|
||||
export const appliedFontsManager = new AppliedFontsManager();
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
/** @vitest-environment jsdom */
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { FontFetchError } from '../../errors';
|
||||
import { FontBufferCache } from './FontBufferCache';
|
||||
|
||||
|
||||
+12
-4
@@ -3,9 +3,13 @@ import { FontFetchError } from '../../errors';
|
||||
type Fetcher = (url: string, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
interface FontBufferCacheOptions {
|
||||
/** Custom fetch implementation. Defaults to `globalThis.fetch`. Inject in tests for isolation. */
|
||||
/**
|
||||
* Custom fetch implementation. Defaults to `globalThis.fetch`. Inject in tests for isolation.
|
||||
*/
|
||||
fetcher?: Fetcher;
|
||||
/** Cache API cache name. Defaults to `'font-cache-v1'`. */
|
||||
/**
|
||||
* Cache API cache name. Defaults to `'font-cache-v1'`.
|
||||
*/
|
||||
cacheName?: string;
|
||||
}
|
||||
|
||||
@@ -85,12 +89,16 @@ export class FontBufferCache {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/** Removes a URL from the in-memory cache. Next call to `get()` will re-fetch. */
|
||||
/**
|
||||
* Removes a URL from the in-memory cache. Next call to `get()` will re-fetch.
|
||||
*/
|
||||
evict(url: string): void {
|
||||
this.#buffersByUrl.delete(url);
|
||||
}
|
||||
|
||||
/** Clears all in-memory cached buffers. */
|
||||
/**
|
||||
* Clears all in-memory cached buffers.
|
||||
*/
|
||||
clear(): void {
|
||||
this.#buffersByUrl.clear();
|
||||
}
|
||||
|
||||
+18
-6
@@ -1,5 +1,7 @@
|
||||
interface FontEvictionPolicyOptions {
|
||||
/** TTL in milliseconds. Defaults to 5 minutes. */
|
||||
/**
|
||||
* TTL in milliseconds. Defaults to 5 minutes.
|
||||
*/
|
||||
ttl?: number;
|
||||
}
|
||||
|
||||
@@ -28,12 +30,16 @@ export class FontEvictionPolicy {
|
||||
this.#usageTracker.set(key, now);
|
||||
}
|
||||
|
||||
/** Pins a font key so it is never evicted regardless of TTL. */
|
||||
/**
|
||||
* Pins a font key so it is never evicted regardless of TTL.
|
||||
*/
|
||||
pin(key: string): void {
|
||||
this.#pinnedFonts.add(key);
|
||||
}
|
||||
|
||||
/** Unpins a font key, allowing it to be evicted once its TTL expires. */
|
||||
/**
|
||||
* Unpins a font key, allowing it to be evicted once its TTL expires.
|
||||
*/
|
||||
unpin(key: string): void {
|
||||
this.#pinnedFonts.delete(key);
|
||||
}
|
||||
@@ -57,18 +63,24 @@ export class FontEvictionPolicy {
|
||||
return now - lastUsed >= this.#TTL;
|
||||
}
|
||||
|
||||
/** Returns an iterator over all tracked font keys. */
|
||||
/**
|
||||
* Returns an iterator over all tracked font keys.
|
||||
*/
|
||||
keys(): IterableIterator<string> {
|
||||
return this.#usageTracker.keys();
|
||||
}
|
||||
|
||||
/** Removes a font key from tracking. Called by the orchestrator after eviction. */
|
||||
/**
|
||||
* Removes a font key from tracking. Called by the orchestrator after eviction.
|
||||
*/
|
||||
remove(key: string): void {
|
||||
this.#usageTracker.delete(key);
|
||||
this.#pinnedFonts.delete(key);
|
||||
}
|
||||
|
||||
/** Clears all usage timestamps and pinned keys. */
|
||||
/**
|
||||
* Clears all usage timestamps and pinned keys.
|
||||
*/
|
||||
clear(): void {
|
||||
this.#usageTracker.clear();
|
||||
this.#pinnedFonts.clear();
|
||||
|
||||
+12
-4
@@ -34,22 +34,30 @@ export class FontLoadQueue {
|
||||
return entries;
|
||||
}
|
||||
|
||||
/** Returns `true` if the key is currently in the queue. */
|
||||
/**
|
||||
* Returns `true` if the key is currently in the queue.
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
return this.#queue.has(key);
|
||||
}
|
||||
|
||||
/** Increments the retry count for a font key. */
|
||||
/**
|
||||
* Increments the retry count for a font key.
|
||||
*/
|
||||
incrementRetry(key: string): void {
|
||||
this.#retryCounts.set(key, (this.#retryCounts.get(key) ?? 0) + 1);
|
||||
}
|
||||
|
||||
/** Returns `true` if the font has reached or exceeded the maximum retry limit. */
|
||||
/**
|
||||
* Returns `true` if the font has reached or exceeded the maximum retry limit.
|
||||
*/
|
||||
isMaxRetriesReached(key: string): boolean {
|
||||
return (this.#retryCounts.get(key) ?? 0) >= this.#MAX_RETRIES;
|
||||
}
|
||||
|
||||
/** Clears all queued fonts and resets all retry counts. */
|
||||
/**
|
||||
* Clears all queued fonts and resets all retry counts.
|
||||
*/
|
||||
clear(): void {
|
||||
this.#queue.clear();
|
||||
this.#retryCounts.clear();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @vitest-environment jsdom */
|
||||
/**
|
||||
* @vitest-environment jsdom
|
||||
*/
|
||||
import { FontParseError } from '../../errors';
|
||||
import { loadFont } from './loadFont';
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ describe('FontStore', () => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('construction', () => {
|
||||
it('stores initial params', () => {
|
||||
const store = makeStore({ limit: 20 });
|
||||
@@ -90,7 +89,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('state after fetch', () => {
|
||||
it('exposes loaded fonts', async () => {
|
||||
const store = await fetchedStore({}, generateMockFonts(7));
|
||||
@@ -129,7 +127,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('error states', () => {
|
||||
it('isError is false before any fetch', () => {
|
||||
const store = makeStore();
|
||||
@@ -178,7 +175,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('font accumulation', () => {
|
||||
it('replaces fonts when refetching the first page', async () => {
|
||||
const store = makeStore();
|
||||
@@ -212,7 +208,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('pagination state', () => {
|
||||
it('returns zero-value defaults before any fetch', () => {
|
||||
const store = makeStore();
|
||||
@@ -248,7 +243,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('setParams', () => {
|
||||
it('merges updates into existing params', () => {
|
||||
const store = makeStore({ limit: 10 });
|
||||
@@ -266,7 +260,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('filter change resets', () => {
|
||||
it('clears accumulated fonts when a filter changes', async () => {
|
||||
const store = await fetchedStore({}, generateMockFonts(5));
|
||||
@@ -302,7 +295,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('staleTime in buildOptions', () => {
|
||||
it('is 5 minutes with no active filters', () => {
|
||||
const store = makeStore();
|
||||
@@ -331,7 +323,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('buildQueryKey', () => {
|
||||
it('omits empty-string params', () => {
|
||||
const store = makeStore();
|
||||
@@ -366,7 +357,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('destroy', () => {
|
||||
it('does not throw', () => {
|
||||
const store = makeStore();
|
||||
@@ -380,7 +370,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('refetch', () => {
|
||||
it('triggers a fetch', async () => {
|
||||
const store = makeStore();
|
||||
@@ -400,7 +389,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('nextPage', () => {
|
||||
let store: FontStore;
|
||||
|
||||
@@ -437,7 +425,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('prevPage and goToPage', () => {
|
||||
it('prevPage is a no-op — infinite scroll does not support backward navigation', async () => {
|
||||
const store = await fetchedStore({}, generateMockFonts(5));
|
||||
@@ -454,7 +441,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('prefetch', () => {
|
||||
it('triggers a fetch for the provided params', async () => {
|
||||
const store = makeStore();
|
||||
@@ -465,7 +451,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('getCachedData / setQueryData', () => {
|
||||
it('getCachedData returns undefined before any fetch', () => {
|
||||
queryClient.clear();
|
||||
@@ -497,7 +482,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('invalidate', () => {
|
||||
it('calls invalidateQueries', async () => {
|
||||
const store = await fetchedStore();
|
||||
@@ -508,7 +492,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('setLimit', () => {
|
||||
it('updates the limit param', () => {
|
||||
const store = makeStore({ limit: 10 });
|
||||
@@ -518,7 +501,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('filter shortcut methods', () => {
|
||||
let store: FontStore;
|
||||
|
||||
@@ -561,7 +543,6 @@ describe('FontStore', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
describe('category getters', () => {
|
||||
it('each getter returns only fonts of that category', async () => {
|
||||
const fonts = generateMixedCategoryFonts(2); // 2 of each category = 10 total
|
||||
|
||||
@@ -18,7 +18,9 @@ import type { UnifiedFont } from '../../types';
|
||||
|
||||
type PageParam = { offset: number };
|
||||
|
||||
/** Filter params + limit — offset is managed by TQ as a page param, not a user param. */
|
||||
/**
|
||||
* Filter params + limit — offset is managed by TQ as a page param, not a user param.
|
||||
*/
|
||||
type FontStoreParams = Omit<ProxyFontsParams, 'offset'>;
|
||||
|
||||
type FontStoreResult = InfiniteQueryObserverResult<InfiniteData<ProxyFontsResponse, PageParam>, Error>;
|
||||
@@ -44,34 +46,53 @@ export class FontStore {
|
||||
});
|
||||
}
|
||||
|
||||
// -- Public state --
|
||||
|
||||
/**
|
||||
* Current filter and limit configuration
|
||||
*/
|
||||
get params(): FontStoreParams {
|
||||
return this.#params;
|
||||
}
|
||||
/**
|
||||
* Flattened list of all fonts loaded across all pages (reactive)
|
||||
*/
|
||||
get fonts(): UnifiedFont[] {
|
||||
return this.#result.data?.pages.flatMap((p: ProxyFontsResponse) => p.fonts) ?? [];
|
||||
}
|
||||
/**
|
||||
* True if the first page is currently being fetched
|
||||
*/
|
||||
get isLoading(): boolean {
|
||||
return this.#result.isLoading;
|
||||
}
|
||||
/**
|
||||
* True if any background fetch is in progress (initial or pagination)
|
||||
*/
|
||||
get isFetching(): boolean {
|
||||
return this.#result.isFetching;
|
||||
}
|
||||
/**
|
||||
* True if the last fetch attempt resulted in an error
|
||||
*/
|
||||
get isError(): boolean {
|
||||
return this.#result.isError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last caught error from the query observer
|
||||
*/
|
||||
get error(): Error | null {
|
||||
return this.#result.error ?? null;
|
||||
}
|
||||
// isEmpty is false during loading/fetching so the UI never flashes "no results"
|
||||
// while a fetch is in progress. The !isFetching guard is specifically for the filter-change
|
||||
// transition: fonts clear synchronously → isFetching becomes true → isEmpty stays false.
|
||||
/**
|
||||
* True if no fonts were found for the current filter criteria
|
||||
*/
|
||||
get isEmpty(): boolean {
|
||||
return !this.isLoading && !this.isFetching && this.fonts.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination metadata derived from the last loaded page
|
||||
*/
|
||||
get pagination() {
|
||||
const pages = this.#result.data?.pages;
|
||||
const last = pages?.at(-1);
|
||||
@@ -95,37 +116,52 @@ export class FontStore {
|
||||
};
|
||||
}
|
||||
|
||||
// -- Lifecycle --
|
||||
|
||||
/**
|
||||
* Cleans up subscriptions and destroys the observer
|
||||
*/
|
||||
destroy() {
|
||||
this.#unsubscribe();
|
||||
this.#observer.destroy();
|
||||
}
|
||||
|
||||
// -- Param management --
|
||||
|
||||
/**
|
||||
* Merge new parameters into existing state and trigger a refetch
|
||||
*/
|
||||
setParams(updates: Partial<FontStoreParams>) {
|
||||
this.#params = { ...this.#params, ...updates };
|
||||
this.#observer.setOptions(this.buildOptions());
|
||||
}
|
||||
/**
|
||||
* Forcefully invalidate and refetch the current query from the network
|
||||
*/
|
||||
invalidate() {
|
||||
this.#qc.invalidateQueries({ queryKey: this.buildQueryKey(this.#params) });
|
||||
}
|
||||
|
||||
// -- Async operations --
|
||||
|
||||
/**
|
||||
* Manually trigger a query refetch
|
||||
*/
|
||||
async refetch() {
|
||||
await this.#observer.refetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime the cache with data for a specific parameter set
|
||||
*/
|
||||
async prefetch(params: FontStoreParams) {
|
||||
await this.#qc.prefetchInfiniteQuery(this.buildOptions(params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort any active network requests for this store
|
||||
*/
|
||||
cancel() {
|
||||
this.#qc.cancelQueries({ queryKey: this.buildQueryKey(this.#params) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve current font list from cache without triggering a fetch
|
||||
*/
|
||||
getCachedData(): UnifiedFont[] | undefined {
|
||||
const data = this.#qc.getQueryData<InfiniteData<ProxyFontsResponse, PageParam>>(
|
||||
this.buildQueryKey(this.#params),
|
||||
@@ -134,6 +170,9 @@ export class FontStore {
|
||||
return data.pages.flatMap(p => p.fonts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually update the cached font data (useful for optimistic updates)
|
||||
*/
|
||||
setQueryData(updater: (old: UnifiedFont[] | undefined) => UnifiedFont[]) {
|
||||
const key = this.buildQueryKey(this.#params);
|
||||
this.#qc.setQueryData<InfiniteData<ProxyFontsResponse, PageParam>>(
|
||||
@@ -164,56 +203,90 @@ export class FontStore {
|
||||
);
|
||||
}
|
||||
|
||||
// -- Filter shortcuts --
|
||||
|
||||
/**
|
||||
* Shortcut to update provider filters
|
||||
*/
|
||||
setProviders(v: ProxyFontsParams['providers']) {
|
||||
this.setParams({ providers: v });
|
||||
}
|
||||
/**
|
||||
* Shortcut to update category filters
|
||||
*/
|
||||
setCategories(v: ProxyFontsParams['categories']) {
|
||||
this.setParams({ categories: v });
|
||||
}
|
||||
/**
|
||||
* Shortcut to update subset filters
|
||||
*/
|
||||
setSubsets(v: ProxyFontsParams['subsets']) {
|
||||
this.setParams({ subsets: v });
|
||||
}
|
||||
/**
|
||||
* Shortcut to update search query
|
||||
*/
|
||||
setSearch(v: string) {
|
||||
this.setParams({ q: v || undefined });
|
||||
}
|
||||
/**
|
||||
* Shortcut to update sort order
|
||||
*/
|
||||
setSort(v: ProxyFontsParams['sort']) {
|
||||
this.setParams({ sort: v });
|
||||
}
|
||||
|
||||
// -- Pagination navigation --
|
||||
|
||||
/**
|
||||
* Fetch the next page of results if available
|
||||
*/
|
||||
async nextPage(): Promise<void> {
|
||||
await this.#observer.fetchNextPage();
|
||||
}
|
||||
prevPage(): void {} // no-op: infinite scroll accumulates forward only; method kept for API compatibility
|
||||
goToPage(_page: number): void {} // no-op
|
||||
/**
|
||||
* Backward pagination (no-op: infinite scroll accumulates forward only)
|
||||
*/
|
||||
prevPage(): void {}
|
||||
/**
|
||||
* Jump to specific page (no-op for infinite scroll)
|
||||
*/
|
||||
goToPage(_page: number): void {}
|
||||
|
||||
/**
|
||||
* Update the number of items fetched per page
|
||||
*/
|
||||
setLimit(limit: number) {
|
||||
this.setParams({ limit });
|
||||
}
|
||||
|
||||
// -- Category views --
|
||||
|
||||
/**
|
||||
* Derived list of sans-serif fonts in the current set
|
||||
*/
|
||||
get sansSerifFonts() {
|
||||
return this.fonts.filter(f => f.category === 'sans-serif');
|
||||
}
|
||||
/**
|
||||
* Derived list of serif fonts in the current set
|
||||
*/
|
||||
get serifFonts() {
|
||||
return this.fonts.filter(f => f.category === 'serif');
|
||||
}
|
||||
/**
|
||||
* Derived list of display fonts in the current set
|
||||
*/
|
||||
get displayFonts() {
|
||||
return this.fonts.filter(f => f.category === 'display');
|
||||
}
|
||||
/**
|
||||
* Derived list of handwriting fonts in the current set
|
||||
*/
|
||||
get handwritingFonts() {
|
||||
return this.fonts.filter(f => f.category === 'handwriting');
|
||||
}
|
||||
/**
|
||||
* Derived list of monospace fonts in the current set
|
||||
*/
|
||||
get monospaceFonts() {
|
||||
return this.fonts.filter(f => f.category === 'monospace');
|
||||
}
|
||||
|
||||
// -- Private helpers (TypeScript-private so tests can spy via `as any`) --
|
||||
|
||||
private buildQueryKey(params: FontStoreParams): readonly unknown[] {
|
||||
const filtered: Record<string, any> = {};
|
||||
|
||||
|
||||
@@ -31,18 +31,28 @@ export type FontSubset = 'latin' | 'latin-ext' | 'cyrillic' | 'greek' | 'arabic'
|
||||
* Combined filter state for font queries
|
||||
*/
|
||||
export interface FontFilters {
|
||||
/** Selected font providers */
|
||||
/**
|
||||
* Active font providers to fetch from
|
||||
*/
|
||||
providers: FontProvider[];
|
||||
/** Selected font categories */
|
||||
/**
|
||||
* Visual classifications (sans, serif, etc.)
|
||||
*/
|
||||
categories: FontCategory[];
|
||||
/** Selected character subsets */
|
||||
/**
|
||||
* Character sets required for the sample text
|
||||
*/
|
||||
subsets: FontSubset[];
|
||||
}
|
||||
|
||||
/** Filter group identifier */
|
||||
/**
|
||||
* Filter group identifier
|
||||
*/
|
||||
export type FilterGroup = 'providers' | 'categories' | 'subsets';
|
||||
|
||||
/** Filter type including search query */
|
||||
/**
|
||||
* Filter type including search query
|
||||
*/
|
||||
export type FilterType = FilterGroup | 'searchQuery';
|
||||
|
||||
/**
|
||||
@@ -80,15 +90,25 @@ export type UnifiedFontVariant = FontVariant;
|
||||
* Font style URLs
|
||||
*/
|
||||
export interface FontStyleUrls {
|
||||
/** Regular weight URL */
|
||||
/**
|
||||
* URL for the regular (400) weight
|
||||
*/
|
||||
regular?: string;
|
||||
/** Italic URL */
|
||||
/**
|
||||
* URL for the italic (400) style
|
||||
*/
|
||||
italic?: string;
|
||||
/** Bold weight URL */
|
||||
/**
|
||||
* URL for the bold (700) weight
|
||||
*/
|
||||
bold?: string;
|
||||
/** Bold italic URL */
|
||||
/**
|
||||
* URL for the bold-italic (700) style
|
||||
*/
|
||||
boldItalic?: string;
|
||||
/** Additional variant mapping */
|
||||
/**
|
||||
* Mapping for all other numeric/custom variants
|
||||
*/
|
||||
variants?: Partial<Record<UnifiedFontVariant, string>>;
|
||||
}
|
||||
|
||||
@@ -96,19 +116,24 @@ export interface FontStyleUrls {
|
||||
* Font metadata
|
||||
*/
|
||||
export interface FontMetadata {
|
||||
/** Timestamp when font was cached */
|
||||
/**
|
||||
* Epoch timestamp of last successful fetch
|
||||
*/
|
||||
cachedAt: number;
|
||||
/** Font version from provider */
|
||||
/**
|
||||
* Semantic version string from upstream
|
||||
*/
|
||||
version?: string;
|
||||
/** Last modified date from provider */
|
||||
/**
|
||||
* ISO date string of last remote update
|
||||
*/
|
||||
lastModified?: string;
|
||||
/** Popularity rank (if available from provider) */
|
||||
/**
|
||||
* Raw ranking integer from provider
|
||||
*/
|
||||
popularity?: number;
|
||||
/**
|
||||
* Normalized popularity score (0-100)
|
||||
*
|
||||
* Normalized across all fonts for consistent ranking
|
||||
* Higher values indicate more popular fonts
|
||||
* Normalized score (0-100) used for global sorting
|
||||
*/
|
||||
popularityScore?: number;
|
||||
}
|
||||
@@ -117,17 +142,38 @@ export interface FontMetadata {
|
||||
* Font features (variable fonts, axes, tags)
|
||||
*/
|
||||
export interface FontFeatures {
|
||||
/** Whether this is a variable font */
|
||||
/**
|
||||
* Whether the font supports fluid weight/width axes
|
||||
*/
|
||||
isVariable?: boolean;
|
||||
/** Variable font axes (for Fontshare) */
|
||||
/**
|
||||
* Definable axes for variable font interpolation
|
||||
*/
|
||||
axes?: Array<{
|
||||
/**
|
||||
* Human-readable axis name (e.g., 'Weight')
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* CSS property name (e.g., 'wght')
|
||||
*/
|
||||
property: string;
|
||||
/**
|
||||
* Default numeric value for the axis
|
||||
*/
|
||||
default: number;
|
||||
/**
|
||||
* Minimum inclusive bound
|
||||
*/
|
||||
min: number;
|
||||
/**
|
||||
* Maximum inclusive bound
|
||||
*/
|
||||
max: number;
|
||||
}>;
|
||||
/** Usage tags (for Fontshare) */
|
||||
/**
|
||||
* Descriptive keywords for search indexing
|
||||
*/
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
@@ -138,29 +184,44 @@ export interface FontFeatures {
|
||||
* for consistent font handling across the application.
|
||||
*/
|
||||
export interface UnifiedFont {
|
||||
/** Unique identifier (Google: family name, Fontshare: slug) */
|
||||
/**
|
||||
* Unique ID (family name for Google, slug for Fontshare)
|
||||
*/
|
||||
id: string;
|
||||
/** Font display name */
|
||||
/**
|
||||
* Canonical family name for CSS font-family
|
||||
*/
|
||||
name: string;
|
||||
/** Font provider (google | fontshare) */
|
||||
/**
|
||||
* Upstream data source
|
||||
*/
|
||||
provider: FontProvider;
|
||||
/**
|
||||
* Provider badge display name
|
||||
*
|
||||
* Human-readable provider name for UI display
|
||||
* e.g., "Google Fonts" or "Fontshare"
|
||||
* Display label for provider badges
|
||||
*/
|
||||
providerBadge?: string;
|
||||
/** Font category classification */
|
||||
/**
|
||||
* Primary typographic category
|
||||
*/
|
||||
category: FontCategory;
|
||||
/** Supported character subsets */
|
||||
/**
|
||||
* All supported character sets
|
||||
*/
|
||||
subsets: FontSubset[];
|
||||
/** Available font variants (weights, styles) */
|
||||
/**
|
||||
* List of available weights and styles
|
||||
*/
|
||||
variants: UnifiedFontVariant[];
|
||||
/** URL mapping for font file downloads */
|
||||
/**
|
||||
* Remote assets for font loading
|
||||
*/
|
||||
styles: FontStyleUrls;
|
||||
/** Additional metadata */
|
||||
/**
|
||||
* Technical metadata and rankings
|
||||
*/
|
||||
metadata: FontMetadata;
|
||||
/** Advanced font features */
|
||||
/**
|
||||
* Variable font details and tags
|
||||
*/
|
||||
features: FontFeatures;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* SINGLE EXPORT POINT
|
||||
* ============================================================================
|
||||
*
|
||||
* This is the single export point for all Font types.
|
||||
* All imports should use: `import { X } from '$entities/Font/model/types'`
|
||||
*/
|
||||
|
||||
// Font domain and model types
|
||||
export type {
|
||||
FilterGroup,
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* ============================================================================
|
||||
* STORE TYPES
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
import type {
|
||||
FontCategory,
|
||||
FontProvider,
|
||||
@@ -12,37 +6,55 @@ import type {
|
||||
} from './font';
|
||||
|
||||
/**
|
||||
* Font collection state
|
||||
* Global state for the local font collection
|
||||
*/
|
||||
export interface FontCollectionState {
|
||||
/** All cached fonts */
|
||||
/**
|
||||
* Map of cached fonts indexed by their unique family ID
|
||||
*/
|
||||
fonts: Record<string, UnifiedFont>;
|
||||
/** Active filters */
|
||||
/**
|
||||
* Set of active user-defined filters
|
||||
*/
|
||||
filters: FontCollectionFilters;
|
||||
/** Sort configuration */
|
||||
/**
|
||||
* Current sorting parameters for the display list
|
||||
*/
|
||||
sort: FontCollectionSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Font collection filters
|
||||
* Filter configuration for narrow collections
|
||||
*/
|
||||
export interface FontCollectionFilters {
|
||||
/** Search query */
|
||||
/**
|
||||
* Partial family name to match against
|
||||
*/
|
||||
searchQuery: string;
|
||||
/** Filter by providers */
|
||||
/**
|
||||
* Data sources (Google, Fontshare) to include
|
||||
*/
|
||||
providers?: FontProvider[];
|
||||
/** Filter by categories */
|
||||
/**
|
||||
* Typographic categories (Serif, Sans, etc.) to include
|
||||
*/
|
||||
categories?: FontCategory[];
|
||||
/** Filter by subsets */
|
||||
/**
|
||||
* Character sets (Latin, Cyrillic, etc.) to include
|
||||
*/
|
||||
subsets?: FontSubset[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Font collection sort configuration
|
||||
* Ordering configuration for the font list
|
||||
*/
|
||||
export interface FontCollectionSort {
|
||||
/** Sort field */
|
||||
/**
|
||||
* The font property to order by
|
||||
*/
|
||||
field: 'name' | 'popularity' | 'category';
|
||||
/** Sort direction */
|
||||
/**
|
||||
* The sort order (Ascending or Descending)
|
||||
*/
|
||||
direction: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user