Coder Social home page Coder Social logo

background-fetch's Introduction

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}`);
  }());
});

background-fetch's People

Contributors

autokagami avatar beaufortfrancois avatar beverloo avatar cynthia avatar foolip avatar jakearchibald avatar marcoscaceres avatar miketaylr avatar philnash avatar rayankans avatar travisleithead avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

background-fetch's Issues

Be optimal with quota usage

Background-fetching 1gb of data then adding it to the cache API should only use 1gb of quota.

This should work since responses can only be read once, but the spec should be explicit about it.

How to handle redirects

It feels like we should reject on "manual" redirect modes to avoid dealing with opaque resources.

However, what about "follow"? If we hit a redirect, then need to resume the download, does the request go to the destination url or the original target? I suspect it's the target, but need to test what downloads do today.

What if you drain a request?

Since you give access to the Request object, what if someone drains it? Or is it a clone of sorts? Wouldn't that be rather wasteful?

CORS only?

Restricting to CORS only makes it easier to deal with 404s etc. It also avoids adding another API where opaque responses use quota.

Naming of BackgroundFetch(Active,Settled)Fetches

https://wicg.github.io/background-fetch/#background-fetch-registration

A BackgroundFetchRegistration has a sequence of active fetches, whereas two events have a sequence of settled fetches. Each entry in such a sequence, of type BackgroundFetchFetches, contains a single request/response(Ready) pair, while the name implies its multiple.

Could we consider renaming them as follows?

Current Proposed
BackgroundFetchFetches BackgroundFetchRequest
BackgroundFetchActiveFetches BackgroundFetchActiveRequest
BackgroundFetchSettledFetches BackgroundFetchSettledRequest

One concern here is that they describe and contain a request, as opposed to being an actual request. We can stick Info in the name too, but that makes the types even longer. WDYT?

Interfaces should be [SecureContext]

Whilst the APIs implicitly require a secure context since they hang off ServiceWorkerRegistration, the interfaces themselves are still exposed as window.BackgroundFetchManager etc in non-secure contexts.

It would be neater to hide them there by adding [SecureContext]. This matches the consensus in w3c/ServiceWorker#941 which added [SecureContext] to interfaces in the service worker spec (though I notice we haven't yet updated Blink's IDL to match).

Is the download notification clickable?

If the user interacts with the notification should there be a separate event for that?

While the fetch is in progress it could take the user to a "more details" page. When the fetch is complete it could take them to the thing they downloaded/uploaded.

Add "total size" option

Allow devs to signal total size so progress bars can be displayed before headers have arrived for all resources.

"fetches" is different in bgFetchJob and bgFetchEvent

const registration = await navigator.serviceWorker.ready;
const bgFetchJob = await registration.backgroundFetch.get(tag);

Here, bgFetchJob.fetches has properties:

  • request - a Request.
  • responseReady - a promise for a Response. Rejects if the fetch has terminally failed.

The response needs to be behind a promise, as we may not have headers yet. I tried having response which may be undefined, but that turns the object into a kind of snapshot, which doesn't work since the other parts are pretty active.

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

Here, bgFetchEvent.fetches has properties:

We don't need a promise here, as we definitely have a response. However, the difference between this and the other .fetches is weird.

They could become the same object, and just live with an extra await to get the actual response even though it isn't needed, or the objects could be changed a bit more so this difference makes sense.

Deferred background upload of large files

Hi,

I currently work for Kinderly and am scoping out a way to rewrite our current Flash/Air mobile app as a progressive web application. One of the key requirements is the ability to schedule fire-and-forget uploads of large files (photos and videos in addition to other data) when the user is offline, for automatic assumption and resumption when the user returns online. The use case is as follows:

Picture a nursery with a large garden, with one crappy BT Hub from 5 years ago. The manager there bought a bunch of cheap tablets¹ with just WiFi on them and told the staff that they need to use these to take EYFS observations and attach photos and videos to them. Now, in the far end of the garden, the WiFi is nonexistent so the app absolutely must work offline - so far that's not too much of a problem as service workers are already here. Then the staff return the devices to the office, where it can connect to WiFi again. What should happen at this point is that background sync should be initiated, and all those observations with media attached should start being uploaded to the server.

My question is, how much of this use case is covered by the current service worker spec as well as this proposal? On first reading of this example it seems like it may actually be quite difficult to pull off, requiring a request to be fired immediately and then put on hold by the cache. In my scenario, I'd like to be able to store all the media in say IndexedDB, and initiate uploads in response to some event that informs the service worker that it has network again (unsure if I can listen to online/offline events in a service worker, probably not).

¹ For now I'm assuming Android tablets which can run latest Chrome; I have no illusions that a shim Cordova app for iOS won't be necessary

bgFetchJob.activeFetches and bgFetchEvent.fetches could be very large

A bg fetch could be 10,000+ requests & responses (think of a chunked movie). We could be creating ourselves a performance problem we can't later work around.

bgFetchJob.activeFetches doesn't feel like a common thing to use. The only place I use it in the examples is to play a podcast that's in-progress of downloading. Notably I had to create my own matching function that found the single entry I was interested in.

In success events, it's likely that you'll want to add all of bgFetchEvent.fetches to a cache.

In error events, you may wish to inspect which items succeeded & failed, or you might just show a message, so bgFetchFailEvent.fetches isn't always needed.

partial interface BackgroundFetchRegistration {
  readonly attribute BackgroundFetchActiveFetches activeFetches;
}

interface BackgroundFetchActiveFetches {
  // .match works like the cache API
  Promise<BackgroundFetchActiveFetch> match(RequestInfo request, optional CacheQueryOptions options)
  iterable<Promise<BackgroundFetchActiveFetch>>;
}

partial interface BackgroundFetchedEvent : BackgroundFetchEvent {
  readonly attribute BackgroundFetchFulfilledFetches fetches;
}

interface BackgroundFetchFailEvent : BackgroundFetchedEvent {
  readonly attribute BackgroundFetchSettledFetches fetches;
};

interface BackgroundFetchSettledFetches {
  // .match works like the cache API
  Promise<BackgroundFetchSettledFetch> match(RequestInfo request, optional CacheQueryOptions options)
  iterable<Promise<BackgroundFetchSettledFetch>>;
}

interface BackgroundFetchFulfilledFetches : BackgroundFetchSettledFetches {
  // We might not include this for v1
  Promise<void> putAllInCache(Cache cache);
}

I'm a little concerned about using .fetches for two slightly different objects.

Playing a podcast as it's background-fetching

addEventListener('fetch', event => {
  if (isPodcastAudioRequest(event.request)) {
    const podcastId = getPodcastId(event.request);

    event.respondWith(async function() {
      const bgFetchJob = await self.registration.backgroundFetch.get(`podcast-${podcastId}`);

      if (bgFetchJob) {
        // Look for response in fetches
        const activeFetch = await bgFetchJob.activeFetches.match(event.request);
        return activeFetch.responseReady;
      }

      // Else fall back to cache or network
      const response = await caches.match(event.request);
      return response || fetch(event.request);
    }());
  }
});

This example got quite a bit shorter thanks to .match.

Caching a podcast after background fetching

addEventListener('backgroundfetched', event => {
  if (event.id.startsWith('podcast-')) {
    event.waitUntil(async function() {
      // Get podcast by ID
      const podcast = await getPodcast(/podcast-(.*)$/.exec(event.id)[0]);

      // Cache podcasts
      const cache = await caches.open(event.id);
      const promises = [...event.fetches].map(async p => {
        const {request, response} = await p;
        return cache.put(request, response);
      });

      await Promise.all(promises);
      event.updateUI(`Downloaded ${podcast.showName} - ${podcast.episodeName}`);
    }());
  }
});

This got slightly more complicated, but not by much. If add putAllInCache:

addEventListener('backgroundfetched', event => {
  if (event.id.startsWith('podcast-')) {
    event.waitUntil(async function() {
      // Get podcast by ID
      const podcast = await getPodcast(/podcast-(.*)$/.exec(event.id)[0]);

      await event.fetches.putAllInCache(await caches.open(event.id));
      event.updateUI(`Downloaded ${podcast.showName} - ${podcast.episodeName}`);
    }());
  }
});

…much simpler.

Should backgroundfetched and backgroundfetchfailed be the same thing?

They're almost identical, and could be replaced by a single event that has a property to indicate success/failure.

I tried this in an earlier draft and it felt a bit funky.

There's a small benefit of having them as different events: If the user doesn't handle backgroundfetchfailed explicitly, the browser is in a better position to change the download notification itself.

Should this be independent of the cache API?

This could be entirely separate to the cache API and be called "background-fetch". The "bgfetchcomplete" event could hold a map of requests to responses, and it's down to the developer to put them wherever they want.

When should backgroundfetchfailed fire?

Does it fire as soon as possible, and discard any in-progress fetches? Or does it wait for all fetched to settle?

On one hand, failing fast is nice, because it's fast, but you might end up throwing away a 99% downloaded movie because a piece of metadata 404'd.

Is the backgroundfetched event badly named?

Some background:

The backgroundfetched event fires in the service worker once the background fetch operation is complete, as in we can provide a full response for every request within the background fetch operation.

The event name is a little weird and past-tense because I felt like this may be too similar to the fetch event, which fires early on in fetch. I also considered backgroundfetchcomplete (which felt really long) and backgroundfetchend (which felt a little clunky).

@annevk is there any prior art you're aware of for the above?

The event constructor is currently BackgroundFetchEndEvent, which is wrong, as it should mirror the name. I'll fix that now.

Malicious usage of the background-fetch API

Excuse me bringing that in. Probably this topic is well thought through, but I couldn't find anything related in this repository.

If I wanted to abuse the background-fetch I'd do the following:

  1. When someone visits my website I would
  2. install a service worker and
  3. start a dozen (or just one) of small file downloads (uploads?).
  4. On the server side I'd abort the stream in the middle.
  5. That would trigger backgroundfetchfail in the browser, then
  6. I'd do my malicious actions (work as a bot in the DDOS botnet?) and
  7. (re)start another file download. GoTo # 4.

What am I missing?

Fail on 404?

We made a big mistake with cache.addAll by considering 404 etc successful. We changed it so 404 is considered a failure. However, this means it can't be used for opaque resources, until we decide that response.ok already leaks for opaque responses by other means (<object>), and expose it.

Personally I'm flip-flopping on this, and think it could be dependent on the outcome of #3.

Coalesce or defer SW events

Summary: I suspect we need a way to minimize the number of events fired into the SW if the site is downloading files in chunks. I suspect we also want to be able to defer the events for some time.

Observations:

  • Often these downloads/uploads will be occurring in the background on low memory devices
  • On such devices it may be difficult to wake up a SW in the background, or keep a renderer alive
  • Some sites may want to download many files (e.g. songs for an album) or chunk files and download them individually (e.g. parts of a movie).

I'm presuming that the current design coalesces all files downloaded in one call to the API into one event fired in the SW, is that correct? If not I think we need to support some form of coalescing.

Do you see any issues with a browser deferring the events until it is next opened in the foreground to maximize the chance of success on low memory devices?

Thanks

Which requests can be retried?

We need to think about which cases are "retryable", and which indicate terminal failure. Eg, a POST that results in a network failure may be "retryable", but a POST that results in a 403 may not. We also need to define how often a "retryable" request can be retried before failing, and any kind of delay before retrying.

Range requests probably don't make sense for non-GET requests.

Add to an in-progress job

Currently, calling backgroundFetch.fetch(tag, requests) will reject if there's already an in-progress job with that tag name.

Should it be possible to add to an in-progress job. This may great weirdness in UI.

CSP integration

Background fetch should be bound by the same rules as fetch().

Should BackgroundFetchRegistration.abort() return a Promise?

Since only a single registration for a {service worker registration, tag} pair may be active at a time, having abort() return a void Promise will enable developers to avoid a race condition when having to re-register a background fetch with the same tag.

Create IDL

Maybe just merge a version of the spec with it.

BackgroundFetchSettledFetches needs a constructor

The backgroundfetched and backgroundfetchfail events have init dictionaries that require a sequence of BackgroundFetchSettledFetches objects. (Note that it's not a sequence right now either.)

However, BackgroundFetchSettledFetches is not constructable. It should be.

Remove bgCacheReg.done

The API is intended for Service Workers and most of the time will expect that the download/upload completes a significant time after it's started. In that case, most consumers will be implementing event listeners to deal with success/failure.

If event listeners are going to be the expected use case for notification on status, I'd suggest you remove the "done" promise just to keep the API clean.

Support retrying of uploads

It feels like something we shouldn't do by default, as making the same "POST" request twice may have side effects.

Perhaps we should provide an opt-in to this? Something that says "My server is coded in a way that prevents double-posting. Please retry POSTs".

+@beverloo @johnmellor

In-document progress events

A common use case will be to display the transfer progress of a Background Fetch on a page. Right now that'd require the developer to (a) know the total download size, and (b) iterate over all activeFetches to read the length of the bodies of those that have completed. That's super expensive and complicated.

There's not much we can do about (a), but we can make (b) far more convenient:

interface BackgroundFetchRegistration : EventTarget {
    // Exposes the number of bytes that have been downloaded so far.
    readonly attribute long totalDownloadedSize;

    // Will be fired _at some frequency_ when progress happens.
    [Exposed=Window] attribute EventHandler onprogress;
}

(On a tangent, do we need something similar for totalUploadedSize?)

I guess the event type could just be a simple event without properties, as the developer can re-read totalDownloadSize and totalDownloadedSize from the BackgroundFetchRegistration instance. It also implies that we need to continue to return the same instances from e.g. BackgroundFetchManager.get() instead of creating new ones every time, and keep them updated. How would we define that? Would we be expected to keep updating totalDownloadedSize without event listeners?

Add options to restrict download to particular connection types

I'm in two minds about this.

We could add these options to background sync, then developers could trigger the background-cache from the sync event. This seems like a nice lower-level way of doing it.

Alternatively, we could add the feature directly to background-cache. This would be higher-level, but would allow the OS to pause the download if the connection changes.

Naming for the requests returned in BackgroundFetchFailedEvent and BackgroundFetchedEvent

I had been thinking that the backgroundfetchfail could return a set of results, only some of which failed and some of which succeeded. However, the attribute on the event is called "failedFetches". On the backgroundfetched event, the list of result/response pairs is called "completedFetches".

Proposal: Use "completedFetches" for the attribute on both BackgroundFetchedEvent and BackgroundFetchFailedEvent. Then embed the success or failure of each request in the response?

This may be a moot point depending on the outcome of #23.

bgFetchJob.pause() and .resume()

Probably not for "v1".

Apps might want to have buttons to pause/resume an upload/download job. At least we can have it now for downloading jobs. Also, these buttons can be part of the browser UI (e.g. in the Android drop down notification).

Maybe also add backgroundFetchPause and backgroundFetchResume events. And allow browser to pause jobs. Reasons to pause a job:

  • Not enough space (can happen after the download started)
  • User tap to pause via the Android drop down notification or via app UI
  • Device goes offline
  • etc

Some background on the feature necessity.

We are developing an app to report incidents which can and often happen in rural no-coverage areas. The connectivity might be actually present occasionally or the coverage can be weak/slow (Australian thing). The incident media (photos, videos) should be uploaded with fewer possible retries.

Pausing and resuming upload sounds like a dream feature. I wish there was a standard to resume uploads. But as a workaround we can upload a large file contents in smaller chunks.

Manual range requests

The current proposal allows the browser to turn a single request into multiple range requests, allowing for pause/resume. However, what should we do if the request the user passes contains a range header?

We should probably prevent the browser making its own range requests for that resource in this case.

cc @paullewis

Reuse fetch() somehow?

I wonder if we can reuse fetch() somehow. That way we don't have all the features of the Fetch API in two places. That already causes issues with the Cache API.

Perhaps with the controller/observer design it would be possible to hand those off to the background process somehow?

Restrict requests to http(s): scheme

Currently Background Fetch supports any URI scheme supported by Fetch, with the added restriction that Background Fetch is only exposed in Secure Contexts since it hangs off ServiceWorkerRegistration, and so Mixed Content §should-block-fetch requires request urls to be a priori authenticated. This is defined to be either a data: URI or anything considered "Potentially Trustworthy" by Secure Contexts §is-url-trustworthy.

So in practice requests are currently limited to: https:, [loopback] http:, data:, wss:, file:, chrome-extension: (or similar), about:blank, or about:srcdoc.

I'd like to propose restricting to only https: and [loopback] http:, since:

  • wss: doesn't seem to allow fetching resources.
  • data:, file:, chrome-extension:, and about: are local requests that can be satisfied almost immediately, so the ordinary Fetch API should be preferred for these.

(one case that's debatable is file: URLs to network shares, for which a background fetch might make more sense; however since that's a rather niche usecase I'd rather start without it, and we can always add support later)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.