Service Worker Download Fixes for Firefox & Improved Caching
🐛 Bug Fixes
- web: Added
try/catcharound bothcrypto.subtle.decryptcalls in the Service Worker. An unauthenticated or corrupted encrypted record caused the decrypt call to throw, which rejected thepull()promise and let Firefox mark the download as completed with the incorrect partial file size. The stream is now explicitly errored viacontroller.error()and adl-errorbroadcast is sent to the UI on failure. - web: Added a
cancelledflag to the Service Worker'scancel()handler, checked at everyawaitboundary inpull(). Previously, callingcancel()whilepull()was awaitingreadMore()letpull()continue executing on an already-cancelled stream, which could reachcontroller.close()and throw.
🎨 Improvements
- web: Changed the Service Worker ReadableStream from
highWaterMark: 0tohighWaterMark: 2to address reported Firefox browser freezes during large-file downloads. WithhighWaterMark: 0,pull()is only triggered by a pending consumerread(), which may cause Firefox's download pipeline to block until a decrypted record is ready. WithhighWaterMark: 2,pull()runs proactively and keeps up to two pre-decrypted records in the internal queue, reducing the chance of a synchronous stall.pull()still enqueues exactly one record per call, so the stall-at-random-percentage behavior from v2.9.2 cannot recur. - server:
download-sw.jsis now served withCache-Control: no-cache, must-revalidate. Previously the file was served by the static file middleware with no explicit cache header, allowing browsers and intermediate proxies to apply heuristic caching. Withno-cache, browsers byte-check the Service Worker file on every navigation and register any updated version immediately after a deployment, so users are never stuck running an outdated SW. - web: Replaced the single-buffer copy strategy in the Service Worker ECE decryption pipeline with a zero-copy chunk queue. Previously
appendToBuf()copied both the leftover bytes from the previous chunk (~65 KB) and the newly received chunk (~65 KB) into a fresh combined buffer on everypull()call - generating ~5 GB of unnecessary allocations for a 2.4 GB file. The new implementation pushes incomingreader.read()chunks into a queue by reference.readFromBuf()copies only the bytes required for the current decrypt call (one record, 65552 bytes), reducing GC pressure by roughly 2.5x and eliminating the speed oscillation and browser sluggishness during large downloads. - web: Eliminated the per-record
Uint8Arrayallocation inreadFromBuf()by introducing a pre-allocatedscratchRecordbuffer (ENCRYPTED_RECORD_SIZEbytes) that is reused across allpull()calls.crypto.subtle.decryptcopies its input synchronously before returning the Promise, making the buffer safe to reuse as soon as theawaitresolves. This removes a further ~2.4 GB of GC-managed allocations per large file download, reducing peak heap pressure and Firefox RAM consumption.
🐳 Docker
- Image:
skyfay/skysend:v2.9.4 - Also tagged as:
latest,v2 - Platforms: linux/amd64, linux/arm64