feat: add project-aware agent workspace
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user