diff --git a/packages/ui-default/service-worker.ts b/packages/ui-default/service-worker.ts index 8c86327a..3649bd74 100644 --- a/packages/ui-default/service-worker.ts +++ b/packages/ui-default/service-worker.ts @@ -3,7 +3,75 @@ /// /* global clients */ /* eslint-disable no-restricted-globals */ -import 'streamsaver/sw.js'; +const map = new Map(); + +function createStream(port) { + return new ReadableStream({ + start(controller) { + port.onmessage = ({ data }) => { + if (data === 'end') return controller.close(); + if (data === 'abort') return controller.error('Aborted the download'); + return controller.enqueue(data); + }; + }, + cancel(reason) { + console.log('user aborted', reason); + port.postMessage({ abort: true }); + }, + }); +} + +self.onmessage = (event) => { + if (event.data === 'ping') return; + const data = event.data; + const downloadUrl = data.url || `${self.registration.scope + Math.random()}/${typeof data === 'string' ? data : data.filename}`; + const port = event.ports[0]; + const metadata = new Array(3); // [stream, data, port] + metadata[1] = data; + metadata[2] = port; + if (event.data.readableStream) { + metadata[0] = event.data.readableStream; + } else if (event.data.transferringReadable) { + port.onmessage = (evt) => { + port.onmessage = null; + metadata[0] = evt.data.readableStream; + }; + } else metadata[0] = createStream(port); + map.set(downloadUrl, metadata); + port.postMessage({ download: downloadUrl }); +}; + +function streamsaver(event: FetchEvent) { + const hijacke = map.get(event.request.url); + const [stream, data, port] = hijacke; + map.delete(event.request.url); + const responseHeaders = new Headers({ + 'Content-Type': 'application/octet-stream; charset=utf-8', + 'Content-Security-Policy': "default-src 'none'", + 'X-Content-Security-Policy': "default-src 'none'", + 'X-WebKit-CSP': "default-src 'none'", + 'X-XSS-Protection': '1; mode=block', + }); + const headers = new Headers(data.headers || {}); + if (headers.has('Content-Length')) { + responseHeaders.set('Content-Length', headers.get('Content-Length')); + } + if (headers.has('Content-Disposition')) { + responseHeaders.set('Content-Disposition', headers.get('Content-Disposition')); + } + if (data.size) { + console.warn('Depricated'); + responseHeaders.set('Content-Length', data.size); + } + let fileName = typeof data === 'string' ? data : data.filename; + if (fileName) { + console.warn('Depricated'); + fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A'); + responseHeaders.set('Content-Disposition', `attachment; filename*=UTF-8''${fileName}`); + } + event.respondWith(new Response(stream, { headers: responseHeaders })); + port.postMessage({ debug: 'Download started' }); +} // TODO not working self.addEventListener('notificationclick', (event) => { @@ -74,7 +142,10 @@ self.addEventListener('activate', (event) => { initConfig(); event.waitUntil(self.clients.claim()); const valid = [PRECACHE]; - caches.keys().then((names) => names.filter((name) => !valid.includes(name)).map((p) => caches.delete(p))); + caches.keys().then((names) => names + .filter((name) => name.startsWith('precache-')) + .filter((name) => !valid.includes(name)) + .map((p) => caches.delete(p))); }); async function get(request: Request) { @@ -122,6 +193,14 @@ async function cachedRespond(request: Request) { } self.addEventListener('fetch', (event: FetchEvent) => { + if (event.request.url.endsWith('/ping')) { + event.respondWith(new Response('pong')); + return; + } + if (map.get(event.request.url)) { + streamsaver(event); + return; + } if (!['get', 'post', 'head'].includes(event.request.method.toLowerCase())) return; if (!config) return; // Don't do anything when not initialized const url = new URL(event.request.url);