refactor: extract magic constants — wave 3 (font lifecycle)

Promote font-loading scheduling and lifecycle tunables to named
module-level constants:

- comparisonStore: FONT_READY_FALLBACK_MS (1000ms) — UI unblock safety net
- fontLifecycleManager:
  - PURGE_INTERVAL_MS (60000) — periodic eviction sweep
  - IDLE_CALLBACK_TIMEOUT_MS (150) — requestIdleCallback timeout
  - SCHEDULE_FALLBACK_MS (16) — setTimeout fallback (~60fps)
  - YIELD_INTERVAL_MS (8) — parse-loop yield budget for non-Chromium
  - CRITICAL_FONT_WEIGHTS ([400, 700]) — data-saver allowlist
- FontEvictionPolicy: DEFAULT_FONT_TTL_MS (5 minutes)
- FontLoadQueue: FONT_LOAD_MAX_RETRIES (3)

No behavior changes — values preserved exactly. Class-private fields
that mirrored these constants are removed in favor of module scope.
This commit is contained in:
Ilia Mashkov
2026-05-24 21:13:38 +03:00
parent ccef3cf7bb
commit 2bb43797f0
4 changed files with 56 additions and 14 deletions
@@ -17,6 +17,35 @@ import { FontBufferCache } from './utils/fontBufferCache/FontBufferCache';
import { FontEvictionPolicy } from './utils/fontEvictionPolicy/FontEvictionPolicy'; import { FontEvictionPolicy } from './utils/fontEvictionPolicy/FontEvictionPolicy';
import { FontLoadQueue } from './utils/fontLoadQueue/FontLoadQueue'; 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 { interface FontLifecycleManagerDeps {
cache?: FontBufferCache; cache?: FontBufferCache;
eviction?: FontEvictionPolicy; eviction?: FontEvictionPolicy;
@@ -70,8 +99,6 @@ export class FontLifecycleManager {
// Tracks which callback type is pending ('idle' | 'timeout' | null) for proper cancellation // Tracks which callback type is pending ('idle' | 'timeout' | null) for proper cancellation
#pendingType: 'idle' | 'timeout' | null = null; #pendingType: 'idle' | 'timeout' | null = null;
readonly #PURGE_INTERVAL = 60000;
// Reactive status map for Svelte components to track font states // Reactive status map for Svelte components to track font states
statuses = new SvelteMap<string, FontLoadStatus>(); statuses = new SvelteMap<string, FontLoadStatus>();
@@ -85,7 +112,7 @@ export class FontLifecycleManager {
this.#eviction = eviction; this.#eviction = eviction;
this.#queue = queue; this.#queue = queue;
if (typeof window !== 'undefined') { 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') { if (typeof requestIdleCallback !== 'undefined') {
this.#timeoutId = requestIdleCallback( this.#timeoutId = requestIdleCallback(
() => this.#processQueue(), () => this.#processQueue(),
{ timeout: 150 }, { timeout: IDLE_CALLBACK_TIMEOUT_MS },
) as unknown as ReturnType<typeof setTimeout>; ) as unknown as ReturnType<typeof setTimeout>;
this.#pendingType = 'idle'; this.#pendingType = 'idle';
} else { } else {
this.#timeoutId = setTimeout(() => this.#processQueue(), 16); this.#timeoutId = setTimeout(() => this.#processQueue(), SCHEDULE_FALLBACK_MS);
this.#pendingType = 'timeout'; this.#pendingType = 'timeout';
} }
} }
@@ -183,7 +210,7 @@ export class FontLifecycleManager {
// In data-saver mode, only load variable fonts and common weights (400, 700) // In data-saver mode, only load variable fonts and common weights (400, 700)
if (this.#shouldDeferNonCritical()) { 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) // 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 // Parse buffers one at a time with periodic yields to avoid blocking UI
const hasInputPending = !!(navigator as any).scheduling?.isInputPending; const hasInputPending = !!(navigator as any).scheduling?.isInputPending;
let lastYield = performance.now(); let lastYield = performance.now();
const YIELD_INTERVAL = 8;
for (const [key, config] of entries) { for (const [key, config] of entries) {
const buffer = buffers.get(key); const buffer = buffers.get(key);
@@ -214,7 +240,7 @@ export class FontLifecycleManager {
// Others: yield every 8ms as fallback // Others: yield every 8ms as fallback
const shouldYield = hasInputPending const shouldYield = hasInputPending
? (navigator as any).scheduling.isInputPending({ includeContinuous: true }) ? (navigator as any).scheduling.isInputPending({ includeContinuous: true })
: performance.now() - lastYield > YIELD_INTERVAL; : performance.now() - lastYield > YIELD_INTERVAL_MS;
if (shouldYield) { if (shouldYield) {
await yieldToMainThread(); await yieldToMainThread();
@@ -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 { interface FontEvictionPolicyOptions {
/** /**
* TTL in milliseconds. Defaults to 5 minutes. * TTL in milliseconds. Defaults to {@link DEFAULT_FONT_TTL_MS}.
*/ */
ttl?: number; ttl?: number;
} }
@@ -17,7 +22,7 @@ export class FontEvictionPolicy {
readonly #TTL: number; readonly #TTL: number;
constructor({ ttl = 5 * 60 * 1000 }: FontEvictionPolicyOptions = {}) { constructor({ ttl = DEFAULT_FONT_TTL_MS }: FontEvictionPolicyOptions = {}) {
this.#TTL = ttl; this.#TTL = ttl;
} }
@@ -1,5 +1,11 @@
import type { FontLoadRequestConfig } from '../../../../types'; 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. * Manages the font load queue and per-font retry counts.
* *
@@ -10,8 +16,6 @@ export class FontLoadQueue {
#queue = new Map<string, FontLoadRequestConfig>(); #queue = new Map<string, FontLoadRequestConfig>();
#retryCounts = new Map<string, number>(); #retryCounts = new Map<string, number>();
readonly #MAX_RETRIES = 3;
/** /**
* Adds a font to the queue. * Adds a font to the queue.
* @returns `true` if the key was newly enqueued, `false` if it was already present. * @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. * Returns `true` if the font has reached or exceeded the maximum retry limit.
*/ */
isMaxRetriesReached(key: string): boolean { isMaxRetriesReached(key: string): boolean {
return (this.#retryCounts.get(key) ?? 0) >= this.#MAX_RETRIES; return (this.#retryCounts.get(key) ?? 0) >= FONT_LOAD_MAX_RETRIES;
} }
/** /**
@@ -44,6 +44,13 @@ export type Side = 'A' | 'B';
const STORAGE_KEY = 'glyphdiff:comparison'; 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 // Persistent storage for selected comparison fonts
const storage = createPersistentStore<ComparisonState>(STORAGE_KEY, { const storage = createPersistentStore<ComparisonState>(STORAGE_KEY, {
fontAId: null, fontAId: null,
@@ -236,7 +243,7 @@ export class ComparisonStore {
this.#fontsReady = true; this.#fontsReady = true;
} catch (error) { } catch (error) {
console.warn('[ComparisonStore] Font loading failed:', error); console.warn('[ComparisonStore] Font loading failed:', error);
setTimeout(() => (this.#fontsReady = true), 1000); setTimeout(() => (this.#fontsReady = true), FONT_READY_FALLBACK_MS);
} }
} }