DEV Community

Cover image for Supercharge Your Website Using PWA: Background Sync
Tapajyoti Bose
Tapajyoti Bose

Posted on • Edited on

16 4

Supercharge Your Website Using PWA: Background Sync

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

Need a Top Rated Software Development Freelancer to chop away your development woes? Contact me on Upwork

Want to see what I am working on? Check out my Personal Website and GitHub

Want to connect? Reach out to me on LinkedIn

Follow my blogs for bi-weekly new Tidbits on Medium

FAQ

These are a few commonly asked questions I get. So, I hope this FAQ section solves your issues.

  1. I am a beginner, how should I learn Front-End Web Dev?
    Look into the following articles:

    1. Front End Buzz words
    2. Front End Development Roadmap
    3. Front End Project Ideas
    4. Transition from a Beginner to an Intermediate Frontend Developer
  2. Would you mentor me?

    Sorry, I am already under a lot of workload and would not have the time to mentor anyone.

Sentry blog image

How to reduce TTFB

In the past few years in the web dev world, we’ve seen a significant push towards rendering our websites on the server. Doing so is better for SEO and performs better on low-powered devices, but one thing we had to sacrifice is TTFB.

In this article, we’ll see how we can identify what makes our TTFB high so we can fix it.

Read more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay