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).
<img clear="both" align="left" width="200px" src="https://raw.githubusercontent.com/AnthumChris/fetch-progress-indicators/master/images/logo-streams-300.png" /><br>
# Demo
https://fetch-progress.anthum.com/
<br><br>
### 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 `<img>` 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. `<img onabort>` 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<br>
https://fetch-progress.anthum.com/20kbps/images/sunrise-baseline.jpg<br>
https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg<br>
https://fetch-progress.anthum.com/60kbps/images/sunrise-baseline.jpg<br>
https://fetch-progress.anthum.com/120kbps/images/sunrise-baseline.jpg
https://fetch-progress.anthum.com/10kbps/images/sunrise-progressive.jpg<br>
https://fetch-progress.anthum.com/20kbps/images/sunrise-progressive.jpg<br>
https://fetch-progress.anthum.com/30kbps/images/sunrise-progressive.jpg<br>
https://fetch-progress.anthum.com/60kbps/images/sunrise-progressive.jpg<br>
https://fetch-progress.anthum.com/120kbps/images/sunrise-progressive.jpg
================================================
FILE: fetch-basic/index.html
================================================
<html>
<head>
<meta charset="utf8">
<title>Basic Fetch() Progress Indicator</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="../favicon.ico" type="image/png" />
<meta name="description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch">
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@AnthumChris" />
<meta name="og:title" content="Progress Indicators using Streams, Service Workers, and Fetch" />
<meta name="twitter:title" content="Progress Indicators using Streams, Service Workers, and Fetch" />
<meta name="og:description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch" />
<meta name="twitter:description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch" />
<meta name="og:image" content="https://fetch-progress.anthum.com/images/logo-streams-300.png" />
<meta name="twitter:image" content="https://fetch-progress.anthum.com/images/logo-streams-300.png" />
<style>
html, body {
padding: 0;
margin: 0;
height: 100%;
}
body {
text-align: center;
font-size: 16px;
padding: 1rem 0;
box-sizing: border-box;
font-family: sans-serif;
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
}
#img {
width: 600px;
max-width: 100%;
margin-bottom: 1rem;
display: inline-block;
}
button {
display: block;
margin: 0 auto;
}
footer a {
padding: 1em;
display: inline-block;
}
</style>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-50610215-6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-50610215-6');
</script>
</head>
<body>
<main>
<div id="status"> </div>
<h1 id="progress"> </h1>
<img id="img" />
<button onclick="window.location.reload()">Reload</button>
</main>
<footer>
<a href="../">More Examples</a>
<a href="https://github.com/AnthumChris/fetch-progress-indicators">View on GitHub</a>
</footer>
<script>
'use strict'
var elStatus = document.getElementById('status');
function status(text) {
elStatus.innerHTML = text;
}
// Ensure unsupported browsers don't load invalid JS (e.g. ES6, Promises), compile successfully, and show errors
if (!(window.fetch && window.ReadableStream)) {
status('Fetch or ReadableStream API is not supported in this browser.<p>This demo cannot function without them.');
} else {
var script = document.createElement('script');
script.src = 'supported-browser.js';
document.body.append(script);
}
</script>
</body>
</html>
================================================
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
================================================
<html>
<head>
<meta charset="utf8">
<title>Enhanced Fetch() Progress Indicator</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="../favicon.ico" type="image/png" />
<link rel="stylesheet" type="text/css" href="app.css">
<meta name="description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch">
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@AnthumChris" />
<meta name="og:title" content="Progress Indicators using Streams, Service Workers, and Fetch" />
<meta name="twitter:title" content="Progress Indicators using Streams, Service Workers, and Fetch" />
<meta name="og:description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch" />
<meta name="twitter:description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch" />
<meta name="og:image" content="https://fetch-progress.anthum.com/images/logo-streams-300.png" />
<meta name="twitter:image" content="https://fetch-progress.anthum.com/images/logo-streams-300.png" />
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-50610215-6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-50610215-6');
</script>
</head>
<body>
<main>
<div class="loader" id="loader">
<div id="image" class="image x40-21"><div class="progress-bar" id="progress-bar"><div class="progress" id="progress"></div></div><img id="img" src=""></div>
<div class="btn btn-restart"><a href="javascript:void(0)" onclick="imageLoader.startDownload()">Restart</a></div>
<div class="btn btn-stop"><a href="javascript:void(0)" onclick="imageLoader.stopDownload()">Stop</a></div>
<div id="error" class="error"></div>
</div>
<p>
This example uses a <code>ReadableStream</code> to update a progress bar while a remote image is downloaded with a declarative <code>fetch()</code> request.
Users probably prefer seeing images load progressively with inline <code><img></code> tags, and
progress bars may be better suited for larger, non-image <code>fetch()</code> requests.
</p>
<p>
The <a href="../sw-basic/">Service Worker example</a> shows progress indicators while inline images load
natively with <code><img></code> tags.
</p>
</main>
<footer>
<a href="../">More Examples</a>
<a href="https://github.com/AnthumChris/fetch-progress-indicators">View on GitHub</a>
</footer>
<script>
// Ensure unsupported browsers don't load invalid JS (e.g. ES6, Promises), compile successfully, and show errors
if (!(window.fetch && window.ReadableStream)) {
document.getElementsByTagName('main')[0].innerHTML = '<div style="margin-top: 2rem; color: #fff;">Fetch or ReadableStream API is not supported in this browser.<br><br>This demo cannot function without them.</div>'
} else {
var script = document.createElement('script');
script.src = 'supported-browser.js';
document.body.append(script);
}
</script>
</body>
</html>
================================================
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. <a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream">More Info</a>')
}
// 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
================================================
<html>
<head>
<meta charset="utf8">
<title>Fetch & Streams API Progress Indicators</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="favicon.ico" type="image/png" />
<meta name="description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch">
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@AnthumChris" />
<meta name="og:title" content="Progress Indicators using Streams, Service Workers, and Fetch" />
<meta name="twitter:title" content="Progress Indicators using Streams, Service Workers, and Fetch" />
<meta name="og:description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch" />
<meta name="twitter:description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch" />
<meta name="og:image" content="https://fetch-progress.anthum.com/images/logo-streams-300.png" />
<meta name="twitter:image" content="https://fetch-progress.anthum.com/images/logo-streams-300.png" />
<style>
html, body {
margin: 0;
padding: 0;
color: #333;
}
body {
padding: 0 1em;
text-align: left;
font-family: sans-serif;
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
max-width: 600px;
margin: 0 auto;
}
a {
text-decoration: underline;
}
footer {
padding: 1rem 0;
text-align: center;
}
footer a {
padding: .5em 1em;
display: inline-block;
}
p {
text-align: left;
line-height: 1.5em;
margin: 0 auto 1.5em auto;
}
code {
line-height: 1em;
}
h1 {
margin-bottom: 3rem;
font-size: 130%
}
h2 {
font-size: 110%;
margin-bottom: .25em;
}
</style>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-50610215-6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-50610215-6');
</script>
</head>
<body>
<main>
<h1>Fetch & Streams API Progress Indicator Examples</h1>
<h2><a href="sw-basic/">Service Worker - Basic</a></h2>
<p>
An inline <code><img></code> tag renders on the page while a Service Worker and <code>ReadableStream</code> simultaneously provide download progress. Minimal code is used to exemplify the basic JavaScript requirements.
</p>
<h2><a href="fetch-basic/">Fetch - Basic</a></h2>
<p>
A <code>ReadableStream</code> shows progress while a remote image is downloaded with <code>fetch()</code>. Minimal code is used to exemplify the basic JavaScript requirements.
</p>
<h2><a href="fetch-enhanced/">Fetch - Enhanced</a></h2>
<p>
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.
</p>
<h2>Service Worker - Enhanced UI</h2>
<p>[not yet started]. This will exemplify progress for multiple <code><img></code> tags, download cancellations, and other real-world UI interactions.</p>
</main>
<footer>
<a href="https://github.com/AnthumChris/fetch-progress-indicators">View on GitHub</a>
</footer>
</body>
</html>
================================================
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
================================================
<!-- This page loads Service Worker and won't be shown again after it refreshes for SW interception. -->
<html>
<head>
<meta charset="utf8">
<title>Basic Service Worker Progress Indicator</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="../favicon.ico" type="image/png" />
<style>
html, body {
padding: 0;
margin: 0;
height: 100%;
}
body {
text-align: center;
font-size: 16px;
padding: 5em 1em 1em;
box-sizing: border-box;
font-family: sans-serif;
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
}
footer a {
padding: 1em;
display: inline-block;
}
</style>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-50610215-6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-50610215-6');
</script>
</head>
<body>
<main>
<div id="status">Installing Service Worker...</div>
</main>
<footer>
<a href="../">More Examples</a>
<a href="https://github.com/AnthumChris/fetch-progress-indicators">View on GitHub</a>
</footer>
<script>
'use strict'
var elStatus = document.getElementById('status');
// Ensure unsupported browsers don't load invalid JS (e.g. ES6, Promises), compile successfully, and show errors
if (!(navigator.serviceWorker && window.fetch && window.ReadableStream)) {
status('Service Worker, Fetch, or ReadableStream API is not supported in this browser.<p>This demo cannot function without them.');
} else {
var script = document.createElement('script');
script.src = 'supported-browser-install.js';
document.body.append(script);
}
function status(text) {
elStatus.innerHTML = text;
}
</script>
</body>
</html>
================================================
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('<p>Service Worker is installed and not functioning as intended.<p>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
================================================
<!-- This page should not be accessed directly and should be served by Service Worker. -->
<html>
<head>
<meta charset="utf8">
<title>Basic Service Worker Progress Indicator</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="../favicon.ico" type="image/png" />
<meta name="description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch">
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@AnthumChris" />
<meta name="og:title" content="Progress Indicators using Streams, Service Workers, and Fetch" />
<meta name="twitter:title" content="Progress Indicators using Streams, Service Workers, and Fetch" />
<meta name="og:description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch" />
<meta name="twitter:description" content="Examples for showing progress indicators with Streams, Service Workers, and Fetch" />
<meta name="og:image" content="https://fetch-progress.anthum.com/images/logo-streams-300.png" />
<meta name="twitter:image" content="https://fetch-progress.anthum.com/images/logo-streams-300.png" />
<style>
html, body {
padding: 0;
margin: 0;
height: 100%;
}
body {
text-align: center;
font-size: 16px;
padding: 0 0 1rem 0;
box-sizing: border-box;
font-family: sans-serif;
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
}
.image {
margin: 0 auto 1.5rem auto;
display: inline-block;
width: 100%;
max-width: 600px;
position: relative;
overflow: hidden;
box-shadow: 0 0 10px 0 #ccc;
}
.image:before {
display: block;
content: "";
width: 100%;
padding-top: 52.5%; /* 40:21 aspect ratio */
}
.image img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
}
.notice {
display: none;
color: #f00;
margin-bottom: 1em;
}
p {
color: #333;
padding: 0 1em;
text-align: left;
line-height: 1.5em;
max-width: 600px;
margin: 0 auto 2em auto;
}
button {
display: block;
margin: 0 auto 2rem auto;
}
footer {
margin-top: 1em;
}
footer a {
padding: 1em;
display: inline-block;
}
@media (min-width: 700px) {
main {
margin-top: 3rem;
}
}
</style>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-50610215-6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-50610215-6');
</script>
</head>
<body>
<main>
<div class="image"><img id="img" onerror="status('Error loading image. See console for details.')" onload="status('Done')" /></div>
<div id="status"> </div>
<h1 id="progress"> </h1>
<div class="notice" id="new-service-worker-installed">Service Worker updated. Please reload.</div>
<button onclick="window.location.reload()">Reload</button>
<p>
This example uses a Service Worker and <code>ReadableStream</code> to simultaneously show download progress of an <code><img></code> tag.
Unlike the <code>fetch()</code> examples, the image renders progressively with the browser's native loading behavior.
</p>
</main>
<footer>
<a href="../">More Examples</a>
<a href="https://github.com/AnthumChris/fetch-progress-indicators">View on GitHub</a>
</footer>
<script>
const elStatus = document.getElementById('status');
const elProgress = document.getElementById('progress');
const elImg = document.getElementById('img');
navigator.serviceWorker.addEventListener('message', event => progress(event.data));
loadImage();
navigator.serviceWorker.register('sw-simple.js')
.then(reg => {
// notify new user if an updated SW was installed.
reg.onupdatefound = function() {
const newSw = reg.installing;
newSw.onstatechange = function() {
if (newSw.state === 'installed') {
document.getElementById('new-service-worker-installed').style.display='block';
}
};
};
})
function loadImage() {
status('loading inline <code><img></code>');
// elImg.src = 'http://localhost/300kbps/images/sunrise-baseline.jpg?requestId=1';
// elImg.src = 'http://localhost/30kbps/images/sunrise-baseline.jpg?requestId=1';
elImg.src = 'https://fetch-progress.anthum.com/30kbps/images/sunrise-baseline.jpg?requestId=1';
}
function progress({loaded, total}) {
elProgress.innerHTML = Math.round(loaded/total*100)+'%';
}
function status(text) {
elStatus.innerHTML = text;
}
</script>
</body>
</html>
================================================
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
================================================
<html>
<head>
<meta charset="utf8">
<title>Abort Event Detection</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="../favicon.ico" type="image/png" />
<style>
body {
font-size: 16px;
font-family: sans-serif;
padding: 2em;
}
img {
max-width: 100%;
margin-top: 1em;
box-shadow: 0 0 10px 0 #ccc;
}
#konsole {
position: fixed;
bottom: 0;
right: 0;
padding: 8px;
background: rgba(0,0,0,.8);
color: lime;
font-size: 12px;
}
#konsole > div {
padding: 2px 0;
}
</style>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-50610215-6"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-50610215-6');
</script>
</head>
<body>
<p>
This page attempts to detect the <code>abort</code> event. Type ESC or press browser's stop button before image download completes
</p>
<img id="img" src="https://fetch-progress.anthum.com/10kbps/images/sunrise-baseline.jpg" />
<div id="konsole"></div>
<script>
var img = document.getElementById('img');
var konsole = document.getElementById('konsole');
function bindEvents(object, events) {
for (var i=0; i<events.length; i++) {
var eventName = events[i];
object.addEventListener(eventName, function(event) {
var objString = object.toString().replace(/object /, '');
console.log(event.type, objString)
var msg = document.createElement('div');
msg.innerHTML = event.type+' - '+objString;
konsole.appendChild(msg);
});
}
}
var events = [
'abort',
'afterprint',
'beforeprint',
'beforeunload',
'blur',
'cancel',
'canplay',
'canplaythrough',
'change',
'click',
'close',
'connect',
'contextmenu',
'copy',
'ctextmenu',
'cuechange',
'cut',
'DOMContentLoaded',
'duratichange',
'emptied',
'ended',
'error',
'focus',
'gotpointercapture',
'input',
'invalid',
'load',
'loadeddata',
'loadedmetadata',
'loadend',
'loadstart',
'lostpointercapture',
'message',
'messageerror',
'offline',
'online',
'open',
'pagehide',
'pageshow',
'paste',
'popstate',
'progress',
'ratechange',
'readystatechange',
'rejectionhandled',
'reset',
'resize',
'search',
'securitypolicyviolation',
'seeked',
'seeking',
'select',
'selectstart',
'stalled',
'storage',
'submit',
'suspend',
'toggle',
'unhandledrejection',
'unload',
'waiting',
// 'auxclick',
// 'beforecopy',
// 'beforecut',
// 'beforepaste',
// 'copy',
// 'cut',
// 'dblclick',
// 'drag',
// 'dragend',
// 'dragenter',
// 'dragleave',
// 'dragover',
// 'dragstart',
// 'drop',
// 'hashchange',
// 'keydown',
// 'keypress',
// 'keyup',
// 'languagechange',
// 'mousedown',
// 'mouseenter',
// 'mouseleave',
// 'mousemove',
// 'mouseout',
// 'mouseover',
// 'mouseup',
// 'mousewheel',
// 'paste',
// 'pause',
// 'play',
// 'playing',
// 'pointercancel',
// 'pointerdown',
// 'pointerenter',
// 'pointerleave',
// 'pointermove',
// 'pointerout',
// 'pointerover',
// 'pointerup',
// 'scroll',
// 'timeupdate',
// 'volumechange',
// 'webkitfullscreenchange',
// 'webkitfullscreenerror',
// 'wheel',
];
bindEvents(window, events);
bindEvents(document, events);
bindEvents(img, events);
</script>
</body>
</html>
================================================
FILE: test/sw-response-with-url-fragments/index.html
================================================
<html>
<head>
<meta charset="utf8">
<title>Service Worker Response Debugger</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="../favicon.ico" type="image/png" />
<style>
body {
font-size: 16px;
font-family: sans-serif;
padding: 2em;
margin: 0 auto;
max-width: 650px;
font-weight: 300;
}
h4 {
margin: 2em 0 .5em 0;
}
.image {
display: inline-block;
width: 100%;
max-width: 36px;
position: relative;
overflow: hidden;
margin-right: .5em;
vertical-align: middle;
}
.image:before {
display: block;
content: "";
width: 100%;
padding-top: 100%; /* 1:1; aspect ratio */
}
.image img {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
}
.test {
margin-top: .5em;
}
label {
font-family: monospace;
font-size: 12px;
}
label:before {
content: 'URI: ';
color: #999;
}
p {
line-height: 1.5em;
}
</style>
</head>
<body>
<p>A Service Worker installs/updates and refreshes the page. It then intercepts "sw-image.svg" <code>img</code> 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</p>
<h4>respondWith() fetch()</h4>
<div class="test"><div class="image"><img src="sw-image.svg?fetch1" /></div><label>sw-image.svg?fetch1</label></div>
<div class="test"><div class="image"><img src="sw-image.svg?fetch2#" /></div><label>sw-image.svg?fetch2#</label></div>
<div class="test"><div class="image"><img src="sw-image.svg?fetch3#value" /></div><label>sw-image.svg?fetch3#value</label></div>
<h4>respondWith() new Response(blob)</code></h4>
<div class="test"><div class="image"><img src="sw-image.svg?blob-response1" /></div><label>sw-image.svg?blob-response1</label></div>
<div class="test"><div class="image"><img src="sw-image.svg?blob-response2#" /></div><label>sw-image.svg?blob-response2#</label></div>
<div class="test"><div class="image"><img src="sw-image.svg?blob-response3#value" /></div><label>sw-image.svg?blob-response3#value</label></div>
<h4>respondWith() new Response(new ReadableStream())</code></h4>
<div class="test"><div class="image"><img src="sw-image.svg?stream-response1" /></div><label>sw-image.svg?stream-response1</label></div>
<div class="test"><div class="image"><img src="sw-image.svg?stream-response2#" /></div><label>sw-image.svg?stream-response2#</label></div>
<div class="test"><div class="image"><img src="sw-image.svg?stream-response3#value" /></div><label>sw-image.svg?stream-response3#value</label></div>
<script>
// Install SW and refresh page if needed
navigator.serviceWorker.register('sw-new-response.js')
.then(reg => {
console.log('SW registered')
reg.onupdatefound = function() {
const newSw = reg.installing;
newSw.onstatechange = function() {
if (newSw.state === 'installed') {
console.log('SW installed/updated')
setTimeout(function() {
window.location.reload();
}, 0);
}
};
};
})
</script>
</body>
</html>
================================================
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<imgBytes.length; i++) {
imgByteArray[i] = imgBytes.charCodeAt(i);
}
const blob = new Blob([imgByteArray], {type: 'image/svg+xml'});
self.addEventListener('install', event => {
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'
}
}
)
)
}
}
})
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
SYMBOL INDEX (16 symbols across 5 files)
FILE: fetch-basic/supported-browser.js
method start (line 29) | start(controller) {
function progress (line 62) | function progress({loaded, total}) {
FILE: fetch-enhanced/supported-browser.js
class ProgressReportFetcher (line 3) | class ProgressReportFetcher {
method constructor (line 4) | constructor(onProgress = function() {}) {
method fetch (line 9) | fetch(input, init = {}) {
method cancel (line 83) | cancel() {
function downloadDone (line 104) | function downloadDone(url) {
function startDownload (line 113) | function startDownload() {
function stopDownload (line 148) | function stopDownload() {
function showError (line 158) | function showError(error) {
function updateDownloadProgress (line 168) | function updateDownloadProgress({loaded, total}) {
FILE: sw-basic/supported-browser-install.js
function onward (line 19) | function onward() {
FILE: sw-basic/sw-simple.js
function fetchWithProgressMonitor (line 24) | function fetchWithProgressMonitor(event) {
function respondWithProgressMonitor (line 41) | function respondWithProgressMonitor(clientId, response) {
function dispatchProgress (line 107) | function dispatchProgress({client, loaded, total}) {
FILE: test/sw-response-with-url-fragments/sw-new-response.js
method start (line 36) | start(controller) {
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (55K chars).
[
{
"path": ".conf/nginx/fetch-progress-headers.conf",
"chars": 606,
"preview": "## Nginx Lua module must be installed https://docs.nginx.com/nginx/admin-guide/dynamic-modules/lua/\n## https://github.c"
},
{
"path": ".conf/nginx/fetch-progress-throttle.conf",
"chars": 226,
"preview": "rewrite .*/((images|test)/.*) /$1 break;\n\ninclude conf.d/includes/fetch-progress-headers.conf;\n\n# Force browser to alway"
},
{
"path": ".conf/nginx/server.conf",
"chars": 1583,
"preview": "# HTTPs Redirect\nserver {\n listen 80;\n listen [::]:80;\n server_name fetch-progress.anthum.com;\n\n return 301 https://"
},
{
"path": ".gitignore",
"chars": 11,
"preview": "/test/data\n"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2018 Anthum, Inc\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 4993,
"preview": "Examples for showing progress bars and progress indicators for `fetch()`. Uses the [Fetch API](https://developer.mozill"
},
{
"path": "fetch-basic/index.html",
"chars": 3087,
"preview": "<html>\n <head>\n <meta charset=\"utf8\">\n <title>Basic Fetch() Progress Indicator</title>\n <meta name=\"viewport\" "
},
{
"path": "fetch-basic/supported-browser.js",
"chars": 1856,
"preview": "'use strict'\n\nconst elProgress = document.getElementById('progress');\n\nstatus('downloading with fetch()...');\nfetch('htt"
},
{
"path": "fetch-enhanced/app.css",
"chars": 2655,
"preview": "html, body {\n padding: 0;\n margin: 0;\n height: 100%;\n}\nbody {\n font-size: 16px;\n font-family: sans-serif;\n backgr"
},
{
"path": "fetch-enhanced/index.html",
"chars": 3362,
"preview": "<html>\n <head>\n <meta charset=\"utf8\">\n <title>Enhanced Fetch() Progress Indicator</title>\n <meta name=\"viewpor"
},
{
"path": "fetch-enhanced/supported-browser.js",
"chars": 5739,
"preview": "'use strict'\n\nclass ProgressReportFetcher {\n constructor(onProgress = function() {}) {\n this.onProgress = onProgress"
},
{
"path": "index.html",
"chars": 3671,
"preview": "<html>\n <head>\n <meta charset=\"utf8\">\n <title>Fetch & Streams API Progress Indicators</title>\n <meta name="
},
{
"path": "sw-basic/app.js",
"chars": 210,
"preview": "if ('serviceWorker' in navigator) {\n navigator.serviceWorker.register('sw-fetch-progress.js');\n navigator.serviceWorke"
},
{
"path": "sw-basic/index.html",
"chars": 2052,
"preview": "<!-- This page loads Service Worker and won't be shown again after it refreshes for SW interception. -->\n\n<html>\n <head"
},
{
"path": "sw-basic/supported-browser-install.js",
"chars": 701,
"preview": "navigator.serviceWorker.register('sw-simple.js')\n.then(reg => {\n if (reg.installing) {\n const sw = reg.installing ||"
},
{
"path": "sw-basic/sw-installed.html",
"chars": 5231,
"preview": "<!-- This page should not be accessed directly and should be served by Service Worker. -->\n\n<html>\n <head>\n <meta c"
},
{
"path": "sw-basic/sw-simple.js",
"chars": 3844,
"preview": "// evaluate progress on *?requestId=* URLs only\nconst progressIndicatorUrls = /\\?requestId=/i;\n\n// always install update"
},
{
"path": "test/abort-event.html",
"chars": 4388,
"preview": "<html>\n <head>\n <meta charset=\"utf8\">\n <title>Abort Event Detection</title>\n <meta name=\"viewport\" content=\"wi"
},
{
"path": "test/sw-response-with-url-fragments/index.html",
"chars": 3556,
"preview": "<html>\n <head>\n <meta charset=\"utf8\">\n <title>Service Worker Response Debugger</title>\n <meta name=\"viewport\" "
},
{
"path": "test/sw-response-with-url-fragments/sw-new-response.js",
"chars": 2814,
"preview": "// base64 SVG image\nconst img64 = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gU3ZnIFZlY3RvciBJY29ucyA6I"
}
]
About this extraction
This page contains the full source code of the AnthumChris/fetch-progress-indicators GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (50.4 KB), approximately 13.8k tokens, and a symbol index with 16 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.