From 64b97794a66c1beb91833ead57ddf5edfebe251d Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 3 Apr 2026 15:01:36 +0300 Subject: [PATCH] feat: extract FontLoadQueue with retry tracking --- .../fontLoadQueue/FontLoadQueue.test.ts | 65 +++++++++++++++++++ .../fontLoadQueue/FontLoadQueue.ts | 55 ++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/entities/Font/model/store/appliedFontsStore/fontLoadQueue/FontLoadQueue.test.ts create mode 100644 src/entities/Font/model/store/appliedFontsStore/fontLoadQueue/FontLoadQueue.ts diff --git a/src/entities/Font/model/store/appliedFontsStore/fontLoadQueue/FontLoadQueue.test.ts b/src/entities/Font/model/store/appliedFontsStore/fontLoadQueue/FontLoadQueue.test.ts new file mode 100644 index 0000000..f833fcf --- /dev/null +++ b/src/entities/Font/model/store/appliedFontsStore/fontLoadQueue/FontLoadQueue.test.ts @@ -0,0 +1,65 @@ +import type { FontLoadRequestConfig } from '../../../types'; +import { FontLoadQueue } from './FontLoadQueue'; + +const config = (id: string): FontLoadRequestConfig => ({ + id, + name: id, + url: `https://example.com/${id}.woff2`, + weight: 400, +}); + +describe('FontLoadQueue', () => { + let queue: FontLoadQueue; + + beforeEach(() => { + queue = new FontLoadQueue(); + }); + + it('enqueue returns true for a new key', () => { + expect(queue.enqueue('a@400', config('a'))).toBe(true); + }); + + it('enqueue returns false for an already-queued key', () => { + queue.enqueue('a@400', config('a')); + expect(queue.enqueue('a@400', config('a'))).toBe(false); + }); + + it('has returns true after enqueue, false after flush', () => { + queue.enqueue('a@400', config('a')); + expect(queue.has('a@400')).toBe(true); + queue.flush(); + expect(queue.has('a@400')).toBe(false); + }); + + it('flush returns all entries and atomically clears the queue', () => { + queue.enqueue('a@400', config('a')); + queue.enqueue('b@700', config('b')); + const entries = queue.flush(); + expect(entries).toHaveLength(2); + expect(queue.has('a@400')).toBe(false); + expect(queue.has('b@700')).toBe(false); + }); + + it('isMaxRetriesReached returns false below MAX_RETRIES', () => { + queue.incrementRetry('a@400'); + queue.incrementRetry('a@400'); + expect(queue.isMaxRetriesReached('a@400')).toBe(false); + }); + + it('isMaxRetriesReached returns true at MAX_RETRIES (3)', () => { + queue.incrementRetry('a@400'); + queue.incrementRetry('a@400'); + queue.incrementRetry('a@400'); + expect(queue.isMaxRetriesReached('a@400')).toBe(true); + }); + + it('clear resets queue and retry counts', () => { + queue.enqueue('a@400', config('a')); + queue.incrementRetry('a@400'); + queue.incrementRetry('a@400'); + queue.incrementRetry('a@400'); + queue.clear(); + expect(queue.has('a@400')).toBe(false); + expect(queue.isMaxRetriesReached('a@400')).toBe(false); + }); +}); diff --git a/src/entities/Font/model/store/appliedFontsStore/fontLoadQueue/FontLoadQueue.ts b/src/entities/Font/model/store/appliedFontsStore/fontLoadQueue/FontLoadQueue.ts new file mode 100644 index 0000000..1cc6685 --- /dev/null +++ b/src/entities/Font/model/store/appliedFontsStore/fontLoadQueue/FontLoadQueue.ts @@ -0,0 +1,55 @@ +import type { FontLoadRequestConfig } from '../../../types'; + +/** + * Manages the font load queue and per-font retry counts. + * + * Scheduling (when to drain the queue) is handled by the orchestrator — + * this class is purely concerned with what is queued and whether retries are exhausted. + */ +export class FontLoadQueue { + #queue = new Map(); + #retryCounts = new Map(); + + readonly #MAX_RETRIES = 3; + + /** + * Adds a font to the queue. + * @returns `true` if the key was newly enqueued, `false` if it was already present. + */ + enqueue(key: string, config: FontLoadRequestConfig): boolean { + if (this.#queue.has(key)) return false; + this.#queue.set(key, config); + return true; + } + + /** + * Atomically snapshots and clears the queue. + * @returns All queued entries at the time of the call. + */ + flush(): Array<[string, FontLoadRequestConfig]> { + const entries = Array.from(this.#queue.entries()); + this.#queue.clear(); + return entries; + } + + /** Returns `true` if the key is currently in the queue. */ + has(key: string): boolean { + return this.#queue.has(key); + } + + /** Increments the retry count for a font key. */ + incrementRetry(key: string): void { + this.#retryCounts.set(key, (this.#retryCounts.get(key) ?? 0) + 1); + } + + /** Returns `true` if the font has reached or exceeded the maximum retry limit. */ + isMaxRetriesReached(key: string): boolean { + return (this.#retryCounts.get(key) ?? 0) >= this.#MAX_RETRIES; + } + + /** Clears all queued fonts and resets all retry counts. */ + clear(): void { + this.#queue.clear(); + this.#retryCounts.clear(); + } +}