import type { LoadProgress } from "./shared/types"; const CACHE = "laya-webgpu-v1"; async function sha256(bytes: ArrayBuffer): Promise { const hash = await crypto.subtle.digest("SHA-256", bytes); return [...new Uint8Array(hash)].map((part) => part.toString(16).padStart(2, "0")).join(""); } export async function fetchVerified( url: string, expectedHash?: string, onProgress?: (progress: LoadProgress) => void, signal?: AbortSignal, ): Promise { const cache = "caches" in globalThis ? await caches.open(CACHE).catch(() => undefined) : undefined; const cached = await cache?.match(url).catch(() => undefined); if (cached) { const bytes = await cached.arrayBuffer(); if (!expectedHash || await sha256(bytes) === expectedHash.toLowerCase()) return bytes; await cache?.delete(url).catch(() => false); } const response = await fetch(url, { signal }); if (!response.ok) throw new Error(`Download failed (${response.status}) for ${url}`); const total = Number(response.headers.get("content-length")) || undefined; if (!response.body) { const bytes = await response.arrayBuffer(); if (expectedHash && await sha256(bytes) !== expectedHash.toLowerCase()) throw new Error(`SHA-256 mismatch for ${url}`); return bytes; } const reader = response.body.getReader(); const chunks: Uint8Array[] = []; let loaded = 0; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.byteLength; onProgress?.({ phase: "model", loaded, total, detail: url }); } const joined = new Uint8Array(loaded); let offset = 0; for (const chunk of chunks) { joined.set(chunk, offset); offset += chunk.length; } if (expectedHash && await sha256(joined.buffer) !== expectedHash.toLowerCase()) throw new Error(`SHA-256 mismatch for ${url}`); await cache?.put(url, new Response(joined, { headers: response.headers })).catch(() => undefined); return joined.buffer; } export async function clearModelCache(): Promise { if ("caches" in globalThis) await caches.delete(CACHE); }