• Stars
    star
    281
  • Rank 147,023 (Top 3 %)
  • Language
    Shell
  • License
    Apache License 2.0
  • Created over 8 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

API proposal for background downloading/uploading

This is a proposal for a background-fetching API, to handle large upload/downloads without requiring an origin's clients (pages/workers) to be running throughout the job.

The problem

A service worker is capable of fetching and caching assets, the size of which is restricted only by origin storage. However, if the user navigates away from the site or closes the browser, the service worker is likely to be killed. This can happen even if there's a pending promise passed to extendableEvent.waitUntil - if it hasn't resolved within a few minutes the browser may consider it an abuse of service worker and kill the process.

This is excellent for battery and privacy, but it makes it difficult to download and cache large assets such as podcasts and movies, and upload video and images.

This spec aims to solve the long-running fetch case, without impacting battery life or privacy beyond that of a long-running download.

Features

  • Allow fetches (requests & responses) to continue even if the user closes all windows & workers to the origin.
  • Allow a single job to involve many requests, as defined by the app.
  • Allow the browser/OS to show UI to indicate the progress of that job, and allow the user to pause/abort.
  • Allow the browser/OS to deal with poor connectivity by pausing/resuming the download.
  • Allow the app to react to success/failure of the job, perhaps by caching the results.
  • Allow access to background-fetched resources as they fetch.

API Design

Starting a background fetch

const registration = await navigator.serviceWorker.ready;
const bgFetchReg = await registration.backgroundFetch.fetch(id, requests, options);
  • id - a unique identifier for this background fetch.
  • requests - a sequence of URLs or Request objects.
  • options - an object containing any of:
    • icons - A sequence of icon definitions.
    • title - Something descriptive to show in UI, such as "Uploading 'Holiday in Rome'" or "Downloading 'Catastrophe season 2 episode 1'".
    • downloadTotal - The total unencoded download size. This allows the UI to tell the user how big the total of the resources is. If omitted, the UI will be in a non-determinate state.

backgroundFetch.fetch will reject if:

  • The user does not want background downloads, which may be a origin/browser/OS level setting.
  • If there's already a registered background fetch job associated with registration identified by id.
  • Any of the requests have mode no-cors.
  • The browser fails to store the requests and their bodies.
  • downloadTotal suggests there isn't enough quota to complete the job.

The operation will later fail if:

  • A fetch with a non-GET request fails. There's no HTTP mechanism to resume uploads.
  • Fetch rejects when the user isn't offline. As in, CORS failure, MIX issues, CSP issue etc etc.
  • The unencoded download size exceeds the provided downloadTotal.
  • The server provides an unexpected partial response.
  • Quota is exceeded.
  • A response does not have an ok status.

If downloadTotal is exceeded, the operation fails immediately. Otherwise, the other fetches will be given a chance to settle. This means if the user is uploading 100 photos, 99 won't be aborted just because one fails. The operation as a whole will still be considered a failure, but the app can communicate what happened to the user.

bgFetchReg has the following:

  • id - identifier string.
  • uploadTotal - total bytes to send.
  • uploaded - bytes sent so far.
  • downloadTotal - as provided.
  • downloaded - bytes stored so far.
  • result - "", "success", "failure".
  • failureReason - "", "aborted", "bad-status", "fetch-error", "quota-exceeded", "download-total-exceeded".
  • recordsAvailable - Does the underlying request/response data still exist? It's removed once the operation is complete.
  • activeFetches - provides access to the in-progress fetches.
  • onprogress - Event when the above properties change.
  • abort() - abort the whole background fetch job. This returns a promise that resolves with a boolean, which is true if the operation successfully aborted.
  • match(request, options) - Access one of the fetch records.
  • matchAll(request, options) - Access some of the fetch records.

Getting an instance of a background fetch

const registration = await navigator.serviceWorker.ready;
const bgFetchReg = await registration.backgroundFetch.get(id);

If no job with the identifier id exists, get resolves with undefined.

Getting all background fetches

const registration = await navigator.serviceWorker.ready;
const ids = await registration.backgroundFetch.getIds();

…where ids is a sequence of unique identifier strings.

Background fetch records

const bgFetchReg = await registration.backgroundFetch.get(id);
const record = bgFetchReg.match(request);

record has the following:

  • request. A Request.
  • responseReady. A promise for a Response. This will reject if the fetch fails.

Reacting to success

Fires in the service worker if all responses in a background fetch were successfully & fully read, and all status codes were ok.

addEventListener('backgroundfetchsuccess', bgFetchEvent => {
  // …
});

bgFetchEvent extends ExtendableEvent, with the following additional members:

  • registration - The background fetch registration.
  • updateUI({ title, icons }) - update the UI, eg "Uploaded 'Holiday in Rome'", "Downloaded 'Catastrophe season 2 episode 1'", or "Level 5 ready to play!".

Once this event is fired, the background fetch job is no longer stored against the registration, so backgroundFetch.get(bgFetchEvent.id) will resolve with undefined.

Once this has completed (including promises passed to waitUntil), recordsAvailable becomes false, and the requests/responses can no longer be accessed.

Reacting to failure

As backgroundfetchsuccess, but one or more of the fetches encountered an error.

addEventListener('backgroundfetchfail', bgFetchEvent => {
  // …
});

Aside from the event name, the details are the same as backgroundfetchsuccess.

Reacting to abort

If a background fetch job is aborted, either by the user, or by the developer calling abort() on the background fetch job, the following event is fired in the service worker:

addEventListener('backgroundfetchabort', bgFetchAbortEvent => {
  // …
});

bgFetchAbortEvent extends ExtendableEvent, with the following additional members:

  • registration - The background fetch registration.

The rest is as backgroundfetchsuccess.

Reacting to click

If the UI representing a background fetch job is clicked, either during or after the job, the following event is fired in the service worker:

addEventListener('backgroundfetchclick', bgFetchClickEvent => {
  // …
});
  • registration - The background fetch registration.

Since this is a user interaction event, developers can call clients.openWindow in response.

The rest is as backgroundfetchsuccess.

Possible UI

Background fetches will be immediately visible using a UI of the browser's choosing. On Android this is likely to be a sticky notification displaying:

  • The origin of the site.
  • The chosen icon.
  • The title.
  • A progress bar.
  • Buttons to pause/abort the job.
  • Potentially a way to prevent the origin starting any more background downloads.

If aborted by the user, the notification is likely to disappear immediately. If aborted via code, the notification will remain in an "ended" state.

Once ended, the progress bar will be replaced with an indication of how much data was transferred. The button to pause/abort the job will no longer be there. The notification will no longer be sticky.

If the job has ended, clicking the notification may also close/hide it (in addition to firing the event).

Quota usage

The background fetch requests & in-progress responses can be accessed at any time until the backgroundfetchsuccess, backgroundfetchfail, or backgroundfetchabort event end, so they count against origin quota.

Lifecycle

The background fetch job is linked to the service worker registration. If the service worker is unregistered, background fetches will be aborted (without firing events) and its storage purged.

This means the feature may be used in "private browsing modes" that use a temporary profile, as the fetches will be cancelled and purged along with the service worker registrations.

Security & privacy

Some browsers can already start downloads without user interaction, but they're easily abortable. We're following the same pattern here.

Background fetch may happen as the result of other background operations, such as push messages. In this case the background fetch may start in a paused state, effectively asking the user permission to continue.

The icon and title of the background fetch are controllable by the origin. Hopefully the UI can make clear which parts are under the site's control, and which parts are under the browser's control (origin, data used, abort/pause). There's some prior art here with notifications.

Background fetches are limited to CORS, to avoid opaque responses taking up origin quota.

Relation to one-off background sync

Background-fetch is intended to be very user-visible, via OS-level UI such as a persistent notification, as such background-sync remains a better option for non-massive transfers such as IM messages.

Examples

Downloading a movie

Movies are either one large file (+ extra things like metadata and artwork), or 1000s of chunks.

In the page:

downloadButton.addEventListener('click', async () => {
  try {
    const movieData = getMovieDataSomehow();
    const reg = await navigator.serviceWorker.ready;
    const bgFetch = await reg.backgroundFetch.fetch(`movie-${movieData.id}`, movieData.urls, {
      icons: movieData.icons,
      title: `Downloading ${movieData.title}`,
      downloadTotal: movieData.downloadTotal
    });
    // Update the UI.

    bgFetch.addEventListener('progress', () => {
      // Update the UI some more.
    });
  } catch (err) {
    // Display an error to the user
  }
});

In the service worker:

addEventListener('backgroundfetchsuccess', (event) => {
  event.waitUntil(async function() {
    // Copy the fetches into a cache:
    try {
      const cache = await caches.open(event.registration.id);
      const records = await event.registration.matchAll();
      const promises = records.map(async (record) => {
        const response = await record.responseReady;
        await cache.put(record.request, response);
      });
      await Promise.all(promises);
      const movieData = await getMovieDataSomehow(event.registration.id);
      await event.updateUI({ title: `${movieData.title} downloaded!` });
    } catch (err) {
      event.updateUI({ title: `Movie download failed` });
    }
  }());
});

// There's a lot of this that's copied from 'backgroundfetchsuccess', but I've avoided
// abstracting it for this example.
addEventListener('backgroundfetchfail', (event) => {
  event.waitUntil(async function() {
    // Store everything successful, maybe we can just refetch the bits that failed
    try {
      const cache = await caches.open(event.registration.id);
      const records = await event.registration.matchAll();
      const promises = records.map(async (record) => {
        const response = await record.responseReady.catch(() => undefined);
        if (response && response.ok) {
          await cache.put(record.request, response);
        }
      });
      await Promise.all(promises);
    } finally {
      const movieData = await getMovieDataSomehow(event.registration.id);
      await event.updateUI({ title: `${movieData.title} download failed.` });
    }
  }());
});

addEventListener('backgroundfetchclick', (event) => {
  event.waitUntil(async function() {
    const movieData = await getMovieDataSomehow(event.registration.id);
    clients.openWindow(movieData.pageUrl);
  }());
});

// Allow the data to be fetched while it's being downloaded:
addEventListener('fetch', (event) => {
  if (isMovieFetch(event)) {
    event.respondWith(async function() {
      const cachedResponse = await caches.match(event.request);
      if (cachedResponse) return cachedResponse;

      // Maybe it's mid-download?
      const movieData = getMovieDataSomehow(event.request);
      const bgFetch = await registration.backgroundFetch.get(`movie-${movieData.id}`);

      if (bgFetch) {
        const record = await bgFetch.match(event.request);
        if (record) return record.responseReady;
      }

      return fetch(event.request);
    }());
  }
  // …
});

Uploading photos

In the page:

uploadButton.addEventListener('click', async () => {
  try {
    // Create the requests:
    const galleryId = createGalleryIdSomehow();
    const photos = getPhotoFilesSomehow();
    const requests = photos.map((photo) => {
      const body = new FormData();
      body.set('gallery', galleryId);
      body.set('photo', photo);

      return new Request('/upload-photo', {
        body,
        method: 'POST',
        credentials: 'include',
      });
    });

    const reg = await navigator.serviceWorker.ready;
    const bgFetch = await reg.backgroundFetch.fetch(`photo-upload-${galleryId}`, requests, {
      icons: getAppIconsSomehow(),
      title: `Uploading photos`,
    });

    // Update the UI.

    bgFetch.addEventListener('progress', () => {
      // Update the UI some more.
    });
  } catch (err) {
    // Display an error to the user
  }
});

In the service worker:

addEventListener('backgroundfetchsuccess', (event) => {
  event.waitUntil(async function() {
    const galleryId = getGalleryIdSomehow(event.registration.id);
    await event.updateUI({ title: `Photos uploaded` });

    // The gallery is complete, so we can show it to the user's friends:
    await fetch('/enable-gallery', {
      method: 'POST',
      body: new URLSearchParams({ id: galleryId }),
    })
  }());
});

addEventListener('backgroundfetchfail', (event) => {
  event.waitUntil(async function() {
    const records = await event.registration.matchAll();
    let failed = 0;

    for (const record of records) {
      const response = await record.responseReady.catch(() => undefined);
      if (response && response.ok) continue;
      failed++;
    }

    if (successful) {
      event.updateUI({ title: `${failed}/${records.length} uploads failed` });
    }
  }());
});

addEventListener('backgroundfetchclick', (event) => {
  event.waitUntil(async function() {
    const galleryId = getGalleryIdSomehow(event.registration.id);
    clients.openWindow(`/galleries/${galleryId}`);
  }());
});

More Repositories

1

webcomponents

Web Components specifications
HTML
4,360
star
2

import-maps

How to control the behavior of JavaScript imports
JavaScript
2,705
star
3

virtual-scroller

1,998
star
4

focus-visible

Polyfill for `:focus-visible`
JavaScript
1,607
star
5

webusb

Connecting hardware to the web.
Bikeshed
1,310
star
6

webpackage

Web packaging format
Go
1,231
star
7

EventListenerOptions

An extension to the DOM event pattern to allow authors to disable support for preventDefault
JavaScript
1,166
star
8

portals

A proposal for enabling seamless navigations between sites or pages
HTML
946
star
9

floc

This proposal has been replaced by the Topics API.
Makefile
934
star
10

inert

Polyfill for the inert attribute and property.
JavaScript
920
star
11

scheduling-apis

APIs for scheduling and controlling prioritized tasks.
HTML
909
star
12

view-transitions

811
star
13

file-system-access

Expose the file system on the user’s device, so Web apps can interoperate with the user’s native applications.
Bikeshed
658
star
14

background-sync

A design and spec for ServiceWorker-based background synchronization
HTML
639
star
15

ua-client-hints

Wouldn't it be nice if `User-Agent` was a (set of) client hints?
Bikeshed
590
star
16

scroll-to-text-fragment

Proposal to allow specifying a text snippet in a URL fragment
HTML
586
star
17

observable

Observable API proposal
Bikeshed
582
star
18

aom

Accessibility Object Model
HTML
567
star
19

kv-storage

[On hold] A proposal for an async key/value storage API for the web
550
star
20

turtledove

TURTLEDOVE
Bikeshed
526
star
21

navigation-api

The new navigation API provides a new interface for navigations and session history, with a focus on single-page application navigations.
Makefile
486
star
22

webmonetization

Proposed Web Monetization standard
HTML
461
star
23

trust-token-api

Trust Token API
Bikeshed
421
star
24

attribution-reporting-api

Attribution Reporting API
Bikeshed
360
star
25

direct-sockets

Direct Sockets API for the web platform
HTML
329
star
26

shape-detection-api

Detection of shapes (faces, QR codes) in images
Bikeshed
304
star
27

display-locking

A repository for the Display Locking spec
HTML
297
star
28

dbsc

Bikeshed
297
star
29

first-party-sets

Bikeshed
280
star
30

serial

Serial ports API for the platform.
HTML
256
star
31

resize-observer

This repository is no longer active. ResizeObserver has moved out of WICG into
HTML
255
star
32

priority-hints

A browser API to enable developers signal the priorities of the resources they need to download.
Bikeshed
249
star
33

sanitizer-api

Bikeshed
227
star
34

is-input-pending

HTML
221
star
35

proposals

A home for well-formed proposed incubations for the web platform. All proposals welcome.
216
star
36

spatial-navigation

Directional focus navigation with arrow keys
JavaScript
212
star
37

js-self-profiling

Proposal for a programmable JS profiling API for collecting JS profiles from real end-user environments
HTML
197
star
38

cq-usecases

Use cases and requirements for standardizing element queries.
HTML
184
star
39

isolated-web-apps

Repository for explainers and other documents related to the Isolated Web Apps proposal.
Bikeshed
182
star
40

visual-viewport

A proposal to add explicit APIs to the Web for querying and setting the visual viewport
HTML
177
star
41

interventions

A place for browsers and web developers to collaborate on user agent interventions.
176
star
42

frame-timing

Frame Timing API
HTML
170
star
43

layout-instability

A proposal for a Layout Instability specification
Makefile
158
star
44

page-lifecycle

Lifecycle API to support system initiated discarding and freezing
HTML
154
star
45

nav-speculation

Proposal to enable privacy-enhanced preloading
HTML
154
star
46

speech-api

Web Speech API
Bikeshed
145
star
47

cookie-store

Asynchronous access to cookies from JavaScript
Bikeshed
143
star
48

construct-stylesheets

API for constructing CSS stylesheet objects
Bikeshed
137
star
49

webhid

Web API for accessing Human Interface Devices (HID)
HTML
137
star
50

color-api

A proposal and draft spec for a Color object for the Web Platform, loosely influenced by the Color.js work. Heavily WIP, if you landed here randomly, please move along.
HTML
132
star
51

fenced-frame

Proposal for a strong boundary between a page and its embedded content
Bikeshed
126
star
52

devtools-protocol

DevTools Protocol
JavaScript
120
star
53

sms-one-time-codes

A way to format SMS messages for use with browser autofill features such as HTML’s autocomplete=one-time-code.
Makefile
111
star
54

bundle-preloading

Bundles of multiple resources, to improve loading JS and the Web.
HTML
105
star
55

translation-api

A proposal for translator and language detector APIs
Bikeshed
104
star
56

privacy-preserving-ads

Privacy-Preserving Ads
HCL
100
star
57

manifest-incubations

Before install prompt API for installing web applications
HTML
99
star
58

window-controls-overlay

HTML
97
star
59

netinfo

HTML
95
star
60

compression-dictionary-transport

94
star
61

intrinsicsize-attribute

Proposal to add an intrinsicsize attribute to media elements
93
star
62

animation-worklet

🚫 Old repository for AnimationWorklet specification ➑️ New repository: https://github.com/w3c/css-houdini-drafts
Makefile
92
star
63

container-queries

HTML
91
star
64

local-peer-to-peer

↔️ Proposal for local communication between browsers without the aid of a server.
Bikeshed
90
star
65

shared-storage

Explainer for proposed web platform Shared Storage API
Bikeshed
89
star
66

async-append

A way to create DOM and add it to the document without blocking the main thread.
HTML
87
star
67

indexed-db-observers

Prototyping and discussion around indexeddb observers.
WebIDL
84
star
68

canvas-formatted-text

HTML
82
star
69

file-handling

API for web applications to handle files
82
star
70

canvas-color-space

Proposed web platform feature to add color management, wide gamut and high bit-depth support to the <canvas> element.
79
star
71

local-font-access

Web API for enumerating fonts on the local system
Bikeshed
77
star
72

performance-measure-memory

performance.measureMemory API
HTML
77
star
73

digital-credentials

Digital Credentials, like driver's licenses
HTML
77
star
74

handwriting-recognition

Handwriting Recognition Web API Proposal
Bikeshed
75
star
75

web-app-launch

Web App Launch Handler
HTML
75
star
76

pwa-url-handler

72
star
77

ContentPerformancePolicy

A set of policies that a site guarantees to adhere to, browsers enforce, and embedders can count on.
HTML
72
star
78

starter-kit

A simple starter kit for incubations
JavaScript
72
star
79

css-parser-api

This is the repo where the CSS Houdini parser API will be worked on
HTML
71
star
80

close-watcher

A web API proposal for watching for close requests (e.g. Esc, Android back button, ...)
Makefile
71
star
81

eyedropper-api

HTML
69
star
82

idle-detection

A proposal for an idle detection and notification API for the web
Bikeshed
67
star
83

storage-foundation-api-explainer

Explainer showcasing a new web storage API, NativeIO
65
star
84

video-editing

65
star
85

uuid

UUID V4
63
star
86

client-hints-infrastructure

Specification for the Client Hints infrastructure - privacy preserving proactive content negotiation
Bikeshed
61
star
87

sparrow

60
star
88

private-network-access

HTML
58
star
89

element-timing

A proposal for an Element Timing specification.
Bikeshed
57
star
90

document-picture-in-picture

Bikeshed
56
star
91

video-rvfc

video.requestVideoFrameCallback() incubation
HTML
53
star
92

time-to-interactive

Repository for hosting TTI specification and discussions around it.
52
star
93

digital-goods

Bikeshed
50
star
94

soft-navigations

Heuristics to detect Single Page Apps soft navigations
Bikeshed
46
star
95

raw-clipboard-access

An explainer for the Raw Clipboard Access feature
44
star
96

storage-buckets

API proposal for managing multiple storage buckets
Bikeshed
43
star
97

pending-beacon

A better beaconing API
Bikeshed
43
star
98

admin

πŸ‘‹ Ask your questions here! πŸ‘‹
HTML
42
star
99

web-smart-card

Repository for the Web Smart Card Explainer
HTML
42
star
100

web-preferences-api

The Web Preference API aims to provide a way for sites to override the value for a given user preference (e.g. color-scheme preference) in a way that fully integrates with existing Web APIs.
Bikeshed
41
star