From a711e4e12a72c0c7bb2cdfc640fdc68d2bd8c330 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 3 Apr 2026 12:50:50 +0300 Subject: [PATCH] chore(appliedFontsStore): move generateFontKey into separate function and cover it with tests --- .../appliedFontsStore.svelte.ts | 83 +++++++++++-------- .../generateFontKey/generateFontKey.test.ts | 25 ++++++ .../utils/generateFontKey/generateFontKey.ts | 22 +++++ .../store/appliedFontsStore/utils/index.ts | 1 + 4 files changed, 98 insertions(+), 33 deletions(-) create mode 100644 src/entities/Font/model/store/appliedFontsStore/utils/generateFontKey/generateFontKey.test.ts create mode 100644 src/entities/Font/model/store/appliedFontsStore/utils/generateFontKey/generateFontKey.ts diff --git a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts index 62cda8d..98dc0f8 100644 --- a/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts +++ b/src/entities/Font/model/store/appliedFontsStore/appliedFontsStore.svelte.ts @@ -4,6 +4,7 @@ import { type FontLoadStatus, } from '../../types'; import { + generateFontKey, getEffectiveConcurrency, loadFont, yieldToMainThread, @@ -81,11 +82,6 @@ export class AppliedFontsManager { } } - // Generates font key: `{id}@vf` for variable, `{id}@{weight}` for static. - #getFontKey(id: string, weight: number, isVariable: boolean): string { - return isVariable ? `${id.toLowerCase()}@vf` : `${id.toLowerCase()}@${weight}`; - } - /** * Requests fonts to be loaded. Updates usage tracking and queues new fonts. * @@ -93,34 +89,40 @@ export class AppliedFontsManager { * Scheduling: Prefers requestIdleCallback (150ms timeout), falls back to setTimeout(16ms). */ touch(configs: FontLoadRequestConfig[]) { - if (this.#abortController.signal.aborted) return; - - const now = Date.now(); - let hasNewItems = false; - - for (const config of configs) { - const key = this.#getFontKey(config.id, config.weight, !!config.isVariable); - this.#usageTracker.set(key, now); - - const status = this.statuses.get(key); - if (status === 'loaded' || status === 'loading' || this.#queue.has(key)) continue; - if (status === 'error' && (this.#retryCounts.get(key) ?? 0) >= this.#MAX_RETRIES) continue; - - this.#queue.set(key, config); - hasNewItems = true; + if (this.#abortController.signal.aborted) { + return; } - if (hasNewItems && !this.#timeoutId) { - if (typeof requestIdleCallback !== 'undefined') { - this.#timeoutId = requestIdleCallback( - () => this.#processQueue(), - { timeout: 150 }, - ) as unknown as ReturnType; - this.#pendingType = 'idle'; - } else { - this.#timeoutId = setTimeout(() => this.#processQueue(), 16); - this.#pendingType = 'timeout'; + try { + const now = Date.now(); + let hasNewItems = false; + + for (const config of configs) { + const key = generateFontKey(config); + this.#usageTracker.set(key, now); + + const status = this.statuses.get(key); + if (status === 'loaded' || status === 'loading' || this.#queue.has(key)) continue; + if (status === 'error' && (this.#retryCounts.get(key) ?? 0) >= this.#MAX_RETRIES) continue; + + this.#queue.set(key, config); + hasNewItems = true; } + + if (hasNewItems && !this.#timeoutId) { + if (typeof requestIdleCallback !== 'undefined') { + this.#timeoutId = requestIdleCallback( + () => this.#processQueue(), + { timeout: 150 }, + ) as unknown as ReturnType; + this.#pendingType = 'idle'; + } else { + this.#timeoutId = setTimeout(() => this.#processQueue(), 16); + this.#pendingType = 'timeout'; + } + } + } catch (error) { + console.error(error); } } @@ -274,17 +276,32 @@ export class AppliedFontsManager { /** Returns current loading status for a font, or undefined if never requested. */ getFontStatus(id: string, weight: number, isVariable = false) { - return this.statuses.get(this.#getFontKey(id, weight, isVariable)); + try { + const key = generateFontKey({ id, weight, isVariable }); + return this.statuses.get(key); + } catch (error) { + console.error(error); + } } /** Pins a font so it is never evicted by #purgeUnused(), regardless of TTL. */ pin(id: string, weight: number, isVariable?: boolean): void { - this.#pinnedFonts.add(this.#getFontKey(id, weight, !!isVariable)); + try { + const key = generateFontKey({ id, weight, isVariable: !!isVariable }); + this.#pinnedFonts.add(key); + } catch (error) { + console.error(error); + } } /** Unpins a font, allowing it to be evicted by #purgeUnused() once its TTL expires. */ unpin(id: string, weight: number, isVariable?: boolean): void { - this.#pinnedFonts.delete(this.#getFontKey(id, weight, !!isVariable)); + try { + const key = generateFontKey({ id, weight, isVariable: !!isVariable }); + this.#pinnedFonts.delete(key); + } catch (error) { + console.error(error); + } } /** Waits for all fonts to finish loading using document.fonts.ready. */ diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/generateFontKey/generateFontKey.test.ts b/src/entities/Font/model/store/appliedFontsStore/utils/generateFontKey/generateFontKey.test.ts new file mode 100644 index 0000000..3937207 --- /dev/null +++ b/src/entities/Font/model/store/appliedFontsStore/utils/generateFontKey/generateFontKey.test.ts @@ -0,0 +1,25 @@ +import { generateFontKey } from './generateFontKey'; + +describe('generateFontKey', () => { + it('should throw an error if font id is not provided', () => { + const config = { weight: 400, isVariable: false }; + // @ts-expect-error + expect(() => generateFontKey(config)).toThrow('Font id is required'); + }); + + it('should generate a font key for a variable font', () => { + const config = { id: 'Roboto', weight: 400, isVariable: true }; + expect(generateFontKey(config)).toBe('roboto@vf'); + }); + + it('should throw an error if font weight is not provided and is not a variable font', () => { + const config = { id: 'Roboto', isVariable: false }; + // @ts-expect-error + expect(() => generateFontKey(config)).toThrow('Font weight is required'); + }); + + it('should generate a font key for a non-variable font', () => { + const config = { id: 'Roboto', weight: 400, isVariable: false }; + expect(generateFontKey(config)).toBe('roboto@400'); + }); +}); diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/generateFontKey/generateFontKey.ts b/src/entities/Font/model/store/appliedFontsStore/utils/generateFontKey/generateFontKey.ts new file mode 100644 index 0000000..1680180 --- /dev/null +++ b/src/entities/Font/model/store/appliedFontsStore/utils/generateFontKey/generateFontKey.ts @@ -0,0 +1,22 @@ +import type { FontLoadRequestConfig } from '../../../../types'; + +export type PartialConfig = Pick; + +/** + * Generates a font key for a given font load request configuration. + * @param config - The font load request configuration. + * @returns The generated font key. + */ +export function generateFontKey(config: PartialConfig): string { + if (!config.id) { + throw new Error('Font id is required'); + } + if (config.isVariable) { + return `${config.id.toLowerCase()}@vf`; + } + + if (!config.weight) { + throw new Error('Font weight is required'); + } + return `${config.id.toLowerCase()}@${config.weight}`; +} diff --git a/src/entities/Font/model/store/appliedFontsStore/utils/index.ts b/src/entities/Font/model/store/appliedFontsStore/utils/index.ts index bf35e55..dcb4365 100644 --- a/src/entities/Font/model/store/appliedFontsStore/utils/index.ts +++ b/src/entities/Font/model/store/appliedFontsStore/utils/index.ts @@ -1,3 +1,4 @@ +export { generateFontKey } from './generateFontKey/generateFontKey'; export { getEffectiveConcurrency } from './getEffectiveConcurrency/getEffectiveConcurrency'; export { loadFont } from './loadFont/loadFont'; export { yieldToMainThread } from './yieldToMainThread/yieldToMainThread';