Update December 2018: Visit this blog post https://golb.hplar.ch/2018/12/background-sync-ng.html if you are interested in the same example but with the Angular service worker.
Service Worker introduces APIs to the browser that allows 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 (March 2020), Chrome and Chromium-based browsers are the only browsers that support this feature. Visit caniuse.com to see the current state of implementation: https://caniuse.com/#feat=background-sync
Usage ¶
Background Sync is a very simple API. The name is a bit misleading because the API itself has nothing to do with synchronization. It does not magically synchronize data between a client and a 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 of notifying an application even when it is not open in the browser, or the browser runs in the background.
Background Sync is useful for a web application that runs on a mobile device, where you sometimes have a flaky or no internet connection.
The API consists of the SyncManager interface and the 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, the 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 fulfills, the processing was successful. If it fails, the browser is going to schedule 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 multiple times while another sync request is waiting, the browser coalesces the requests 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 a parameter an instance of the SyncEvent interface. This object contains two attributes:
tag
a string containing the name of the tag. Same value as the argument passed to theregister()
method.lastChance
a boolean attribute. If true, the browser is not going to make any further attempts, if the current attempt fails.
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 / Angular web application and a Spring Boot server. Client and server-side technologies do not matter in this context; Background Sync works with any web framework. 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 blog post about integrating a Workbox service worker 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): Promise<void> {
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());
}
}
}
async requestSync(): Promise<void> {
const swRegistration = await navigator.serviceWorker.ready;
// @ts-ignore
await swRegistration.sync.register('todo_updated');
}
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', event => {
// @ts-ignore
if (event.tag === 'todo_updated') {
// @ts-ignore
event.waitUntil(serverSync());
}
});
IndexedDB is a service you can access from the foreground script and 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:
The example application 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 blog post mentioned above 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 install the client and start it. You need to have Node.js, and the Ionic CLI installed.
cd client
npm install
ionic start
Links ¶
You find more information about the Background Sync API in this article from Google
https://developer.chrome.com/blog/background-sync/
and in the specification
https://wicg.github.io/BackgroundSync/spec/
https://github.com/WICG/background-sync/blob/main/explainers/sync-explainer.md