refactor(Font): rename fontStore and appliedFontsManager

Both names were vague or overloaded:

- fontStore / FontStore -> fontCatalogStore / FontCatalogStore
  Three font-related stores live in this slice; the new name names the
  paginated catalog specifically.

- appliedFontsManager / AppliedFontsManager -> fontLifecycleManager /
  FontLifecycleManager
  "Applied" collided with the filter-side appliedFilterStore (different
  meaning). The class actually orchestrates a load-use-evict lifecycle
  with FontBufferCache + FontEvictionPolicy + FontLoadQueue
  collaborators, so "Manager" is justified. Companion types file moved
  alongside (appliedFonts.ts -> fontLifecycle.ts).

Directories, file basenames, factory (createFontStore ->
createFontCatalogStore), and the AppliedFontsManagerDeps interface all
renamed. All consumers (ComparisonView, SampleList, FontList,
FontApplicator, FontVirtualList, FilterAndSortFonts bindings,
createFontRowSizeResolver, mocks) updated.
This commit is contained in:
Ilia Mashkov
2026-05-24 20:00:43 +03:00
parent 07d044f4d6
commit 728380498b
38 changed files with 105 additions and 105 deletions
@@ -0,0 +1,69 @@
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('remove deletes key from tracking so it no longer appears in keys()', () => {
policy.touch('a@400', t0);
policy.touch('b@vf', t0);
policy.remove('a@400');
expect(Array.from(policy.keys())).not.toContain('a@400');
expect(Array.from(policy.keys())).toContain('b@vf');
});
it('remove unpins the key so a subsequent touch + TTL would evict it', () => {
policy.touch('a@400', t0);
policy.pin('a@400');
policy.remove('a@400');
// re-touch and check it can be evicted again
policy.touch('a@400', t0);
expect(policy.shouldEvict('a@400', t0 + TTL)).toBe(true);
});
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,88 @@
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();
}
/**
* Removes a font key from tracking. Called by the orchestrator after eviction.
*/
remove(key: string): void {
this.#usageTracker.delete(key);
this.#pinnedFonts.delete(key);
}
/**
* Clears all usage timestamps and pinned keys.
*/
clear(): void {
this.#usageTracker.clear();
this.#pinnedFonts.clear();
}
}