diff --git a/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts b/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts index 1a4755e..9439a87 100644 --- a/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts +++ b/src/entities/Font/model/store/fontLifecycleManager/fontLifecycleManager.svelte.ts @@ -17,6 +17,35 @@ import { FontBufferCache } from './utils/fontBufferCache/FontBufferCache'; import { FontEvictionPolicy } from './utils/fontEvictionPolicy/FontEvictionPolicy'; import { FontLoadQueue } from './utils/fontLoadQueue/FontLoadQueue'; +/** + * How often the periodic eviction sweep runs. + */ +const PURGE_INTERVAL_MS = 60000; + +/** + * Timeout for `requestIdleCallback`. After this elapses, the callback is + * forced to run regardless of whether the browser is idle. + */ +const IDLE_CALLBACK_TIMEOUT_MS = 150; + +/** + * setTimeout fallback delay when `requestIdleCallback` is unavailable. + * ~16ms ≈ one frame at 60fps. + */ +const SCHEDULE_FALLBACK_MS = 16; + +/** + * How often the parse loop yields back to the main thread when the browser + * does not provide `isInputPending` (non-Chromium fallback). + */ +const YIELD_INTERVAL_MS = 8; + +/** + * Font weights treated as "critical" in data-saver mode. Other weights are + * skipped to reduce network usage; variable fonts bypass this filter. + */ +const CRITICAL_FONT_WEIGHTS = [400, 700]; + interface FontLifecycleManagerDeps { cache?: FontBufferCache; eviction?: FontEvictionPolicy; @@ -70,8 +99,6 @@ export class FontLifecycleManager { // Tracks which callback type is pending ('idle' | 'timeout' | null) for proper cancellation #pendingType: 'idle' | 'timeout' | null = null; - readonly #PURGE_INTERVAL = 60000; - // Reactive status map for Svelte components to track font states statuses = new SvelteMap(); @@ -85,7 +112,7 @@ export class FontLifecycleManager { this.#eviction = eviction; this.#queue = queue; if (typeof window !== 'undefined') { - this.#intervalId = setInterval(() => this.#purgeUnused(), this.#PURGE_INTERVAL); + this.#intervalId = setInterval(() => this.#purgeUnused(), PURGE_INTERVAL_MS); } } @@ -147,11 +174,11 @@ export class FontLifecycleManager { if (typeof requestIdleCallback !== 'undefined') { this.#timeoutId = requestIdleCallback( () => this.#processQueue(), - { timeout: 150 }, + { timeout: IDLE_CALLBACK_TIMEOUT_MS }, ) as unknown as ReturnType; this.#pendingType = 'idle'; } else { - this.#timeoutId = setTimeout(() => this.#processQueue(), 16); + this.#timeoutId = setTimeout(() => this.#processQueue(), SCHEDULE_FALLBACK_MS); this.#pendingType = 'timeout'; } } @@ -183,7 +210,7 @@ export class FontLifecycleManager { // In data-saver mode, only load variable fonts and common weights (400, 700) if (this.#shouldDeferNonCritical()) { - entries = entries.filter(([, c]) => c.isVariable || [400, 700].includes(c.weight)); + entries = entries.filter(([, c]) => c.isVariable || CRITICAL_FONT_WEIGHTS.includes(c.weight)); } // Determine optimal concurrent fetches based on network speed (1-4) @@ -198,7 +225,6 @@ export class FontLifecycleManager { // Parse buffers one at a time with periodic yields to avoid blocking UI const hasInputPending = !!(navigator as any).scheduling?.isInputPending; let lastYield = performance.now(); - const YIELD_INTERVAL = 8; for (const [key, config] of entries) { const buffer = buffers.get(key); @@ -214,7 +240,7 @@ export class FontLifecycleManager { // Others: yield every 8ms as fallback const shouldYield = hasInputPending ? (navigator as any).scheduling.isInputPending({ includeContinuous: true }) - : performance.now() - lastYield > YIELD_INTERVAL; + : performance.now() - lastYield > YIELD_INTERVAL_MS; if (shouldYield) { await yieldToMainThread(); diff --git a/src/entities/Font/model/store/fontLifecycleManager/utils/fontEvictionPolicy/FontEvictionPolicy.ts b/src/entities/Font/model/store/fontLifecycleManager/utils/fontEvictionPolicy/FontEvictionPolicy.ts index 2a64cc6..d49d296 100644 --- a/src/entities/Font/model/store/fontLifecycleManager/utils/fontEvictionPolicy/FontEvictionPolicy.ts +++ b/src/entities/Font/model/store/fontLifecycleManager/utils/fontEvictionPolicy/FontEvictionPolicy.ts @@ -1,6 +1,11 @@ +/** + * Default TTL after which an unpinned font is eligible for eviction. + */ +export const DEFAULT_FONT_TTL_MS = 5 * 60 * 1000; + interface FontEvictionPolicyOptions { /** - * TTL in milliseconds. Defaults to 5 minutes. + * TTL in milliseconds. Defaults to {@link DEFAULT_FONT_TTL_MS}. */ ttl?: number; } @@ -17,7 +22,7 @@ export class FontEvictionPolicy { readonly #TTL: number; - constructor({ ttl = 5 * 60 * 1000 }: FontEvictionPolicyOptions = {}) { + constructor({ ttl = DEFAULT_FONT_TTL_MS }: FontEvictionPolicyOptions = {}) { this.#TTL = ttl; } diff --git a/src/entities/Font/model/store/fontLifecycleManager/utils/fontLoadQueue/FontLoadQueue.ts b/src/entities/Font/model/store/fontLifecycleManager/utils/fontLoadQueue/FontLoadQueue.ts index e921eb9..576b7f8 100644 --- a/src/entities/Font/model/store/fontLifecycleManager/utils/fontLoadQueue/FontLoadQueue.ts +++ b/src/entities/Font/model/store/fontLifecycleManager/utils/fontLoadQueue/FontLoadQueue.ts @@ -1,5 +1,11 @@ import type { FontLoadRequestConfig } from '../../../../types'; +/** + * Maximum number of times a single font key will be retried before it is + * considered permanently failed. + */ +export const FONT_LOAD_MAX_RETRIES = 3; + /** * Manages the font load queue and per-font retry counts. * @@ -10,8 +16,6 @@ export class FontLoadQueue { #queue = new Map(); #retryCounts = new Map(); - readonly #MAX_RETRIES = 3; - /** * Adds a font to the queue. * @returns `true` if the key was newly enqueued, `false` if it was already present. @@ -52,7 +56,7 @@ export class FontLoadQueue { * 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; + return (this.#retryCounts.get(key) ?? 0) >= FONT_LOAD_MAX_RETRIES; } /** diff --git a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts index 0d8a73a..6136a63 100644 --- a/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts +++ b/src/widgets/ComparisonView/model/stores/comparisonStore.svelte.ts @@ -44,6 +44,13 @@ export type Side = 'A' | 'B'; const STORAGE_KEY = 'glyphdiff:comparison'; +/** + * Max time the UI waits after a font-load failure before unblocking + * (#fontsReady = true). Acts as a safety net so a transient load error + * can't strand the comparison view in a permanent loading state. + */ +const FONT_READY_FALLBACK_MS = 1000; + // Persistent storage for selected comparison fonts const storage = createPersistentStore(STORAGE_KEY, { fontAId: null, @@ -236,7 +243,7 @@ export class ComparisonStore { this.#fontsReady = true; } catch (error) { console.warn('[ComparisonStore] Font loading failed:', error); - setTimeout(() => (this.#fontsReady = true), 1000); + setTimeout(() => (this.#fontsReady = true), FONT_READY_FALLBACK_MS); } }