diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/sw.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/sw.js | 60 |
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 })); +}); |