• Sun. Jul 14th, 2024

Progressive web app essentials: Service worker background sync


Jul 10, 2024

const URL = “https://8014-35-223-70-178.ngrok-free.app/”; // 1
const taskChannel = new BroadcastChannel(‘task-channel’); // 2
taskChannel.onmessage = event => { // 3
persistTask(event.data.data); // 4
registration.sync.register(‘task-sync’); // 5

let db = null; // 6
let request = indexedDB.open(“TaskDB”, 1); // 7
request.onupgradeneeded = function(event) { // 8
db = event.target.result; // 9
if (!db.objectStoreNames.contains(“tasks”)) { // 10
let tasksObjectStore = db.createObjectStore(“tasks”, { autoIncrement: true }); // 11
request.onsuccess = function(event) { db = event.target.result; }; // 12
request.onerror = function(event) { console.log(“Error in db: ” + event); }; // 13

persistTask = function(task){ // 14
let transaction = db.transaction(“tasks”, “readwrite”);
let tasksObjectStore = transaction.objectStore(“tasks”);
let addRequest = tasksObjectStore.add(task);
addRequest.onsuccess = function(event){ console.log(“Task added to DB”); };
addRequest.onerror = function(event) { console.log(“Error: “ + event); };
self.addEventListener(‘sync’, async function(event) { // 15
if (event.tag == ‘task-sync’) {
event.waitUntil(new Promise((res, rej) => { // 16
let transaction = db.transaction(“tasks”, “readwrite”);
let tasksObjectStore = transaction.objectStore(“tasks”);
let cursorRequest = tasksObjectStore.openCursor();
cursorRequest.onsuccess = function(event) { // 17
let cursor = event.target.result;
if (cursor) {
let task = cursor.value; // 18
fetch(URL + ‘todos/add’, // a
{ method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ “task” : task })
}).then((serverResponse) => {
console.log(“Task saved to backend.”);
deleteTasks(); // b
res(); // b
}).catch((err) => {
console.log(“ERROR: ” + err);
rej(); //c
async function deleteTasks() { // 19
const transaction = db.transaction(“tasks”, “readwrite”);
const tasksObjectStore = transaction.objectStore(“tasks”);
await transaction.complete;

Now let’s talk about what is happening in this code.

We need to route our requests through the same secure tunnel we created with ngrok, so we save the URL here.

Create the broadcast channel with the same name so we can listen for messages.

Here, we are watching for task-channel message events. In responding to these events, we do two things:

Call persistTask() to save the new task to IndexedDB.

Register a new sync event. This is what invokes the special capability for retrying requests intelligently. The sync handler allows us to specify a promise that it will retry when the network is available, and implements a back off strategy and give-up conditions.

With that done, we create a reference for our database object.

Obtain a “request” for the handle on our database. Everything on IndexedDB is handled asynchronously. (For an excellent overview of IndexedDB, I recommend this series.)

The onupgradeneeded event fires if we are accessing a new or up-versioned database. 

Inside onupgradeneeded, we get a handle on the database itself, with our global db object.

If the tasks collection is not present, we create the tasks collection.

If the database was successfully created, we save it to our db object.

Log the error if the database creation failed.

The persistTask() function called by the add-task broadcast event (4). This simply puts the new task value in the tasks collection.

Our sync event. This is called by the broadcast event (5). We check for the event.tag field being task-sync so we know it’s our task-syncing event.

event.waitUntil() allows us to tell the serviceWorker that we are not done until the Promise inside it completes. Because we are in a sync event, this has special meaning. In particular, if our Promise fails, the syncing algorithm will keep trying. Also, remember that if the network is unavailable, it will wait until it becomes available.

We define a new Promise, and within it we begin by opening a connection to the database.

Within the database onsuccess callback, we obtain a cursor and use it to grab the task we saved. (We are leveraging our wrapping Promise to deal with nested asynchronous calls.)

Now we have a variable with the value of our broadcast task in it. With that in hand:

We issue a new fetch request to our expressJS /todos/add endpoint.

Notice that if the request succeeds, we delete the task from the database and call res() to resolve our outer promise.

If the request fails, we call rej(). This will reject the containing promise, letting the Sync API know the request must be retried.

The deleteTasks() helper method deletes all the tasks in the database. (This is a simplified example that assumes one tasks creation at a time.)

Clearly, there is a lot to this, but the reward is being able to effortlessly retry requests in the background whenever our network is spotty. Remember, we are getting this in the browser, across all kinds of devices, mobile and otherwise.

Testing the PWA example

If you run the PWA now and create a to-do, it’ll be sent to the back end and saved. The interesting test is to open devtools (F12) and disable the network. You can find the “Offline” option in the “throttling” menu of the network tab like so:

Source link