From d524966f0033c1e6dbcdffe9a6bd8da45cbca27c Mon Sep 17 00:00:00 2001 From: ZDDC Date: Wed, 3 Jun 2026 15:33:50 -0500 Subject: [PATCH] perf(browse): stream files into the offline zip instead of buffering all bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit downloadFsSubtree pre-read every file's arrayBuffer() and handed the raw ArrayBuffer to JSZip, so the entire subtree's bytes sat in the JS heap at once before zipping — the likely OOM on a large local folder despite the size warning. Hand JSZip the File (a Blob backed by disk) instead; it reads each lazily during generateAsync, dropping peak memory to roughly the zip output plus JSZip's working set. Also document, on downloadUrl, why server-side download errors aren't surfaced as toasts: the click is fire-and-forget, and the folder path targets zddc-server's streamed virtual ".zip" endpoint — routing it through fetch() to make errors catchable would defeat the streaming for arbitrarily large archives. Left as a known, documented limitation rather than a buffering regression. All 6 browse Playwright specs pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- browse/js/download.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/browse/js/download.js b/browse/js/download.js index 24be58d..b489ff7 100644 --- a/browse/js/download.js +++ b/browse/js/download.js @@ -44,6 +44,12 @@ } // Trigger a download from a same-origin server URL via Content-Disposition. + // NOTE: an click is fire-and-forget — a server error + // (401/403/404/5xx) can't be observed here, so failures surface only as + // the browser's own download error, not a toast. This is deliberate: the + // folder path points at zddc-server's streamed virtual ".zip" + // endpoint, and buffering it through fetch() to make errors catchable + // would defeat the streaming (the archive can be arbitrarily large). function downloadUrl(filename, url) { var a = document.createElement('a'); a.href = url; @@ -97,9 +103,12 @@ var zip = new window.JSZip(); for (var i = 0; i < files.length; i++) { ev.statusInfo('Zipping ' + rootHandle.name + '… (' + (i + 1) + '/' + files.length + ')'); + // Hand JSZip the File (a Blob, backed by disk) rather than + // pre-reading every file's arrayBuffer — otherwise the whole + // tree's raw bytes sit in the JS heap at once before zipping. + // JSZip reads each Blob lazily during generateAsync. var f = await files[i].handle.getFile(); - var buf = await f.arrayBuffer(); - zip.file(rootHandle.name + '/' + files[i].relPath, buf); + zip.file(rootHandle.name + '/' + files[i].relPath, f); } ev.statusInfo('Generating ' + rootHandle.name + '.zip…'); var blob = await zip.generateAsync({ type: 'blob' });