62 lines
2.2 KiB
TypeScript
62 lines
2.2 KiB
TypeScript
import asset from "./persona-resource-manifest.json";
|
|
|
|
const source = `${asset.path}?v=${asset.sha256}`;
|
|
const cacheName = "tjwater-agent-persona-v1";
|
|
|
|
let pending: Promise<ArrayBuffer> | undefined;
|
|
|
|
async function readResource(response: Response): Promise<ArrayBuffer> {
|
|
if (!response.ok) throw new Error(`Obsidian resource: ${response.status}`);
|
|
const bytes = await response.arrayBuffer();
|
|
const header = new Uint8Array(bytes, 0, Math.min(bytes.byteLength, 4));
|
|
if (bytes.byteLength !== asset.byteLength || String.fromCharCode(...header) !== "RIVE") {
|
|
throw new Error("Obsidian resource: invalid animation data");
|
|
}
|
|
if (globalThis.crypto?.subtle) {
|
|
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
const hash = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
if (hash !== asset.sha256) throw new Error("Obsidian resource: version mismatch");
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
async function loadBytes(): Promise<ArrayBuffer> {
|
|
let cache: Cache | undefined;
|
|
try {
|
|
cache = await globalThis.caches?.open(cacheName);
|
|
const cached = await cache?.match(source);
|
|
if (cached) return await readResource(cached);
|
|
} catch {
|
|
// Storage can be denied or contain incomplete data. Retry through the network.
|
|
}
|
|
|
|
const response = await fetch(source, { signal: AbortSignal.timeout(15_000) });
|
|
const bytes = await readResource(response);
|
|
if (cache) {
|
|
try {
|
|
await cache.put(source, new Response(bytes, {
|
|
headers: { "Content-Type": "application/octet-stream" }
|
|
}));
|
|
const current = new URL(source, window.location.href);
|
|
for (const request of await cache.keys()) {
|
|
const url = new URL(request.url);
|
|
if (url.origin === current.origin && url.pathname === asset.path && url.href !== current.href) {
|
|
await cache.delete(request);
|
|
}
|
|
}
|
|
} catch {
|
|
// Quota or storage failures must not prevent the downloaded animation playing.
|
|
}
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
// Share the immutable bytes, never a live renderer or View Model instance.
|
|
export function loadPersonaResource() {
|
|
pending ??= loadBytes().catch((error: unknown) => {
|
|
pending = undefined;
|
|
throw error;
|
|
});
|
|
return pending;
|
|
}
|