Repository: AnthumChris/fetch-progress-indicators Branch: master Commit: efaaaf073bc6 Files: 20 Total size: 50.4 KB Directory structure: gitextract_baf932zw/ ├── .conf/ │ └── nginx/ │ ├── fetch-progress-headers.conf │ ├── fetch-progress-throttle.conf │ └── server.conf ├── .gitignore ├── LICENSE ├── README.md ├── fetch-basic/ │ ├── index.html │ └── supported-browser.js ├── fetch-enhanced/ │ ├── app.css │ ├── index.html │ └── supported-browser.js ├── index.html ├── sw-basic/ │ ├── app.js │ ├── index.html │ ├── supported-browser-install.js │ ├── sw-installed.html │ └── sw-simple.js └── test/ ├── abort-event.html └── sw-response-with-url-fragments/ ├── index.html └── sw-new-response.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .conf/nginx/fetch-progress-headers.conf ================================================ ## Nginx Lua module must be installed https://docs.nginx.com/nginx/admin-guide/dynamic-modules/lua/ ## https://github.com/openresty/lua-nginx-module#header_filter_by_lua header_filter_by_lua_block { function file_len(file_name) local file = io.open(file_name, "r") if (file == nil) then return -1 end local size = file:seek("end") file:close() return size end ngx.header["X-File-Size"] = file_len(ngx.var.request_filename); } add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Expose-Headers "content-encoding, content-length, x-file-size" always; ================================================ FILE: .conf/nginx/fetch-progress-throttle.conf ================================================ rewrite .*/((images|test)/.*) /$1 break; include conf.d/includes/fetch-progress-headers.conf; # Force browser to always fetch etag off; if_modified_since off; add_header cache-control no-cache; add_header Last-Modified ""; ================================================ FILE: .conf/nginx/server.conf ================================================ # HTTPs Redirect server { listen 80; listen [::]:80; server_name fetch-progress.anthum.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name fetch-progress.anthum.com; include conf.d/includes/anthum.com-ssl.conf; include conf.d/includes/security.conf; root /var/www/vhost/fetch-progress-prod; access_log /var/log/nginx/fetch-progress-prod.access.log; error_log /var/log/nginx/fetch-progress-prod.error.log; autoindex on; charset_types *; charset utf-8; gzip on; gzip_types text/plain; location / { include conf.d/includes/fetch-progress-headers.conf; } ## ## output_buffers adjustments show smoother, non-choppy progress bars for the demo. ## technically, they control number of iterations in ReadableStreamDefaultReader.read() JavaScript ## location ~ ^/120kbps/ { limit_rate 120k; output_buffers 2 32k; include conf.d/includes/fetch-progress-throttle.conf; } location ~ ^/60kbps/ { limit_rate 60k; output_buffers 1 4k; include conf.d/includes/fetch-progress-throttle.conf; } location ~ ^/30kbps/ { limit_rate 30k; output_buffers 1 2k; include conf.d/includes/fetch-progress-throttle.conf; } location ~ ^/20kbps/ { limit_rate 20k; output_buffers 1 1k; include conf.d/includes/fetch-progress-throttle.conf; } location ~ ^/10kbps/ { limit_rate 10k; output_buffers 1 1k; include conf.d/includes/fetch-progress-throttle.conf; } location /drop-connection { return 444; } } ================================================ FILE: .gitignore ================================================ /test/data ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Anthum, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ Examples for showing progress bars and progress indicators for `fetch()`. Uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API), and [Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API).
# Demo https://fetch-progress.anthum.com/

### Examples * [Fetch](https://fetch-progress.anthum.com/fetch-basic/): A ReadableStream is used to show download progress during a `fetch()` download. * [Fetch - Enhanced](https://fetch-progress.anthum.com/fetch-enhanced/): Same as above with robust code for preventing multiple downloads and handling other real-world UI interactions and edge cases. * [Service Worker](https://fetch-progress.anthum.com/sw-basic/): A ReadableStream is used in a Service Worker to simulatenously show download progress for the `FetchEvent` of an inline `` tag. ### Gzip & Content-Encoding Support - https://github.com/AnthumChris/fetch-progress-indicators/issues/13 ### Browser Support The aforementioned APIs are new/expermiental and do not currently work on all browsers. Testing was done with the browsers below: | Browser | Test Results | | :--- | :--- | | Chrome 64 | Full support | | Firefox 58/59 | Full support (requires [activation of experimental flags](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#Browser_support)) | | iOS Safari 8 | Unsupported | | iOs Safari 11 | Fetch support only. Service Workers unsupported | | Mac Safari 11.0 | Fetch support only. Service Workers unsupported | | Mac Safari 11.1 | Full Support | | IE/Edge | Not tested (no device available) | # Background Prior to the recent addition of [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch), the [XMLHttpRequest.onprogress](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/onprogress) callback handler was traditionally used to show progress bars and progress indicators. The Fetch API is great and we should use it, but "onprogress" handlers are not currently implemented with Fetch. These examples allow us to leverage Fetch while providing users with immediate feedback during certain "loading" states. Progress indicators could be especially useful to users on slow networks. # Lessons & Conclusions This repository began as a proof of concept for showing progress indicators from Service Workers. Everything seemed to work out and a few important lessons and caveats were discovered: 1. Firefox successfully stops network reading and cancels downloads on fetch events that implement custom `ReadableStream` readers when the user signals the cancelation/abort of a page load (e.g. pressing ESC, clicking stop button, invoking `window.stop()`) 1. Chrome and Safari don't stop network reading and files continue to download when a page load cancel/abort occurs. 1. The `abort` event does not seem to be firing on Chrome, Firefox, or Safari as defined in the HTML spec [7.8.12 Aborting a document load](https://html.spec.whatwg.org/multipage/browsing-the-web.html#abort-a-document). 1. `` callbacks are not called. 1. `window.onabort` callbacks are not called. 2. see [Abort Event Detection Test](https://fetch-progress.anthum.com/test/abort-event.html) 1. A Firefox bug was discovered when using hash fragments in URLs: https://bugzilla.mozilla.org/show_bug.cgi?id=1443850 ### Back-End Image Server To properly exemplify progress indicators for slow downloads or large files, a small (100kb) JPEG is being served from a remote HTTP/2 Nginx server that limits download speeds. The buffer/packet size is also reduced to show smoother, or more frequent progress updates (more iterations of `ReadableStreamDefaultReader.read()`). Otherwise, `read()` would send fewer progress updates that result in choppy progress indicators. Caching is disabled to force network requests for repeated tests. Both Baseline and Progressive JPEG files are available for testing with other speeds: https://fetch-progress.anthum.com/10kbps/images/sunrise-baseline.jpg
https://fetch-progress.anthum.com/20kbps/images/sunrise-baseline.jpg
https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg
https://fetch-progress.anthum.com/60kbps/images/sunrise-baseline.jpg
https://fetch-progress.anthum.com/120kbps/images/sunrise-baseline.jpg https://fetch-progress.anthum.com/10kbps/images/sunrise-progressive.jpg
https://fetch-progress.anthum.com/20kbps/images/sunrise-progressive.jpg
https://fetch-progress.anthum.com/30kbps/images/sunrise-progressive.jpg
https://fetch-progress.anthum.com/60kbps/images/sunrise-progressive.jpg
https://fetch-progress.anthum.com/120kbps/images/sunrise-progressive.jpg ================================================ FILE: fetch-basic/index.html ================================================ Basic Fetch() Progress Indicator
 

 

================================================ FILE: fetch-basic/supported-browser.js ================================================ 'use strict' const elProgress = document.getElementById('progress'); status('downloading with fetch()...'); fetch('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg') .then(response => { if (!response.ok) { throw Error(response.status+' '+response.statusText) } if (!response.body) { throw Error('ReadableStream not yet supported in this browser.') } // to access headers, server must send CORS header "Access-Control-Expose-Headers: content-encoding, content-length x-file-size" // server must send custom x-file-size header if gzip or other content-encoding is used const contentEncoding = response.headers.get('content-encoding'); const contentLength = response.headers.get(contentEncoding ? 'x-file-size' : 'content-length'); if (contentLength === null) { throw Error('Response size header unavailable'); } const total = parseInt(contentLength, 10); let loaded = 0; return new Response( new ReadableStream({ start(controller) { const reader = response.body.getReader(); read(); function read() { reader.read().then(({done, value}) => { if (done) { controller.close(); return; } loaded += value.byteLength; progress({loaded, total}) controller.enqueue(value); read(); }).catch(error => { console.error(error); controller.error(error) }) } } }) ); }) .then(response => response.blob()) .then(data => { status('download completed') document.getElementById('img').src = URL.createObjectURL(data); }) .catch(error => { console.error(error); status(error); }) function progress({loaded, total}) { elProgress.innerHTML = Math.round(loaded/total*100)+'%'; } ================================================ FILE: fetch-enhanced/app.css ================================================ html, body { padding: 0; margin: 0; height: 100%; } body { font-size: 16px; font-family: sans-serif; background: #2b334a; box-sizing: border-box; text-align: center; font-weight: 300; display: flex; flex-flow: column nowrap; justify-content: space-between; } a { color: #839adc; text-decoration: none; } a:hover { color: #fff; text-decoration: underline; } footer { padding: 1rem 0; } footer a { padding: .5em 1em; display: inline-block; } p { color: #9096a7; padding: 0 1em; text-align: left; line-height: 1.5em; max-width: 600px; margin: 2em auto 0 auto; } p a { color: #dbe4ff; text-decoration: underline; } .btn { position: absolute; width: 100%; opacity: 0; pointer-events: none; } .btn a { padding: .5em 1em; border-radius: 5px; background: #54638e; color: #dbe4ff; } .btn-restart { opacity: 1; pointer-events: all; transition: opacity .5s .1s ease-in; } .btn-stop { opacity: 0; pointer-events: none; } .loading .btn-restart { opacity: 0; pointer-events: none; transition: none; } .loading .btn-stop { opacity: 1; pointer-events: all; transition: opacity .5s .1s ease-in; } .image { margin: 0 auto; margin-bottom: 3rem; display: inline-block; width: 100%; max-width: 600px; background: #000; position: relative; overflow: hidden; } .image:before { display: block; content: ""; width: 100%; } .image.x40-21:before { padding-top: 52.5%; /* 40:21 aspect */ } .image img { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; z-index: 20; opacity: 0; } .loading-complete img { transition: all .8s ease-in; opacity: 1; } .image .progress-bar { position: absolute; background: #444; color: #fff; line-height: 0; text-align: left; top: 50%; left: 50%; width: 100px; margin-left: -50px; height: 4px; margin-top: -2px; z-index: 10; opacity: 0; } .loading .progress-bar { opacity: 1; } .loading-complete .progress-bar { transition: opacity .2s .2s ease-out; opacity: 0; } .progress-bar .progress { background: #1ac3c3; display: inline-block; width: 100%; transform-origin: left; transform: scaleX(0); box-sizing: border-box; height: 100%; box-shadow: 0 0 4px 1px #627188; } .loading .progress { transition: transform 100ms linear; } .error { margin-top: 3em; color: #f00; opacity: 0; height: 1em; } .error a { padding: 0; display: inline; color: inherit; text-decoration: underline; } .loading-error .error { transition: opacity .5s ease-in; opacity: 1; } @media (min-width: 700px) { main { margin-top: 3rem; } } ================================================ FILE: fetch-enhanced/index.html ================================================ Enhanced Fetch() Progress Indicator

This example uses a ReadableStream to update a progress bar while a remote image is downloaded with a declarative fetch() request. Users probably prefer seeing images load progressively with inline <img> tags, and progress bars may be better suited for larger, non-image fetch() requests.

The Service Worker example shows progress indicators while inline images load natively with <img> tags.

================================================ FILE: fetch-enhanced/supported-browser.js ================================================ 'use strict' class ProgressReportFetcher { constructor(onProgress = function() {}) { this.onProgress = onProgress; } // mimic native fetch() instantiation and return Promise fetch(input, init = {}) { const request = (input instanceof Request)? input : new Request(input) this._cancelRequested = false; return fetch(request, init).then(response => { if (!response.body) { throw Error('ReadableStream is not yet supported in this browser. More Info') } // this occurs if cancel() was called before server responded (before fetch() Promise resolved) if (this._cancelRequested) { response.body.getReader().cancel(); return Promise.reject('cancel requested before server responded.'); } if (!response.ok) { // HTTP error server response throw Error(`Server responded ${response.status} ${response.statusText}`); } // to access headers, server must send CORS header "Access-Control-Expose-Headers: content-encoding, content-length x-file-size" // server must send custom x-file-size header if gzip or other content-encoding is used const contentEncoding = response.headers.get('content-encoding'); const contentLength = response.headers.get(contentEncoding ? 'x-file-size' : 'content-length'); if (contentLength === null) { // don't evaluate download progress if we can't compare against a total size throw Error('Response size header unavailable'); } const total = parseInt(contentLength,10); let loaded = 0; this._reader=response.body.getReader() const me = this; return new Response( new ReadableStream({ start(controller) { if (me.cancelRequested) { console.log('canceling read') controller.close(); return; } read(); function read() { me._reader.read().then(({done, value}) => { if (done) { // ensure onProgress called when content-length=0 if (total === 0) { me.onProgress.call(me, {loaded, total}); } controller.close(); return; } loaded += value.byteLength; me.onProgress.call(me, {loaded, total}); controller.enqueue(value); read(); }).catch(error => { console.error(error); controller.error(error) }); } } }) ) }); } cancel() { console.log('download cancel requested.') this._cancelRequested = true; if (this._reader) { console.log('cancelling current download'); return this._reader.cancel(); } return Promise.resolve(); } } const imageLoader = (function() { const loader = document.getElementById('loader'); const img = loader.querySelector('img'); const errorMsg = loader.querySelector('.error'); const loading = loader.querySelector('.progress-bar'); const progress = loader.querySelector('.progress'); let locked, started, progressFetcher, pct; function downloadDone(url) { console.log('downloadDone()') img.src=url; img.offsetWidth; // pre-animation enabler loader.classList.remove('loading'); loader.classList.add('loading-complete'); // progressFetcher = null; } function startDownload() { // Ensure "promise-safe" (aka "thread-safe" JavaScript). // Caused by slow server response or consequetive calls to startDownload() before stopDownload() Promise resolves if (locked) { console.error('startDownload() failed. Previous download not yet initialized'); return; } locked = true; stopDownload() .then(function() { locked = false; progress.style.transform=`scaleX(0)`; progress.offsetWidth; /* prevent animation when set to zero */ started = false; pct = 0; loader.classList.add('loading'); loader.classList.remove('loading-complete'); if (!progressFetcher) { progressFetcher = new ProgressReportFetcher(updateDownloadProgress); } console.log('Starting download...'); progressFetcher.fetch('https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg') .then(response => response.blob()) .then(blob => URL.createObjectURL(blob)) .then(url => downloadDone(url)) .catch(error => showError(error)) }); } function stopDownload() { // stop previous download if (progressFetcher) { return progressFetcher.cancel() } else { // no previous download to cancel return Promise.resolve(); } } function showError(error) { console.error(error); loader.classList.remove('loading'); loader.classList.remove('loading-complete'); loader.classList.remove('loading-error'); errorMsg.offsetWidth; // pre-animation enabler errorMsg.innerHTML = 'ERROR: '+ error.message; loader.classList.add('loading-error'); } function updateDownloadProgress({loaded, total}) { if (!started) { loader.classList.add('loading'); started = true; } // handle divide-by-zero edge case when Content-Length=0 pct = total? loaded/total : 1; progress.style.transform=`scaleX(${pct})`; // console.log('downloaded', Math.round(pct*100)+'%') if (loaded === total) { console.log('download complete') } } return { startDownload, stopDownload } })() imageLoader.startDownload(); ================================================ FILE: index.html ================================================ Fetch & Streams API Progress Indicators

Fetch & Streams API Progress Indicator Examples

Service Worker - Basic

An inline <img> tag renders on the page while a Service Worker and ReadableStream simultaneously provide download progress. Minimal code is used to exemplify the basic JavaScript requirements.

Fetch - Basic

A ReadableStream shows progress while a remote image is downloaded with fetch(). Minimal code is used to exemplify the basic JavaScript requirements.

Fetch - Enhanced

Similar to above with progress bars and robust code to handle real-world UI interactions. Downloads can be cancelled/restarted, multiple downloads are prevented, race conditions don't exist, errors edge cases are handled.

Service Worker - Enhanced UI

[not yet started]. This will exemplify progress for multiple <img> tags, download cancellations, and other real-world UI interactions.

================================================ FILE: sw-basic/app.js ================================================ if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw-fetch-progress.js'); navigator.serviceWorker.addEventListener('message', event => { ProgressBar.evalProgress(event.data) }) } ================================================ FILE: sw-basic/index.html ================================================ Basic Service Worker Progress Indicator
Installing Service Worker...
================================================ FILE: sw-basic/supported-browser-install.js ================================================ navigator.serviceWorker.register('sw-simple.js') .then(reg => { if (reg.installing) { const sw = reg.installing || reg.waiting; sw.onstatechange = function() { if (sw.state === 'installed') { onward(); } }; } else if (reg.active) { // something's not right or SW is bypassed. previously-installed SW should have redirected this request to different page status('

Service Worker is installed and not functioning as intended.

Please contact developer.') } }) .catch(error => status(error)) // SW installed. Refresh page so SW can respond with SW-enabled page. function onward() { setTimeout(function() { window.location.reload(); },2000); } ================================================ FILE: sw-basic/sw-installed.html ================================================ Basic Service Worker Progress Indicator

 

 

Service Worker updated. Please reload.

This example uses a Service Worker and ReadableStream to simultaneously show download progress of an <img> tag. Unlike the fetch() examples, the image renders progressively with the browser's native loading behavior.

================================================ FILE: sw-basic/sw-simple.js ================================================ // evaluate progress on *?requestId=* URLs only const progressIndicatorUrls = /\?requestId=/i; // always install updated SW immediately self.addEventListener('install', event => { self.skipWaiting(); }) self.addEventListener('fetch', event => { const url = new URL(event.request.url); const scope = self.registration.scope; // redirect index.html to service-worker-enabled page if (event.request.url === scope || event.request.url === scope+'index.html') { const newUrl = scope+'sw-installed.html'; console.log('respondWith', newUrl); event.respondWith(fetch(newUrl)) } else if (progressIndicatorUrls.test(event.request.url)) { console.log('VER',2,event.request.url) event.respondWith(fetchWithProgressMonitor(event)) } }) function fetchWithProgressMonitor(event) { /* opaque request responses won't give us access to Content-Length and * Response.body.getReader(), which are required for calculating download * progress. Respond with a newly-constructed Request from the original Request * that will give us access to those. * See https://stackoverflow.com/questions/39109789/what-limitations-apply-to-opaque-responses * 'Access-Control-Allow-Origin' header in the response must not be the * wildcard '*' when the request's credentials mode is 'include'. We'll omit credentials in this demo. */ const newRequest = new Request(event.request, { mode: 'cors', credentials: 'omit' }) return fetch(newRequest).then(response => respondWithProgressMonitor(event.clientId, response)); } function respondWithProgressMonitor(clientId, response) { if (!response.body) { console.warn("ReadableStream is not yet supported in this browser. See https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream") return response; } if (!response.ok) { // HTTP error code response return response; } // server must send custom x-file-size header if gzip or other content-encoding is used const contentEncoding = response.headers.get('content-encoding'); const contentLength = response.headers.get(contentEncoding ? 'x-file-size' : 'content-length'); if (contentLength === null) { // don't track download progress if we can't compare against a total size console.warn('Response size header unavailable. Cannot measure progress'); return response; } let loaded = 0; debugReadIterations=0; // direct correlation to server's response buffer size const total = parseInt(contentLength,10); const reader = response.body.getReader(); return new Response( new ReadableStream({ start(controller) { // get client to post message. Awaiting resolution first read() progress // is sent for progress indicator accuracy let client; clients.get(clientId).then(c => { client = c; read(); }); function read() { debugReadIterations++; reader.read().then(({done, value}) => { if (done) { console.log('read()', debugReadIterations); controller.close(); return; } controller.enqueue(value); loaded += value.byteLength; // console.log(' SW', Math.round(loaded/total*100)+'%'); dispatchProgress({client, loaded, total}); read(); }) .catch(error => { // error only typically occurs if network fails mid-download console.error('error in read()', error); controller.error(error) }); } }, // Firefox excutes this on page stop, Chrome does not cancel(reason) { console.log('cancel()', reason); } }) ) } function dispatchProgress({client, loaded, total}) { client.postMessage({loaded,total}) } ================================================ FILE: test/abort-event.html ================================================ Abort Event Detection

This page attempts to detect the abort event. Type ESC or press browser's stop button before image download completes

================================================ FILE: test/sw-response-with-url-fragments/index.html ================================================ Service Worker Response Debugger

A Service Worker installs/updates and refreshes the page. It then intercepts "sw-image.svg" img URLs and dynamicaly responds with a static base64 image using the methods below. A bug occurs when in Firefox 59 when URLs contain a fragment value and SW responds with new Response() constructor

respondWith() fetch()

respondWith() new Response(blob)

respondWith() new Response(new ReadableStream())

================================================ FILE: test/sw-response-with-url-fragments/sw-new-response.js ================================================ // base64 SVG image const img64 = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gU3ZnIFZlY3RvciBJY29ucyA6IGh0dHA6Ly93d3cub25saW5ld2ViZm9udHMuY29tL2ljb24gLS0+DQo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwMCAxMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxMDAwIDEwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPG1ldGFkYXRhPiBTdmcgVmVjdG9yIEljb25zIDogaHR0cDovL3d3dy5vbmxpbmV3ZWJmb250cy5jb20vaWNvbiA8L21ldGFkYXRhPg0KPGc+PHBhdGggZmlsbD0iIzBiMCIgZD0iTTcyMi43LDEwSDI3Ny4zQzEyOS42LDEwLDEwLDEyOS42LDEwLDI3Ny4zdjQ0NS41QzEwLDg3MC4zLDEyOS42LDk5MCwyNzcuMyw5OTBoNDQ1LjVDODcwLjMsOTkwLDk5MCw4NzAuMyw5OTAsNzIyLjdWMjc3LjNDOTkwLDEyOS42LDg3MC4zLDEwLDcyMi43LDEweiBNODkyLDY3OC4yQzg5Miw3OTYuMiw3OTYuMiw4OTIsNjc4LjIsODkySDMyMS44QzIwMy43LDg5MiwxMDgsNzk2LjIsMTA4LDY3OC4yVjMyMS44QzEwOCwyMDMuNywyMDMuNywxMDgsMzIxLjgsMTA4aDM1Ni40Qzc5Ni4yLDEwOCw4OTIsMjAzLjcsODkyLDMyMS44VjY3OC4yTDg5Miw2NzguMnoiLz48cGF0aCBmaWxsPSIjMGIwIiBkPSJNNTA4LjgsNjYzLjljLTksOS41LTIwLjcsMTQuMi0zMi41LDE0LjJzLTIzLjUtNC44LTMyLjUtMTQuMmwtMTUzLjEtMTYyYy05LjQtMTAtMTMuNi0yMy4yLTEzLjEtMzYuM2MwLjQtMTEuOCw0LjYtMjMuNSwxMy4xLTMyLjRjOC41LTksMTkuNS0xMy40LDMwLjctMTMuOGMxMi4zLTAuNSwyNC44LDMuOSwzNC4zLDEzLjhsMTIwLjcsMTI3LjZMNjg4LjksMzM2YzkuNC0xMCwyMS45LTE0LjQsMzQuMy0xMy44YzExLjEsMC41LDIyLjEsNC45LDMwLjYsMTMuOGM4LjUsOSwxMi43LDIwLjcsMTMuMSwzMi40YzAuNSwxMy4xLTMuNiwyNi4zLTEzLjEsMzYuM0w1MDguOCw2NjMuOXoiLz48L2c+DQo8L3N2Zz4='; const imgUrl = 'data:image/svg+xml;base64,'+img64; const imgBytes = atob(img64); // create blob for testing responses const imgByteArray = new Uint8Array(imgBytes.length); for (let i=0; i { self.skipWaiting(); }) self.addEventListener('fetch', event => { const url = event.request.url if (/\/sw-image\.svg/.test(url)) { if (/\?fetch/.test(url)) { console.log('fetch', url); event.respondWith(fetch(imgUrl)) } else if (/\?blob-response/.test(url)) { console.log('blob-response', url); event.respondWith(new Response(blob)) } else if (/\?stream-response/.test(url)) { console.log('stream-response', url); event.respondWith( new Response( new ReadableStream({ start(controller) { controller.enqueue(imgByteArray); controller.close(); } }), { headers: { 'content-type': 'image/svg+xml' } } ) ) } } })