A closer look at the Background Sync API

Published: June 26, 2018  •  pwa, javascript, java, spring

Service Worker introduce APIs to the browser that allow your application to intercept requests, cache resources and to receive push notification.
Another API, that is part of a Service Worker implementation, is the Background Sync API. It's a very small interface and already exists for quite some time, the feature was introduced by Google in Chrome 49 (March 2016). But until now (June 2018), Chrome is the only browser that supports this feature. Microsoft and Firefox are working on the feature, so we might see more widespread support in the future. Visit caniuse.com to see the current implementation state: https://caniuse.com/#feat=background-sync


Usage

Background Sync is a very simple and low level API. The name is a bit misleading, because the API itself has nothing to do with synchronizing. It does not magically synchronize data between a client and server, this is still your job to implement that. What the API does is telling an application when the device has network connectivity. It's very similar to the Online/Offline events we already have in our browsers. But the Background Sync API runs in the Service Worker and is therefore capable to notify an application even when it is not open in the browser or the browser runs in the background.

Background Sync is useful for web application that run on a mobile device, where you sometimes have a flaky or no internet connection.

The API itself consists of a SyncManager interface and a sync event.

In the foreground script an application asks for an event to be fired as soon as the device has connectivity.

navigator.serviceWorker.ready.then(swRegistration => swRegistration.sync.register('todo_updated'));

In the Service Worker you register a listener for the sync event. When the device is online, a register() call immediately emits the sync event.
When the device has no network connectivity, the browser registers the request and waits until the device goes online and then fires the event.

self.addEventListener('sync', function (event) {
  if (event.tag == 'todo_updated') {
    event.waitUntil(serverSync());
  }
});

async function serverSync() {
  // synchronize data with a server
}

Make sure that you wrap your code in an event.waitUntil() call. This tells the browser that work is ongoing until the promise settles and to make sure that the operating system does not put the browser to sleep.

The method that processes the synchronization, serverSync() in this example, has to return a Promise. If it fulfils the processing was successful, if it fails the browser will be scheduling another sync. Retry syncs also wait for connectivity and a certain amount of time.

Here is a blog post that explains what happens when the promise fails multiple times. Essentially, what the current version of Chrome does is emitting the sync event three times, when the Promise fails. Between the 1st and 2nd event he waits five minutes and between the 2nd and 3rd event he waits 15 minutes and after the 3rd failed event he gives up.

The argument you pass to the register() method ("todo_updated") is called the tag name and it should be unique for a given sync. If an application calls register() with the same tag as a waiting sync request, the browser coalesces the request with the existing one. Or in other words, if your application calls register() ten times with the same tag name while the device is offline the browser emits only one sync event as soon as network connectivity is available. If your application needs ten separate sync events, you have to call register() with a unique tag name each time.

In the sync listener your application receives as parameter an instance of the SyncEvent interface. This object contains two attributes:


Demo application

After the basic introduction we now know what the Background Sync API does and how to use it. In this section I want to show you a simple example that uses the Background Sync API to synchronize data between an Ionic 4 / Angular web application and a Spring Boot server. Client and server side technologies do not matter in this context, Background Sync will work with any web technology. I just chose the frameworks that I'm familiar with.

Angular provides a built in Service Worker, but for this example I wrote a custom Service Worker with Workbox. See my previous blog post about incorporating Workbox into an Angular application.

The example implements a very trivial to-do application. It displays a list of to-dos and the user can insert, update and delete entries. Each time the user changes something the application stores the data in an IndexedDB object store and then requests a sync operation with register().

  async save(todo: Todo) {
    if (!todo.id) {
      todo.id = uuidv4();
      todo.ts = 0;
      this.db.todos.add(todo).then(() => this.requestSync());
    } else {
      const oldTodo = await this.db.todos.get(todo.id);
      if (this.changed(oldTodo, todo)) {
        todo.ts = Date.now();
        this.db.todos.put(todo).then(() => this.requestSync());
      }
    }
  }

  requestSync() {
    navigator.serviceWorker.ready.then(swRegistration => swRegistration.sync.register('todo_updated'));
  }

todo.service.ts

In the Service Worker, the sync event handler reads the data from the IndexedDB object store and compares it with the data from the server.

self.addEventListener('sync', function (event) {
  if (event.tag == 'todo_updated') {
    event.waitUntil(serverSync());
  }
});

async function serverSync() {

service-worker.js

IndexedDB is a service you can access from the foreground script and from the Service Worker. You can't use localStorage because this database is not accessible from a Service Worker.
IndexedDB has not the most user-friendly API, therefore I use Dexie.js, a thin wrapper library that simplifies IndexedDB development.

Here an overview of the different parts of the application and how they play together:

overview

The example applications employs a simple synchronization algorithm that I implemented based on the description from this blog post:
https://coderwall.com/p/gt_rfa/simple-data-synchronisation-for-web-mobile-apps-working-offline

My implementation is not tested and I don't use it in a real application, it should be considered as an experiment. For the sake of brevity, I won't go into more detail about the algorithm. The above mentioned blog post explains the algorithm in detail and if you have questions about my implementation send me a message (bug reports and improvements are also welcome).

You find the complete source code for the client and the server on GitHub: https://github.com/ralscha/blog/tree/master/background-sync

To run the example you first have to start the server.

cd server
./mvnw spring-boot:run
//or on Windows
.\mvnw.cmd spring-boot:run

then build the client and start it. You need to have Node.js installed.

cd client
npm install
npm run dist
npm run open

Links

You find more information about the Background Sync API in this article from Google
https://developers.google.com/web/updates/2015/12/background-sync

and in the specification
http://wicg.github.io/BackgroundSync/spec/
https://github.com/WICG/BackgroundSync/blob/master/explainer.md