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");
tasksObjectStore.clear();
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 toIndexedDB
. - 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 ofIndexedDB
, 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 globaldb
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 beingtask-sync
so we know it’s our task-syncing event. event.waitUntil()
allows us to tell theserviceWorker
that we are not done until thePromise
inside it completes. Because we are in a sync event, this has special meaning. In particular, if ourPromise
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.
- We define a new
- Within the database
onsuccess
callback, we obtain a cursor and use it to grab the task we saved. (We are leveraging our wrappingPromise
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 ourexpressJS /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.
- We issue a new
- The
deleteTasks()
helper method deletes all the tasks in the database. (This is a simplified example that assumes onetasks
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: