Files
tyx_AI_xhs/apps/web/src/text-font-loader.ts
T

76 lines
2.4 KiB
TypeScript

export type ArchivedFontStatus = "idle" | "loading" | "ready" | "unavailable";
interface LoadableFontFace {
load: () => Promise<unknown>;
}
interface FontSetPort {
add: (face: unknown) => unknown;
check: (font: string) => boolean;
ready: Promise<unknown>;
}
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<string, Promise<ArchivedFontStatus>>();
private readonly statuses = new Map<string, ArchivedFontStatus>();
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<ArchivedFontStatus> {
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<ArchivedFontStatus> {
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,
});
}