export type ArchivedFontStatus = "idle" | "loading" | "ready" | "unavailable"; interface LoadableFontFace { load: () => Promise; } interface FontSetPort { add: (face: unknown) => unknown; check: (font: string) => boolean; ready: Promise; } export interface ArchivedFontReference { fontId: string; url: string; } export function fontFamilyName(fontId: string) { if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,119}$/.test(fontId)) throw new Error("font_id_invalid"); return `Dada_${fontId.replaceAll(/[^A-Za-z0-9_]/g, "_")}`; } export class ArchivedFontLoader { private readonly createFace: (family: string, source: string) => LoadableFontFace; private readonly fontSet: FontSetPort; private readonly pending = new Map>(); private readonly statuses = new Map(); constructor(input: { createFace: (family: string, source: string) => LoadableFontFace; fontSet: FontSetPort }) { this.createFace = input.createFace; this.fontSet = input.fontSet; } status(fontId: string): ArchivedFontStatus { return this.statuses.get(fontId) ?? "idle"; } ensure(reference: ArchivedFontReference): Promise { const existing = this.pending.get(reference.fontId); if (existing) return existing; const operation = this.load(reference); this.pending.set(reference.fontId, operation); return operation; } retry(reference: ArchivedFontReference) { this.pending.delete(reference.fontId); this.statuses.delete(reference.fontId); return this.ensure(reference); } private async load(reference: ArchivedFontReference): Promise { const family = fontFamilyName(reference.fontId); this.statuses.set(reference.fontId, "loading"); try { const face = this.createFace(family, `url("${reference.url}")`); const loaded = await face.load(); this.fontSet.add(loaded); await this.fontSet.ready; if (!this.fontSet.check(`16px "${family}"`)) throw new Error("font_not_ready"); this.statuses.set(reference.fontId, "ready"); return "ready"; } catch { this.statuses.set(reference.fontId, "unavailable"); return "unavailable"; } } } export function createBrowserArchivedFontLoader() { return new ArchivedFontLoader({ createFace: (family, source) => new FontFace(family, source), fontSet: document.fonts as unknown as FontSetPort, }); }