Friday, November 29, 2024

Cache in JavaScript: IndexedDB Database



Caching is a technique for storing data in a temporary storage area called a cache. It aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. In the first article, I introduced the different possibilities for caching data in JavaScript and how to choose the appropriate caching possibility according to your needs. In a previous article, I introduced an example of using the Web Storage API (localStorage and sessionStorage) to cache data. Then, I dedicated another article to using objects and data-* attributes to temporarily cache data in JavaScript. Also, I elucidated the use of service workers to cache static resources like HTML, CSS, and JavaScript files or API responses. In this article, I talk about caching data using the IndexedDB database.


1. IndexedDB Database and IndexedDB API

IndexedDB database is a client-side database that stores large amounts of structured data including files and blobs in browsers. This means that a web application can use IndexedDB to cache data so that it can be accessed quickly and avoid frequent requests to the server. 


IndexedDB API is a low-level API that manages the corresponding database. It is good for complex applications that need to cache large JSON responses or images and allows working offline.


To avoid blocking web applications, IndexedDB operations are executed asynchronously.


2. IndexedDB Operations

You can perform several operations on IndexedDB. Some can be applied to the database itself, some can be applied to object stores, and some operations are used to manage the data stored in the database. Let’s look at the different IndexedDB operations based on their types.


Database Operations and Event Handling: Database operations are asynchronous and allow you to create, manage, and delete the database. Here is the list of major operations and events for managing the IndexedDB database.


Database Operations

Operation

Definition

Syntax

Create/Open

Opens a database if it exists. Otherwise, it is created.

indexedDB.open()

Delete

Deletes a database.

deleteDatabase()

Close

Closes the connection to the database.

close()

Events

DB Structure upgrade or change

Triggered when the structure of the database changes.

versionchange Event

Operation successfully performed

Triggered upon successful operation completion.

onsuccess

Error occurred

Triggered when an error occurs.

onerror

DB version changed

- Triggered when the database version changes.

- It is used for schema updates.

onupgradeneeded


Object Store Operations: Object stores are analogous to relational database tables, serving as a structured repository for data organization. The following table explains the main operations of creating and deleting object stores.


Object Store Operations

Operation

Definition

Syntax

Create

- Creates an object store as part of the database upgrade process.

- This method can only be invoked within the context of a version change transaction.

createObjectStore()

Delete

Deletes an object store.

deleteObjectStore()


Transaction Operations: Transactions allow you to perform operations atomically. Here is the list of the main ones you can apply to transactions.

Transaction Operations

Operation

Definition

Syntax

Create a transaction

Establishes a transaction for reading, writing, or a combination of both, guaranteeing data consistency.

Two modes are defined:

- readonly: read-only operations.

- readwrite: read and write operations.

transaction()


CRUD (Create, Read, Update, Delete) Operations: Crud operations refer to the four basic functions that can be performed on data in the data storage system.

CRUD (Create, Read, Update, Delete) Operations

Operation

Definition

Syntax

Read

- Retrieves a record by its key (unique identifier).

- Retrieves all records present in the object store.

- Traverses each record in the object store or index.

- get()

- getAll()

- openCursor()

Insert

- Adds a new record.

- Fails if a record with the same key already exists.

add()

Update

Inserts a new record or updates an existing one in the object store.

put()

Delete

- Deletes a record using its key.

- Deletes all records in an object store.

- delete()

- clear()


Index Operations: Indexes enable fast and optimized querying by providing a quick way to locate specific data. The following table summarizes the principal operations on indexes.

Index Operations

Operation

Definition

Syntax

Create index

Creates an index on a particular field or attribute of records.

createIndex()

Delete Index

Deletes an index.

deleteIndex()

Get Index

Utilizes an existing index to retrieve and filter data.

index()


Key Range Queries: Key ranges are used to filter data during queries. They allow you to retrieve only the specific subset of data that falls within a defined range.

Key Range Queries

Operation

Definition

Syntax

Define a range

Specifies a key range boundary (inclusive or exclusive), allowing for targeted data retrieval within a defined scope.

IDBKeyRange.bound()

Define a range with a lower bound

Specifies a range with a lower bound.

IDBKeyRange.lowerBound()

Define a range with an upper bound

Defines a range with an upper limit.

Specifies a range with an upper bound

IDBKeyRange.upperBound()

Define a specific key

Specifies an exact key value, allowing for the retrieval of a single, specific record.

IDBKeyRange.only()


3. Example of Caching Data Using IndexedDB Database

IndexedDB database is a suitable mechanism for caching large amounts of data. Let’s take an example of how you can cache the result of getting data from the back-end using IndexedDB. For this, we need some back-end code to return data. An easy way to do this is to use Python’s Flask framework. Additionally, we develop HTML, CSS, and JavaScript code for the front-end.


Note. We only write the relevant part here to explain how to use IndexedDB to cache data. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/indexeddb-app-blog


Python Code

In the back-end code, we first define ‘company_data’ which stores data about companies, departments, and employees. The dictionary has three main keys: ‘companies’, ‘departments’, and ‘employees’. Each dictionary in the ‘companies’ list represents a company with an ‘id’ and a ‘name’. Each dictionary in the ‘departments’ list represents a department with an ‘id’, a ‘name’, and a ‘companyId’ that references its company. Finally, each dictionary in ‘employees’ is an employee with an ‘id’, a ‘firstname’, a ‘lastname’, an ‘email’, a ‘departmentId’, a ‘departmentName’, and a ‘companyName’.


company_data = {

    "companies": [

        {"id": 1, "name": "TechCorp"},

        {"id": 2, "name": "TechMicro"}

    ],

    "departments": [

        {"id": 1, "name": "Engineering", "companyId": 1},

        {"id": 2, "name": "Marketing", "companyId": 1},

        {"id": 3, "name": "Sales", "companyId": 2},

        {"id": 4, "name": "HR", "companyId": 2}

    ],

    "employees": [

        {"id": 1, "firstname": "Alice", "lastname": "Smith", "email": "alice@techcorp.com", "departmentId": 1, "departmentName": "Engineering", "companyName": "TechCorp"},

        {"id": 2, "firstname": "Bob", "lastname": "Brown", "email": "bob@techcorp.com", "departmentId": 1, "departmentName": "Engineering", "companyName": "TechCorp"},

        {"id": 3, "firstname": "Charlie", "lastname": "Davis", "email": "charlie@techcorp.com", "departmentId": 2, "departmentName": "Marketing", "companyName": "TechCorp"},

        {"id": 4, "firstname": "David", "lastname": "Wilson", "email": "david@techmicro.com", "departmentId": 3, "departmentName": "Sales", "companyName": "TechMicro"},

        {"id": 5, "firstname": "Mary", "lastname": "Johnson", "email": "mary@techmicro.com", "departmentId": 4, "departmentName": "HR", "companyName": "TechMicro"}

    ]

}


In this Python code, we need a ‘get_data’ function as a Flask route that handles GET requests to the /data endpoint by returning the ‘company_data’ dictionary as a JSON object.


@app.route('/data', methods=['GET'])

def get_data():

    return jsonify(company_data)


HTML Code

In HTML code, we need mainly a table to display the data:

    <h1>Company Data</h1>

    <table id="dataTable">

        <thead>

            <tr>

                <th>First Name</th>

                <th>Last Name</th>

                <th>Email</th>

                <th>Department Name</th>

                <th>Company Name</th>

            </tr>

        </thead>

        <tbody>

            <!-- Data will be dynamically inserted here -->

        </tbody>

    </table>


JavaScript Code

In the JavaScript code, we show how to initialize an IndexedDB by opening it and creating its stores. We will also show how we store the data in IndexedDB, and how to retrieve and display it.


To initialize an IndexedDB database, we create a function called ‘initializeDatabase()’ which we call once the document is ready as follows:


$(document).ready(function () {

    initializeDatabase();

});


Here is an overview of the purpose of the ‘initializeDatabase()’ function:


Opening the Database: The function starts by opening the IndexedDB database using the ‘indexedDB.open()’ method. The database name is set to ‘CompanyDB’ and the version is set to 1.

Handling the ‘onupgradeneeded’ Event: The ‘onupgradeneeded’ event is triggered when the database is created for the first time or when the version number is incremented. In this case, the function creates three object stores: ‘companyStore’, ‘departmentStore’, and ‘employeeStore’. Each object store has a key path set to ‘id’.

Handling the ‘onsuccess’ Event: When the database is successfully opened, the ‘onsuccess’ event is triggered. The function checks if data already exists in the database by calling the ‘checkExistingData()’ function. If data exists, it calls the ‘loadCachedData()’ function to display the cached data. If no data exists, it calls the ‘fetchDataFromBackend()’ function to fetch data from the back-end.

Handling the ‘onerror’ Event: If an error occurs while opening the database, the ‘onerror’ event is triggered. The function logs the error to the console.


function initializeDatabase() {

    const request = indexedDB.open("CompanyDB", 1);

    request.onupgradeneeded = function (event) {

        const db = event.target.result;

        if (!db.objectStoreNames.contains("companyStore")) {

            db.createObjectStore("companyStore", { keyPath: "id" });

        }

        if (!db.objectStoreNames.contains("departmentStore")) {

            db.createObjectStore("departmentStore", { keyPath: "id" });

        }

        if (!db.objectStoreNames.contains("employeeStore")) {

            db.createObjectStore("employeeStore", { keyPath: "id" });

        }

    };

    request.onsuccess = function (event) {

        const db = event.target.result;

        checkExistingData(db).then(function (dataExists) {

            if (dataExists) {

                console.log("Using cached data");

                loadCachedData(db);

            } else {

                console.log("Fetching data from backend...");

                fetchDataFromBackend(db);

            }

        });

    };

    request.onerror = function (event) {

        console.error("Error opening database:", event.target.error);

    };

}


The ‘checkExistingData()’ Function: This function checks if the ‘companyStore’ object store contains data of a given IndexedDB database (parameter ‘db’). The function creates a read-only transaction on the ‘companyStore’ object store. It gets a reference to the ‘companyStore’ object store. Then, it generates a Promise that will be resolved with a boolean value. It sends a count() request to the object store to retrieve the number of records.

If the request is successful, it resolves the Promise with true if there is at least one record (request.result > 0), and false otherwise. If the request fails (e.g., due to an error), it resolves the Promise with false.


function checkExistingData(db) {

    const transaction = db.transaction(["companyStore"], "readonly");

    const store = transaction.objectStore("companyStore");

    return new Promise(function (resolve) {

        const request = store.count();

        request.onsuccess = function () {

            resolve(request.result > 0);

        };

        request.onerror = function () {

            resolve(false);

        };

    });

}


The function ‘fetchDataFromBackend()’: This is an asynchronous function that takes an IndexedDB database object (‘db’) as an argument. In summary, the function sends a GET request to the /data endpoint using the fetch API. It waits for the response and parses it as JSON using ‘response.json()’. The resulting data is expected to be an object with three properties: companies, departments, and employees, each containing an array of objects. It creates a read-write transaction on three object stores: ‘companyStore’, ‘departmentStore’, and ‘employeeStore’.

It gets references to each of these object stores. It loops through each array of data (companies, departments, employees) and adds each object to its corresponding object store using the ‘add()’ method. It sets up an event handler for when the transaction is complete (‘transaction.oncomplete’). When the transaction is complete, it calls another function named ‘loadCachedData()’, passing the ‘db’ object as an argument.

If any error occurs during the execution of this function, it catches the error and logs it to the console with a message.

async function fetchDataFromBackend(db) {

    try {

        const response = await fetch("/data");

        const data = await response.json();

        const transaction = db.transaction(["companyStore", "departmentStore", "employeeStore"], "readwrite");

        const companyStore = transaction.objectStore("companyStore");

        const departmentStore = transaction.objectStore("departmentStore");

        const employeeStore = transaction.objectStore("employeeStore");

        data.companies.forEach((company) => companyStore.add(company));

        data.departments.forEach((department) => departmentStore.add(department));

        data.employees.forEach((employee) => employeeStore.add(employee));

        transaction.oncomplete = () => loadCachedData(db);

    } catch (error) {

        console.error("Error fetching data from backend:", error);

    }

}


The function ‘loadCachedData()’: This function loads data from an IndexedDB database (‘db’) to display it. In summary, this function creates a read-only transaction on three object stores: ‘companyStore’, ‘departmentStore’, and ‘employeeStore’. It gets references to each of these object stores. It retrieves all data from each object store using ‘getAll()’. When the data is retrieved, it calls the ‘displayData()’ function to display the data, passing the type of data (‘Company’, ‘Department’, or ‘Employee’) and the retrieved data as arguments.


function loadCachedData(db) {

    const transaction = db.transaction(["companyStore", "departmentStore", "employeeStore"], "readonly");

    const companyStore = transaction.objectStore("companyStore");

    const departmentStore = transaction.objectStore("departmentStore");

    const employeeStore = transaction.objectStore("employeeStore");

    companyStore.getAll().onsuccess = function (event) {

        displayData("Company", event.target.result);

    };

    departmentStore.getAll().onsuccess = function (event) {

        displayData("Department", event.target.result);

    };

    employeeStore.getAll().onsuccess = function (event) {

        displayData("Employee", event.target.result);

    };

}


The function ‘displayData()’: This function displays data in an HTML table. It takes two arguments: type (not used in the function) and data (an array of objects). It clears the contents of a table body element using ‘empty()’. It loops through each object in the data array, and for each object, it creates a table row (<tr>) with five columns containing the object’s properties: ‘firstname’, etc. Finally, it appends each table row to the table body element using ‘append()’.


function displayData(type, data) {

    const $tableBody = $("#dataTable tbody");

    $tableBody.empty();

    $.each(data, function(index, employee) {

        const row = `

            <tr>

                <td>${employee.firstname}</td>

                <td>${employee.lastname}</td>

                <td>${employee.email}</td>

                <td>${employee.departmentName}</td>

                <td>${employee.companyName}</td>

            </tr>

        `;

        $tableBody.append(row);

    });

}


Let’s take an example of how to query data from an indexedDB database.


The function ‘queryCompanyDB_getAllEmployees()’: This function retrieves all employees from ‘CompanyDB’. First, it opens the ‘CompanyDB" database. When the database is opened, the function creates a read-only transaction on the ‘employeeStore’ object store. It retrieves all employees from the ‘employeeStore’ using ‘getAll()’. When the data is retrieved, it logs the array of employees to the console.


function queryCompanyDB_getAllEmployees() {

    const dbRequest = indexedDB.open("CompanyDB");

    dbRequest.onsuccess = function(event) {

        const db = event.target.result;

        const tx = db.transaction("employeeStore", "readonly");

        const store = tx.objectStore("employeeStore");

        const request = store.getAll();

        request.onsuccess = function(event) {

            const employees = event.target.result;

            console.log(employees);

        };

    };

}


4. Running the example

If you run the example for the first time, you will see the following web page. The data is initially retrieved from the back-end.


If you go to Chrome DevTools ‘Application’, you can perform some operations like checking the contents of the stores or deleting the database.


Monday, November 11, 2024

Cache in JavaScript: Service Workers

 

Caching is a technique for storing data in a temporary storage area called a cache. It aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. In the first article, I introduced the different possibilities for caching data in JavaScript and how to choose the appropriate caching possibility according to your needs. In a previous article, I introduced an example of using the Web Storage API (localStorage and sessionStorage) to cache data. Then, I dedicated another article to using objects and data-* attributes to temporarily cache data in JavaScript. In this article, I will elucidate the use of service workers to cache static resources like HTML, CSS, and JavaScript files or API responses.


1. Service Workers API

This technique is used especially to cache static resources (HTML, CSS, etc.) or API responses. It helps enhance the responsiveness and performance of the web application. Please note that ‘service worker’ is a complex topic and it is impossible to cover all its features in this article. That’s why we focus only on the basic features to show how they cache resources and API responses.


The Service Worker API is a script that runs asynchronously and independently of the main browser thread.

- It is a kind of interface between the browser and the network.

- It is executed in the background.

- It handles tasks such as intercepting network requests and caching assets to allow the application to continue working even offline.


2. Tasks of Service Worker API

Let’s summarize the main tasks that service workers can perform when are ready to operate.


Interception of Network Requests: Service workers can intercept network requests and decide to handle them. Service workers can respond with cached resources or search data from the network according to some conditions like the network connection. If it is a poor connection, it responds to the client from cached resources, allowing fast loading and enhancing offline working. 


Background Sync: Service workers can synchronize data in the background, sending data to the server even if the app is not open. For example, they save user input while the connection is unavailable and send the data when the connection is back.


Caching: Service workers cache resources like HTML, CSS, JavaScript, and images. Thus the web app can be loaded faster and even continue working offline.


Push Notifications: Service workers handle push notifications so the web app sends messages to users even if the corresponding web page is closed.


3. Security Considerations

- Since service workers intercept network requests to make decisions, they must operate in a secure environment, which means they can only operate over HTTPS.

- Service workers run in a thread independent of the main thread, so they do not block the normal execution of the user interface.


4. Benefits of Service Workers

- Service workers improve the performance of web applications. Since they cache resources, they significantly reduce server calls, which reduces load times and strain on the server.

- Worker services allow offline access. In other words, users can interact with the application without an internet connection.

- Through push notifications, service workers keep users informed with regular updates.


5. Service Worker Lifecycle

The Service Worker lifecycle is event-driven and consists of several phases that describe how a Service Worker is installed, activated, and managed. The following table summarizes the key phases of a Service Worker's lifecycle. Note that a step can only be executed if the previous step has been completed successfully.

Service Worker Lifecycle

Phase

Definition

Code

Registration

A service worker must be registered by a page before it starts to work.

Method: ServiceWorkerContainer.register()

Download

A service worker is downloaded to the client.


Installation

- The browser installs the Service Worker.

- The install event is triggered.

- You can handle the install event to cache resources or complete an initial setup.

Event: install

Activation

- Once installed, the service worker is activated.

- Old caches are cleaned.

- The service worker gets ready to take control of the application.

Event: activate

Fetch

The service worker can intercept network requests using the fetch event and respond to them from the network or the cache.

Event: fetch


6. Example of Registering a Service Worker

Let’s take an example of how you can cache resources and API responses using the service worker API. For this, we need some back-end code to return data. A straightforward way to do this is using Python’s Flask framework. Additionally, we develop HTML, CSS, and JavaScript code for the front-end.


Note. We only write the relevant part here to explain how to use service workers to cache resources and intercept network requests. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/serviceworker-app-blog


Python Code

In the back-end code, we first define ‘employees_datas’ as a list containing a collection of employees, where each dictionary represents data about an employee and includes details such as its ID, first name, last name, and email.


We define some routes to enable the user to access web pages. For instance, to access the root URL of the application, the index function is called, and it renders the ‘index.html’ template file to display the content on the web page. 


We have also a route for ‘employee.html’ file and another one for ‘about.html’ file. Note that in the ‘employee’ function, the ‘render_template’ function renders the specified template file and passes the ‘employees_data’ variable to the template.


@app.route('/')

def index():

    # Render the index.html file

    return render_template('index.html')


@app.route('/employee')

def employee():

    return render_template('employee.html', employees_data=employees_data)


HTML Code

In the front-end code, we define a few HTML files, ‘index.html’ as the main web page, ‘employee.html’ to display some content sent from the back-end, and ‘about.html’. An additional file called ‘layout.html’ will be defined as a common template that will be extended by the other files.


This HTML template provides the structure for a basic web page that uses the Bootstrap library for styling, jQuery for JavaScript functionality, and placeholders for dynamic content through a template engine ‘Jinja’ of the Flask framework.


The {% block swjs %}{% endblock %} block is a placeholder for a service worker script that can be inserted by the template engine only in the ‘index.html’ template.


Inside the ‘navbar’, there are different navigation links to different parts of the website:

- ‘Home’ links to ‘/’, which consists of loading the ‘index.html’ template.

- ‘Employee’ links to ‘/employee’, which consists of loading the ‘employee.html’ template.

- ‘About’ links to ‘/about’, which consists of loading the ‘about.html’ template.


<div class="container"> holds the page’s main content and uses Bootstrap’s container class for responsive layout.

{% block content %}{% endblock %} serves as a placeholder for page-specific content that other templates can insert.


<head>

    ...

    {% block swjs %}{% endblock %}

</head>

<body>

    <nav class="navbar navbar-expand-lg navbar-light bg-primary">

        <a class="navbar-brand" href="/">SW Demo</a>

        <div class="collapse navbar-collapse">

            <ul class="navbar-nav mr-auto">

                <li class="nav-item">

                    <a class="nav-link" href="/">Home</a>

                </li>

                <li class="nav-item">

                    <a class="nav-link" href="/employee">Employee</a>

                </li>

                <li class="nav-item">

                    <a class="nav-link" href="/about">About</a>

                </li>

            </ul>

        </div>

    </nav>


    <div class="container">

        {% block content %}{% endblock %}

    </div>

</body>

</html>


Within the ‘index.html’ file, we have the following code:


{% extends "layout.html" %}


{% block swjs %}

    <script type="text/javascript" src="/static/js/main.js"></script>

{% endblock %}


{% block content %}

    <h1>Welcome to the Home Page</h1>

    <p>This is the home page of the Service Worker API Demo</p>

{% endblock %}


Let’s explain the main parts of this code:


{% extends "layout.html" %} indicates that this template file extends the ‘layout.html’ file,  meaning it inherits the structure and styling defined in ‘layout.html’.


{% block swjs %} and {% endblock %} define a block named ‘swjs’ where you can insert content specific to JavaScript files. In this case, it includes a script tag that links to the ‘main.js’ file located in the /static/js/ directory.


{% block content %} and {% endblock %}: This defines a block named content where you can insert the main content of the page. In this case, there is an <h1> heading and a <p> paragraph welcoming users to the Home Page of the Service Worker API Demo.


The content of the employee template in the file ‘employee.html’ is suggested as follows:


{% extends "layout.html" %}


{% block content %}

    <h1>Employee List</h1>

    <p>This is the employee List</p>

    <table>

        <thead>

            <tr>

                <th>Firstname</th>

                <th>Lastname</th>

                <th>Email</th>

            </tr>

        </thead>

        <tbody id="myTable">

            {% for item in employees_data %}

                <tr>

                    <td>{{ item.first_name }}</td>

                    <td>{{ item.last_name }}</td>

                    <td>{{ item.email }}</td>

                </tr>

            {% endfor %}

        </tbody>

    </table>

{% endblock %}


The above code is a Jinja template that extends a layout defined in ‘layout.html’. Within the {% block content %}, it creates an Employee List page with a table displaying employee data. The table has columns for Firstname, Lastname, and Email. It then loops through the ‘employees_data’ list and populates the table rows with the corresponding data for each employee’s first name, last name, and email address.


JavaScript Code

In the JavaScript code, we show how to register and use a service worker to cache data. Two JavaScript files are defined. The ‘main.js’ file is defined to register a service worker and the ‘sw.js’ file is used to encapsulate the service worker's behavior.


In the ‘main.js’ file, the code waits for the document to be fully loaded. Once the document is ready, it calls the ‘registerServiceWorker()’ function.


$(document).ready(function(){

    registerServiceWorker();

});


function registerServiceWorker() {

    console.log('Register Service Worker ...')

    if ('serviceWorker' in navigator) {

        navigator.serviceWorker.register(href = 'sw.js')

        .then(function(registration) {

            console.log('Service Worker registered with scope:', registration.scope);

        })

        .catch(function(error) {

            console.log('Service Worker registration failed:', error);

        });

    }

}


Function ‘registerServiceWorker()’: This function checks if the browser supports service workers by checking if ‘serviceWorker’ is in ‘navigator’. If service workers are supported, it attempts to register a service worker by calling ‘navigator.serviceWorker.register('sw.js')’. If the registration is successful, it logs a message to the console with the service worker’s scope. Note that the scope is the URL path that determines which pages the service worker will control. If the registration fails, it logs an error message to the console.


In the ‘sw.js’ file, we define the main behaviors of the service worker. This consists of handling the ‘install’, ‘activate’, and ‘fetch’ events. Thus we can install and activate the service workers. The ‘fetch’ event handler will intercept network requests to respond to the client from the cache.


First, we need a few global variables like the cache name (‘cache_name’) and the URLs we want to cache (‘urls_to_cache’).


// Cache name

const cache_name = 'sw-1';


// Urls to cache

const urls_to_cache = [

    '/',

    '/employee',

    '/about',

    '/static/css/styles.css',

    '/static/js/main.js',

    '/static/js/employee.js',

];


We add an event listener for the install event in the service worker as follows:


self.addEventListener('install', function(event) {

    console.log('Installing ...');

    event.waitUntil(

        caches.open(cache_name).then(function(cache) {

            console.log('Caching data ...');

            return cache.addAll(urls_to_cache);

        })

    );

}); 


When the service worker is installed, it performs some actions.  It uses ‘event.waitUntil()’ to extend the installation process until the provided promise resolves successfully. This means the service worker will only be considered installed if everything within ‘waitUntil’ (in this case, the caching process) completes without errors. 


It opens a cache (or creates one if it doesn’t exist) with the name specified by ‘cache_name’. Once the cache is opened, it adds all the URLs listed in the ‘urls_to_cache’ array to the cache for offline access. Note that the ‘Self’ word inside the service worker script refers to the Service Worker itself.


The activate event listener triggers when the service worker becomes active. This usually happens after installation and when there are no more active clients using the previous version of the service worker. The following code snippet is a listener for the ‘activate’ event:


// Activate event: Clean up old caches

self.addEventListener('activate', event => {

    console.log('Activating ...');

    const cacheList = [cache_name];

    event.waitUntil(

        caches.keys().then(cacheNames => {

            return Promise.all(

                cacheNames.map(cName => {

                    if (!cacheList.includes(cName)) {

                        console.log('Deleting old cache:', cName);

                        return caches.delete(cName);

                    }

                })

            );

        })

    );

});


When a service worker is activated, it means it is ready to take control of the page and handle caching and other tasks. Let’s explain what the code does. This code defines an array ‘cacheList’ containing the value of ‘cache_name’. This array specifies the caches to keep. Only the caches in this array will be kept, the others will be deleted.


It uses ‘event.waitUntil(...)’ to delay the activation until all old caches are deleted. This ensures a clean start with only the necessary caches.  Inside ‘event.waitUntil(...)’, it calls caches.keys() to get a list of all the cache names. It then uses ‘Promise.all(...)’ to map over each cache name and check if it's in the ‘cacheList’. If a cache name is not in the ‘cacheList’, it logs the cache to be deleted to the console and deletes that cache using ‘caches.delete(cName)’.


We now develop a common event listener that intercepts the ‘fetch’ event, which is triggered whenever a network request is made.


self.addEventListener("fetch", (event) => {

    console.log('Fetching data ...');

    // Intercept the network request to handle it

    event.respondWith(

        (async () => {

        // Search the response first in a cache

        let cache = await caches.open(cache_name);

        let cachedResponse = await cache.match(event.request);

        console.log("cachedResponse:", cachedResponse)


        // Return a response if it is found in the cache

        if (cachedResponse) return cachedResponse;


        // If no response is already cached, use the network

        return fetch(event.request);

    })(),

    );

});


After logging 'Fetching data ...' to the console, we call the ‘event.respondWith()’ method to intercept the network request and handle it. Within the ‘respondWith()’ method, we first try to find a response in a cache by opening a cache with the name ‘cache_name’ and attempting to match the request with the cached response. If a cached response is found, we return the cached response. If no cached response is found, we make a network request using fetch (‘event.request’).


7. Execution of the Example

When you run this example on the server, you get the following UI:

Visit all the pages and then stop the server. You will see that the application continues to work offline and the pages are showing normally.

Blog Posts

Enhancing Performance of Java-Web Applications

Applications built with a Java back-end, a relational database (such as Oracle or MySQL), and a JavaScript-based front-end form a common and...