aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/sw.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/sw.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/sw.js60
1 files changed, 60 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/sw.js b/packages/meshbay-hub/src/meshbay_hub/static/sw.js
new file mode 100644
index 0000000..d0a8805
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/sw.js
@@ -0,0 +1,60 @@
+/**
+ * Service worker: the only way to stream a download to disk in Firefox.
+ *
+ * Chrome and Edge have the File System Access API — the page opens a file and
+ * writes to it. Firefox and Safari do not, and the alternative there was to
+ * collect the whole download in memory and hand the browser a blob, which is
+ * not an option for a file measured in gigabytes.
+ *
+ * So the page makes up a URL, tells this worker what stream answers it, and
+ * navigates a hidden iframe there. The worker replies with the stream and a
+ * Content-Disposition header, and the browser does what it does with any
+ * download: writes it to disk as it arrives, showing its own progress, with
+ * nothing buffered in the tab.
+ *
+ * It caches nothing and intercepts nothing else. Every request that is not one
+ * of these downloads falls through untouched.
+ */
+
+const PREFIX = '/_mbdl/';
+const pending = new Map();
+
+self.addEventListener('install', () => self.skipWaiting());
+self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim()));
+
+self.addEventListener('message', (event) => {
+ const data = event.data || {};
+ if (data.type !== 'mbdl' || !data.id || !data.readable) return;
+ pending.set(data.id, {
+ readable: data.readable,
+ filename: data.filename || 'download',
+ size: Number(data.size) || 0,
+ });
+ // A tab that is closed before it navigates would leave a stream here for the
+ // life of the worker.
+ setTimeout(() => pending.delete(data.id), 60000);
+});
+
+self.addEventListener('fetch', (event) => {
+ const url = new URL(event.request.url);
+ if (url.origin !== self.location.origin || !url.pathname.startsWith(PREFIX)) {
+ return; // not ours — the network handles it
+ }
+
+ const entry = pending.get(url.pathname.slice(PREFIX.length));
+ if (!entry) return;
+ pending.delete(url.pathname.slice(PREFIX.length));
+
+ const headers = {
+ 'Content-Type': 'application/octet-stream',
+ // filename* so a name with accents or spaces survives the trip.
+ 'Content-Disposition':
+ `attachment; filename*=UTF-8''${encodeURIComponent(entry.filename)}`,
+ 'Cache-Control': 'no-store',
+ };
+ // Only when it is known. A zip is assembled as it goes and announcing a
+ // length we then miss would truncate the file.
+ if (entry.size > 0) headers['Content-Length'] = String(entry.size);
+
+ event.respondWith(new Response(entry.readable, { headers }));
+});