feat: extract FontEvictionPolicy with TTL and pin/unpin
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user