workbox-background-sync

When you send data to a web server, sometimes the requests will fail. It may be because the user has lost connectivity, or it may be because the server is down; in either case you often want to try sending the requests again later.

The new BackgroundSync API is an ideal solution to this problem. When a service worker detects that a network request has failed, it can register to receive a sync event, which gets delivered when the browser thinks connectivity has returned. Note that the sync event can be delivered even if the user has left the application, making it much more effective than the traditional method of retrying failed requests.

Workbox Background Sync is designed to make it easier to use the BackgroundSync API and integrate its usage with other Workbox modules. It also implements a fallback strategy for browsers that don't yet implement BackgroundSync.

Browsers that support the BackgroundSync API will automatically replay failed requests on your behalf at an interval managed by the browser, likely using exponential backoff between replay attempts. In browsers that don't natively support the BackgroundSync API, Workbox Background Sync will automatically attempt a replay whenever your service worker starts up.

Basic Usage

The easiest way to use Background Sync is to use the Plugin that will automatically Queue up failed requests and retry them when future sync events are fired.

import {BackgroundSyncPlugin} from 'workbox-background-sync';
import {registerRoute} from 'workbox-routing';
import {NetworkOnly} from 'workbox-strategies';

const bgSyncPlugin = new BackgroundSyncPlugin('myQueueName', {
  maxRetentionTime: 24 * 60, // Retry for max of 24 Hours (specified in minutes)
});

registerRoute(
  /\/api\/.*\/*.json/,
  new NetworkOnly({
    plugins: [bgSyncPlugin],
  }),
  'POST'
);

BackgroundSyncPlugin hooks into the fetchDidFail plugin callback, and fetchDidFail is only invoked if there's an exception thrown, most likely due to a network failure. This means that requests won't be retried if there's a response received with a 4xx or 5xx error status. If you would like to retry all requests that result in, e.g., a 5xx status, you can do so by adding a fetchDidSucceed plugin to your strategy:

const statusPlugin = {
  fetchDidSucceed: ({response}) => {
    if (response.status >= 500) {
      // Throwing anything here will trigger fetchDidFail.
      throw new Error('Server error.');
    }
    // If it's not 5xx, use the response as-is.
    return response;
  },
};

// Add statusPlugin to the plugins array in your strategy.

Advanced Usage

Workbox Background Sync also provides a Queue class, which you can instantiate and add failed requests to. The failed requests are stored in IndexedDB and are retried when the browser thinks connectivity is restored (i.e. when it receives the sync event).

Creating a Queue

To create a Workbox Background Sync Queue you need to construct it with a queue name (which must be unique to your origin):

import {Queue} from 'workbox-background-sync';

const queue = new Queue('myQueueName');

The queue name is used as part of the tag name that gets register()-ed by the global SyncManager. It's also used as the Object Store name for the IndexedDB database.

Adding a request to the Queue

Once you've created your Queue instance, you can add failed requests to it. You add failed request by invoking the .pushRequest() method. For example, the following code catches any requests that fail and adds them to the queue:

import {Queue} from 'workbox-background-sync';

const queue = new Queue('myQueueName');

self.addEventListener('fetch', event => {
  // Add in your own criteria here to return early if this
  // isn't a request that should use background sync.
  if (event.request.method !== 'POST') {
    return;
  }

  const bgSyncLogic = async () => {
    try {
      const response = await fetch(event.request.clone());
      return response;
    } catch (error) {
      await queue.pushRequest({request: event.request});
      return error;
    }
  };

  event.respondWith(bgSyncLogic());
});

Once added to the queue, the request is automatically retried when the service worker receives the sync event (which happens when the browser thinks connectivity is restored). Browsers that don't support the BackgroundSync API will retry the queue every time the service worker is started up. This requires the page controlling the service worker to be running, so it won't be quite as effective.

Testing Workbox Background Sync

Sadly, testing BackgroundSync is somewhat unintuitive and difficult for a number of reasons.

The best approach to test your implementation is to do the following:

  1. Load up a page and register your service worker.
  2. Turn off your computer's network or turn off your web server.
    • DO NOT USE CHROME DEVTOOLS OFFLINE. The offline checkbox in DevTools only affects requests from the page. Service Worker requests will continue to go through.
  3. Make network requests that should be queued with Workbox Background Sync.
    • You can check the requests have been queued by looking in Chrome DevTools > Application > IndexedDB > workbox-background-sync > requests
  4. Now turn on your network or web server.
  5. Force an early sync event by going to Chrome DevTools > Application > Service Workers, entering the tag name of workbox-background-sync:<your queue name> where <your queue name> should be the name of the queue you set, and then clicking the 'Sync' button.

    Example of Sync button in Chrome DevTools

  6. You should see network requests go through for the failed requests and the IndexedDB data should now be empty since the requests have been successfully replayed.

Types

BackgroundSyncPlugin

A class implementing the fetchDidFail lifecycle callback. This makes it easier to add failed requests to a background sync Queue.

Properties

Queue

A class to manage storing failed requests in IndexedDB and retrying them later. All parts of the storing and replaying process are observable via callbacks.

Properties

  • constructor

    void

    Creates an instance of Queue with the given options

    The constructor function looks like:

    (name: string, options?: QueueOptions) => {...}

    • name

      string

      The unique name for this queue. This name must be unique as it's used to register sync events and store requests in IndexedDB specific to this instance. An error will be thrown if a duplicate name is detected.

    • options

      QueueOptions optional

  • name

    string

  • getAll

    void

    Returns all the entries that have not expired (per maxRetentionTime). Any expired entries are removed from the queue.

    The getAll function looks like:

    () => {...}

    • returns

      Promise<QueueEntry[]>

  • popRequest

    void

    Removes and returns the last request in the queue (along with its timestamp and any metadata). The returned object takes the form: {request, timestamp, metadata}.

    The popRequest function looks like:

    () => {...}

    • returns

      Promise<QueueEntry>

  • pushRequest

    void

    Stores the passed request in IndexedDB (with its timestamp and any metadata) at the end of the queue.

    The pushRequest function looks like:

    (entry: QueueEntry) => {...}

    • entry

      QueueEntry

    • returns

      Promise<void>

  • registerSync

    void

    Registers a sync event with a tag unique to this instance.

    The registerSync function looks like:

    () => {...}

    • returns

      Promise<void>

  • replayRequests

    void

    Loops through each request in the queue and attempts to re-fetch it. If any request fails to re-fetch, it's put back in the same position in the queue (which registers a retry for the next sync event).

    The replayRequests function looks like:

    () => {...}

    • returns

      Promise<void>

  • shiftRequest

    void

    Removes and returns the first request in the queue (along with its timestamp and any metadata). The returned object takes the form: {request, timestamp, metadata}.

    The shiftRequest function looks like:

    () => {...}

    • returns

      Promise<QueueEntry>

  • size

    void

    Returns the number of entries present in the queue. Note that expired entries (per maxRetentionTime) are also included in this count.

    The size function looks like:

    () => {...}

    • returns

      Promise<number>

  • unshiftRequest

    void

    Stores the passed request in IndexedDB (with its timestamp and any metadata) at the beginning of the queue.

    The unshiftRequest function looks like:

    (entry: QueueEntry) => {...}

    • entry

      QueueEntry

    • returns

      Promise<void>

QueueOptions

Properties

  • forceSyncFallback

    boolean optional

  • maxRetentionTime

    number optional

  • onSync

    OnSyncCallback optional

QueueStore

A class to manage storing requests from a Queue in IndexedDB, indexed by their queue name for easier access.

Most developers will not need to access this class directly; it is exposed for advanced use cases.

Properties

  • constructor

    void

    Associates this instance with a Queue instance, so entries added can be identified by their queue name.

    The constructor function looks like:

    (queueName: string) => {...}

    • queueName

      string

  • deleteEntry

    void

    Deletes the entry for the given ID.

    WARNING: this method does not ensure the deleted entry belongs to this queue (i.e. matches the queueName). But this limitation is acceptable as this class is not publicly exposed. An additional check would make this method slower than it needs to be.

    The deleteEntry function looks like:

    (id: number) => {...}

    • id

      number

    • returns

      Promise<void>

  • getAll

    void

    Returns all entries in the store matching the queueName.

    The getAll function looks like:

    () => {...}

    • returns

      Promise<QueueStoreEntry[]>

  • popEntry

    void

    Removes and returns the last entry in the queue matching the queueName.

    The popEntry function looks like:

    () => {...}

    • returns

      Promise<QueueStoreEntry>

  • pushEntry

    void

    Append an entry last in the queue.

    The pushEntry function looks like:

    (entry: UnidentifiedQueueStoreEntry) => {...}

    • entry

      UnidentifiedQueueStoreEntry

    • returns

      Promise<void>

  • shiftEntry

    void

    Removes and returns the first entry in the queue matching the queueName.

    The shiftEntry function looks like:

    () => {...}

    • returns

      Promise<QueueStoreEntry>

  • size

    void

    Returns the number of entries in the store matching the queueName.

    The size function looks like:

    () => {...}

    • returns

      Promise<number>

  • unshiftEntry

    void

    Prepend an entry first in the queue.

    The unshiftEntry function looks like:

    (entry: UnidentifiedQueueStoreEntry) => {...}

    • entry

      UnidentifiedQueueStoreEntry

    • returns

      Promise<void>

StorableRequest

A class to make it easier to serialize and de-serialize requests so they can be stored in IndexedDB.

Most developers will not need to access this class directly; it is exposed for advanced use cases.

Properties

  • constructor

    void

    Accepts an object of request data that can be used to construct a Request but can also be stored in IndexedDB.

    The constructor function looks like:

    (requestData: RequestData) => {...}

    • requestData

      RequestData

      An object of request data that includes the url plus any relevant properties of [requestInit]https://fetch.spec.whatwg.org/#requestinit.

  • clone

    void

    Creates and returns a deep clone of the instance.

    The clone function looks like:

    () => {...}

  • toObject

    void

    Returns a deep clone of the instances _requestData object.

    The toObject function looks like:

    () => {...}

    • returns

      RequestData

  • toRequest

    void

    Converts this instance to a Request.

    The toRequest function looks like:

    () => {...}

    • returns

      Request

  • fromRequest

    void

    Converts a Request object to a plain object that can be structured cloned or JSON-stringified.

    The fromRequest function looks like:

    (request: Request) => {...}

    • request

      Request