summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/sw.js
blob: d0a8805b4a7af4bf51e954148d495f7514a87bf4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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 }));
});