/** * Convert big-endian u32 to native JavaScript number. */ export function uint_bytes_to_num(value: Uint8Array): number { if (value.length != 4) { throw new Error("can't convert non 4-byte integer"); } const data_view = new DataView(value.buffer); return data_view.getUint32(0, false); } export function utf8_encode(value: string): Uint8Array { const encoder = new TextEncoder(); let res: Uint8Array = new Uint8Array(0); for (const ch of value) { const cur = encoder.encode(ch)[0]; const prev = res; res = new Uint8Array(prev.length + 1); res.set(prev, 0); res.set([cur], prev.length); } return res; } export function utf8_decode(value: Uint8Array): string { const decoder = new TextDecoder("utf-8"); return decoder.decode(value); } export async function sleep(millis: number): Promise { await new Promise((resolver) => { setTimeout(resolver, millis); }); } export function wait_for_dom_refresh(): Promise { return new Promise((resolver) => { requestAnimationFrame(() => { requestAnimationFrame(() => { resolver(); }); }); }); } /** * Provides a locking abstraction around any ref-passed object to provide inner mutability that can * automatically release on promise resolution. This provides "atomic transactions" on the inner * type. * * This allows you to not await the transaction result, but offload the waiting to the next call * to run. So other tasks can normally run when the transaction is executing. And if the inner * type is needed again... the run call will wait for the (possibly) running transaction to * finish first. */ export class AsyncRunner { private inner: T; private lock: Promise; public constructor(inner: T) { this.inner = inner; this.lock = Promise.resolve(); } public async run(transaction: (target: T) => Promise): Promise { await this.lock; this.lock = new Promise((resolver) => { transaction(this.inner).then(() => { resolver() }); }); } } export namespace el { function add_classes(target: HTMLElement, classes?: string[]): void { if (classes === undefined) { return; } for (const cls of classes) { target.classList.add(cls); } } export function div(children: HTMLElement[], classes?: string[]): HTMLDivElement { const el = document.createElement("div"); add_classes(el, classes); el.append(...children); return el; } export function h1(text: string, classes?: string[]): HTMLHeadingElement { const el = document.createElement("h1"); el.innerText = text; add_classes(el, classes); return el; } export function h2(text: string, classes?: string[]): HTMLHeadingElement { const el = document.createElement("h2"); el.innerText = text; add_classes(el, classes); return el; } export function p(text: string, classes?: string[]): HTMLParagraphElement { const el = document.createElement("p"); el.innerText = text; add_classes(el, classes); return el; } export async function img(src: string, classes?: string[]): Promise { const el = new Image(); el.src = src; el.draggable = false; add_classes(el, classes); await new Promise((resolver) => { el.addEventListener("load", () => { resolver(); }); }); return el; } export async function video( src: string, subtitles_url: string | null, classes?: string[], ): Promise { const blob = await (await fetch(src)).blob(); const el = document.createElement("video"); el.autoplay = true; const source = document.createElement("source"); source.src = URL.createObjectURL(blob); el.appendChild(source); if (subtitles_url !== null) { const subs = document.createElement("track"); subs.src = subtitles_url; subs.kind = "subtitles"; subs.default = true; el.appendChild(subs); } add_classes(el, classes); return el; } }