DEV Community

loading...
Cover image for Supercharge Your Website Using PWA: Background Sync

Supercharge Your Website Using PWA: Background Sync

Tapajyoti Bose
Freelancer || Blogger || Cross-Platform App Developer || Web Developer || Open Source Contributor
・4 min read

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>
Enter fullscreen mode Exit fullscreen mode

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)
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
})
Enter fullscreen mode Exit fullscreen mode

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
            })
    );
});
Enter fullscreen mode Exit fullscreen mode

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

GitHub logo 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

  1. Desktop: Windows, Linux, MacOS
  2. Mobile: Android, iOS
  3. Website: Any device with a browser

Back-end Setup

The back-end of the app is handled by Firebase.

Basic Setup

  1. Go to firebase console and create a new project with the name Smartsapp
  2. Enable Google Analylitics

App Setup

  1. Create an App for the project from the overview page
  2. Copy and paste the configurations in the required location (given in the readme of the respective apps)

Auth Setup

  1. Go to the project Authentication section
  2. Select Sign-in method tab
  3. Enable Email/Password and Google sign in

Firestore Setup

  1. Go to the project Firestore section
  2. Create firestore provisions for the project (choose the server nearest to your location)
  3. Go to the Rules




Thanks for reading

Reach out to me on:

Discussion (0)