UNPKG

@digital-blueprint/formalize-app

Version:

[GitHub Repository](https://github.com/digital-blueprint/formalize-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/formalize-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/formalize-app/) | [Formalize Bundle](https:

174 lines (157 loc) • 7.88 kB
importScripts('client-zip/worker.js', 'dl-stream/worker.js'); // AbortController for the current download, so it can be cancelled from the main page /** @type {AbortController | null} */ let currentDownloadAbortController = null; const serviceWorker = /** @type {ServiceWorkerGlobalScope} */ (/** @type {unknown} */ (self)); async function* nameFile(responses, names) { for await (const response of responses) { yield {input: response, name: names.next().value}; } } async function* appendFiles(stream, files) { for await (const item of stream) { yield item; } for (const file of files) { yield file; } } serviceWorker.addEventListener('fetch', (event) => { const url = new URL(event.request.url); // This will intercept all request with a URL starting in /downloadZip/ // eslint-disable-next-line no-sparse-arrays const [, name] = url.pathname.match(/\/downloadZip\/(.+)/i) || [,]; if (url.origin === serviceWorker.location.origin && name && name !== 'keep-alive') { // Create a new AbortController for this download currentDownloadAbortController = new AbortController(); const signal = currentDownloadAbortController.signal; event.respondWith( event.request .formData() .then(async (data) => { let requests; let files = []; /** @type {string | null} */ let auth = null; let sumContentLengths = -1; // check meta information given to the sw, and set it if necessary if (data.has('authorization')) { auth = /** @type {string} */ (data.get('authorization')); data.delete('authorization'); } if (data.has('sumContentLengths')) { sumContentLengths = parseInt( /** @type {string} */ (data.get('sumContentLengths')), ); data.delete('sumContentLengths'); } // only send Authorization headers if necessary if (auth != null) { requests = Array.from(data.values(), (data) => { if (data instanceof File) { files.push(data); return undefined; } else return new Request(data, { headers: {Authorization: auth}, signal, }); }).filter((x) => x !== undefined); } else { requests = Array.from( data.values(), (data) => new Request(/** @type {string} */ (data), {signal}), ); } // if sum of content-lengths is given, trust it // else we try to sum up the content-lengths ourselves at the cost of additional runtime if (sumContentLengths === -1) { const items = Array.from(data.values()); for (const item of items) { if (signal.aborted) break; let init = {method: 'HEAD', signal}; if (auth != null) { init['headers'] = {Authorization: auth}; } await fetch(/** @type {string} */ (item), init) .then( (r) => (sumContentLengths += parseInt( /** @type {string} */ (r.headers.get('Content-Length')), )), ) .catch((e) => { if (e.name !== 'AbortError') { console.warn('HEAD request failed for content-length:', e); } }); } } // If the download was cancelled during preparation, return early if (signal.aborted) { currentDownloadAbortController = null; return new Response('', { status: 200, headers: {'Content-Type': 'text/html'}, }); } // put filenames into array in the correct order, thus we need two loops let filenames = []; data.forEach((value, key) => { if (!(value instanceof File)) { filenames.push(key); } }); data.forEach((value, key) => { if (value instanceof File) { filenames.push(value.name); } }); // Notify the client that streaming has begun // includeUncontrolled is required because the SW may not yet control the page const clients = await serviceWorker.clients.matchAll({ type: 'window', includeUncontrolled: true, }); clients.forEach((client) => client.postMessage({type: 'STREAMED_DOWNLOAD_STARTED'}), ); // Clean up the abort controller reference now that streaming has started currentDownloadAbortController = null; // ignore undef errors, as the global definitions are not tracked by default // works as expected: https://github.com/eslint/eslint/issues/16904 // eslint-disable-next-line no-undef // @ts-expect-error Loaded globally by client-zip/worker.js. return downloadZip( nameFile( // eslint-disable-next-line no-undef // @ts-expect-error Loaded globally by dl-stream/worker.js. appendFiles(new DownloadStream(requests, {highWaterMark: 1}), files), filenames[Symbol.iterator](), ), {length: sumContentLengths}, ); }) .catch(() => { currentDownloadAbortController = null; // Return a valid empty response on any error (including cancellation) // so the browser does not show a network error page. return new Response('', { status: 200, headers: {'Content-Type': 'text/html'}, }); }), ); } }); // for some reason, in Firefox (tested on version 140.0.2) the fetch event listener is not enough // to keep the sw alive, even when periodically fetching something. // thus, we register a "message" event listener and periodically send messages serviceWorker.addEventListener('message', (event) => { if (event.data === 'CANCEL_DOWNLOAD' || event.data?.type === 'CANCEL_DOWNLOAD') { if (currentDownloadAbortController) { currentDownloadAbortController.abort(); currentDownloadAbortController = null; } } });