refactor/code-splitting #31

Merged
ilia merged 32 commits from refactor/code-splitting into main 2026-04-08 06:34:20 +00:00
2 changed files with 118 additions and 0 deletions
Showing only changes of commit 128f341399 - Show all commits

View File

@@ -0,0 +1,52 @@
import { FontEvictionPolicy } from './FontEvictionPolicy';
describe('FontEvictionPolicy', () => {
let policy: FontEvictionPolicy;
const TTL = 1000;
const t0 = 100000;
beforeEach(() => {
policy = new FontEvictionPolicy({ ttl: TTL });
});
it('shouldEvict returns false within TTL', () => {
policy.touch('a@400', t0);
expect(policy.shouldEvict('a@400', t0 + TTL - 1)).toBe(false);
});
it('shouldEvict returns true at TTL boundary', () => {
policy.touch('a@400', t0);
expect(policy.shouldEvict('a@400', t0 + TTL)).toBe(true);
});
it('shouldEvict returns false for pinned key regardless of TTL', () => {
policy.touch('a@400', t0);
policy.pin('a@400');
expect(policy.shouldEvict('a@400', t0 + TTL * 10)).toBe(false);
});
it('shouldEvict returns true again after unpin past TTL', () => {
policy.touch('a@400', t0);
policy.pin('a@400');
policy.unpin('a@400');
expect(policy.shouldEvict('a@400', t0 + TTL)).toBe(true);
});
it('shouldEvict returns false for untracked key', () => {
expect(policy.shouldEvict('never@touched', t0 + TTL * 100)).toBe(false);
});
it('keys returns all tracked keys', () => {
policy.touch('a@400', t0);
policy.touch('b@vf', t0);
expect(Array.from(policy.keys())).toEqual(expect.arrayContaining(['a@400', 'b@vf']));
});
it('clear resets all state', () => {
policy.touch('a@400', t0);
policy.pin('a@400');
policy.clear();
expect(Array.from(policy.keys())).toHaveLength(0);
expect(policy.shouldEvict('a@400', t0 + TTL * 10)).toBe(false);
});
});

View File

@@ -0,0 +1,66 @@
interface FontEvictionPolicyOptions {
/** TTL in milliseconds. Defaults to 5 minutes. */
ttl?: number;
}
/**
* Tracks font usage timestamps and pinned keys to determine when a font should be evicted.
*
* Pure data — no browser APIs. Accepts explicit `now` timestamps so tests
* never need fake timers.
*/
export class FontEvictionPolicy {
#usageTracker = new Map<string, number>();
#pinnedFonts = new Set<string>();
readonly #TTL: number;
constructor({ ttl = 5 * 60 * 1000 }: FontEvictionPolicyOptions = {}) {
this.#TTL = ttl;
}
/**
* Records the last-used time for a font key.
* @param key - Font key in `{id}@{weight}` or `{id}@vf` format.
* @param now - Current timestamp in ms. Defaults to `Date.now()`.
*/
touch(key: string, now: number = Date.now()): void {
this.#usageTracker.set(key, now);
}
/** 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. */
unpin(key: string): void {
this.#pinnedFonts.delete(key);
}
/**
* Returns `true` if the font should be evicted.
* A font is evicted when its TTL has elapsed and it is not pinned.
* Returns `false` for untracked keys.
*
* @param key - Font key to check.
* @param now - Current timestamp in ms (pass explicitly for deterministic tests).
*/
shouldEvict(key: string, now: number): boolean {
const lastUsed = this.#usageTracker.get(key);
if (lastUsed === undefined) return false;
if (this.#pinnedFonts.has(key)) return false;
return now - lastUsed >= this.#TTL;
}
/** Returns an iterator over all tracked font keys. */
keys(): IterableIterator<string> {
return this.#usageTracker.keys();
}
/** Clears all usage timestamps and pinned keys. */
clear(): void {
this.#usageTracker.clear();
this.#pinnedFonts.clear();
}
}