This is a continuation of the previous blogs on adding background synchronization, you are highly encouraged to check out the previous blogs before continuing.
Getting Started
We would be required to make a request for some external resource from the web (like data fetch or post), as without that any website by default works offline. Let's create a form
, whose input is submitted to a mock server.
NOTE: This is a continuation from the previous blog where the manifest
& service worker
have already been added.
<form id="email-form">
<input type="email" id="email-input" />
<br /><br />
<button type="submit">Submit</button>
</form>
Converting IndexedDB
The default behavior of IndexedDB
uses a lot of callbacks, let's add a Promise
based wrapper for ease of use.
const DB_NAME = 'background-sync-db';
const DB_VERSION = 1;
const STORE_NAME = 'unsent-requests-store';
const IDB = {
initialize() {
return new Promise((resolve, reject) => {
// Create a new DB
const request = indexedDB.open(DB_NAME, DB_VERSION)
request.onupgradeneeded = function () {
request.result.createObjectStore(STORE_NAME)
resolve()
}
request.onerror = function () {
reject(request.error)
}
})
},
getByKey(key) {
return new Promise((resolve, reject) => {
const oRequest = indexedDB.open(DB_NAME, DB_VERSION)
oRequest.onsuccess = function () {
const db = oRequest.result
const tx = db.transaction(STORE_NAME, 'readonly')
const st = tx.objectStore(STORE_NAME)
const gRequest = st.get(key)
gRequest.onsuccess = function () {
resolve(gRequest.result)
}
gRequest.onerror = function () {
reject(gRequest.error)
}
}
oRequest.onerror = function () {
reject(oRequest.error)
}
})
},
setByKey(value, key) {
return new Promise((resolve, reject) => {
const oRequest = indexedDB.open(DB_NAME, DB_VERSION)
oRequest.onsuccess = function () {
const db = oRequest.result
const tx = db.transaction(STORE_NAME, 'readwrite')
const st = tx.objectStore(STORE_NAME)
const sRequest = st.put(value, key)
sRequest.onsuccess = function () {
resolve()
}
sRequest.onerror = function () {
reject(sRequest.error)
}
}
oRequest.onerror = function () {
reject(oRequest.error)
}
})
},
deletebyKey(key) {
return new Promise((resolve, reject) => {
const oRequest = indexedDB.open(DB_NAME, DB_VERSION)
oRequest.onsuccess = function () {
const db = oRequest.result
const tx = db.transaction(STORE_NAME, 'readwrite')
const st = tx.objectStore(STORE_NAME)
const rRequest = st.delete(key)
rRequest.onsuccess = function () {
resolve()
}
rRequest.onerror = function () {
reject(rRequest.error)
}
}
oRequest.onerror = function () {
reject(oRequest.error)
}
})
},
getAllKeys() {
return new Promise((resolve, reject) => {
const oRequest = indexedDB.open(DB_NAME, DB_VERSION)
oRequest.onsuccess = function () {
const db = oRequest.result
const tx = db.transaction(STORE_NAME, 'readonly')
const st = tx.objectStore(STORE_NAME)
const kRequest = st.getAllKeys()
kRequest.onsuccess = function () {
resolve(kRequest.result)
}
kRequest.onerror = function () {
reject(kRequest.error)
}
}
oRequest.onerror = function () {
reject(oRequest.error)
}
})
}
}
We will only be requiring parts of the above snippet, so you may use only the required part in the corresponding file or make a separate script and add it to the service worker
using importScripts()
and the HTML body.
Registering Background Sync
task
We need to store the data in the IndexedDB
before registering the background sync
task, so that the data can be accessed after the internet connection is re-established (in case the user is not connected to the web).
If the browser being used doesn't support background sync
, there is no point storing it in the IndexedDB
as it cannot be synced later, we directly send the request in this case.
// script.js
const emailForm = document.querySelector('#email-form');
const emailInput = document.querySelector('#email-input');
IDB.initialize()
emailForm.addEventListener("submit", async (e) => {
e.preventDefault()
const data = {
email: emailInput.value
}
emailInput.value = ""
if ('serviceWorker' in navigator && 'SyncManager' in window && 'indexedDB' in window) {
// storing the data in indexedDB
await IDB.setByKey(Date.now(), data) // using current timestamp as key (not a recommended practice)
// registering `background sync` task
const registration = await navigator.serviceWorker.ready
await registration.sync.register('sync-emails')
console.log("[DB] data stored");
console.log("[FORM] sync registered");
} else {
// sending the request directly in case `background sync` is not supported
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
const jsonData = await response.json()
console.log("[FORM] submitted (sync not supported)");
console.log("[RESPONSE]", jsonData);
}
})
Handling the background sync
in service worker
Since we stored the data in IndexedDB
, we will be fetching data from the DB and sending the requests.
If the request fails (sync triggered by registering the sync task), you should throw an error to ensure its automatically registered for sync when the connection is re-established.
// sync handler
const syncEmails = async () => {
const keys = await IDB.getAllKeys()
for (const key of keys) {
// sending data to the server
const data = await IDB.getByKey(key)
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
})
const jsonData = await response.json()
console.log("[RESPONSE]", jsonData)
// removing the data from the `indexedDB` if data was sent successfully
await IDB.deletebyKey(key)
console.log("[DB] removed", key)
}
}
// adding sync listener
self.addEventListener('sync', function (event) {
console.log("[SYNC] sync event triggered");
event.waitUntil(
syncEmails()
.then(() => console.log("[SYNC] Success"))
.catch((err) => {
console.log("[SYNC] Error")
throw err
})
);
});
If you have multiple sync registrations (eg: sync-emails, sync-data, etc), you can use switch(event.tag)
to handle each type of sync event.
Caveats
Some things to keep in mind:
- To go offline you have to physically cut your connection to the internet (eg: turn off wifi and not use offline mode from dev tools)
- Access to
background sync
is still limited (around 71% of the devices) - The sync executes only when the
service worker
detects connection has been re-established.
Reference
Project with basic PWA
features
Smartsapp
Web-app: https://smartsapp-ba40f.firebaseapp.com
ruppysuppy / SmartsApp
π¬π± An End to End Encrypted Cross Platform messenger app.
Smartsapp
A fully cross-platform messenger app with End to End Encryption (E2EE).
Demo
NOTE: The features shown in the demo is not exhaustive. Only the core features are showcased in the demo.
Platforms Supported
- Desktop: Windows, Linux, MacOS
- Mobile: Android, iOS
- Website: Any device with a browser
Back-end Setup
The back-end of the app is handled by Firebase
.
Basic Setup
- Go to firebase console and create a new project with the name
Smartsapp
- Enable
Google Analylitics
App Setup
- Create an
App
for the project from the overview page - Copy and paste the configurations in the required location (given in the readme of the respective apps)
Auth Setup
- Go to the project
Authentication
section - Select
Sign-in method
tab - Enable
Email/Password
andGoogle
sign in
Firestore Setup
- Go to the project
Firestore
section - Create firestore provisions for the project (choose the server nearest to your location)
- Go to the
Rules
β¦
Finding personal finance too intimidating? Checkout my Instagram to become a Dollar Ninja
Thanks for reading
Reach out to me on:
Top comments (0)