diff --git a/frontend/src/lib/api/endpoints/deltaUpload.ts b/frontend/src/lib/api/endpoints/deltaUpload.ts index 4290a443..af5b5a48 100644 --- a/frontend/src/lib/api/endpoints/deltaUpload.ts +++ b/frontend/src/lib/api/endpoints/deltaUpload.ts @@ -80,14 +80,33 @@ export function tryDeltaUpload( const timeoutMs = DELTA_TIMEOUT_BASE_MS + Math.ceil(sizeGB) * DELTA_TIMEOUT_PER_GB_MS; let savedBytes = 0; + let stallTimer: ReturnType; const settle = (answer: DeltaUploadAnswer | null) => { clearTimeout(timer); + clearTimeout(stallTimer); worker.terminate(); resolve(answer); }; const timer = setTimeout(() => settle(null), timeoutMs); + // Liveness watchdog: a healthy worker posts progress sub-second while it + // hashes and uploads. If it goes SILENT this long it is wedged (WASM init + // or chunking hung without throwing, emitting neither fallback nor error) + // — exactly what freezes a folder upload ~2 min per large file. Disable + // delta for this file AND every later one so they fall straight to a plain + // upload instead of each burning the full size-scaled delta timeout. + const STALL_MS = 20_000; + const armStall = () => { + clearTimeout(stallTimer); + stallTimer = setTimeout(() => { + usable = false; + settle(null); + }, STALL_MS); + }; + armStall(); + worker.onmessage = (event: MessageEvent) => { + armStall(); // worker is alive — reset the liveness watchdog const msg = event.data; if (msg.type === 'progress') { savedBytes = msg.reusedBytes; diff --git a/src/interfaces/web/mod.rs b/src/interfaces/web/mod.rs index a2668038..0667d590 100644 --- a/src/interfaces/web/mod.rs +++ b/src/interfaces/web/mod.rs @@ -112,7 +112,14 @@ pub fn content_security_policy(config: &AppConfig) -> String { ); } - let mut script_src = String::from("script-src 'self'"); + // `'wasm-unsafe-eval'` is required for WebAssembly compilation/instantiation + // under a strict CSP (Chromium blocks `WebAssembly.instantiate` otherwise with + // "Wasm code generation disallowed by embedder"). The frontend instantiates the + // vendored BLAKE3/FastCDC WASM both on the main thread (instant by-hash uploads) + // and inside the delta-upload worker; without this they throw and every large + // file silently falls back to a plain byte upload. It is the WASM-only, safe + // variant — it does NOT permit `eval()`/`new Function()` (no `'unsafe-eval'`). + let mut script_src = String::from("script-src 'self' 'wasm-unsafe-eval'"); for hash in &hashes { script_src.push(' '); script_src.push_str(hash);