Chore/architecture refactoring #42
@@ -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();
|
||||||
|
|||||||
+7
-2
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user