Supercharge Your Website Using PWA: Background Sync
Tags: pwa, webdev, ux, javascript
Published: 2021-05-09 04:40:06

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.

```html
<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.

```js
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.

```js
// 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.

```js
// 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](https://caniuse.com/mdn-api_syncmanager_worker_support))
- The sync executes only when the `service worker` detects connection has been re-established.

# Reference

- [Introducing Background Sync](https://developers.google.com/web/updates/2015/12/background-sync)
- [Reach of SyncManager API](https://caniuse.com/mdn-api_syncmanager_worker_support)

# Project with basic `PWA` features

**Smartsapp**

Web-app: [https://smartsapp-ba40f.firebaseapp.com](https://smartsapp-ba40f.firebaseapp.com/)

{% github https://github.com/ruppysuppy/SmartsApp %}

Finding **personal finance** too intimidating? Checkout my **Instagram** to become a [**Dollar Ninja**](https://www.instagram.com/the.dollar.ninja/)

# Thanks for reading

Reach out to me on:

-   [GitHub](https://github.com/ruppysuppy)
-   [LinkedIn](https://www.linkedin.com/in/tapajyoti-bose-429a601a0)
-   [Portfolio](https://tapajyoti-bose.vercel.app) (For freelance projects, reach out through the contact form)