Files
next-tjwater-frontend/src/shared/ai-elements/persona-resource.ts
T
jiang 2d8506fea0
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s
feat: add project-aware agent workspace
2026-10-22 18:52:18 +08:00

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;
}