JavaScript, Dexie, and the Pursuit of Resilience.
Tags: javascript, webdev, programming, frontend
Published: 2024-01-19 07:06:58

I once built a web application that needed to work even if the internet connection was unstable, so essentially the web app supported an offline mode.

In this article, I will go over the method I used to solve this problem using regular old JavaScript, and a neat feature of modern browsers.

## What is the purpose of the web app?

The application was built to facilitate meal ordering. Agents would have the app on their tablet or laptop and travel around campus to collect orders.

Some of these areas were remote and did not have a good internet signal throughout.

Since orders are always placed ahead of time for the next day or a future date, it's okay if there is a delay in sending the order to the backend.

Furthermore, since it's a subscription service, no credit card or billing information is processed when placing an order.

## IndexeDB

IndexeDB is essentially a lightweight database that is available via a browser API. You can think of it as SQLite for the browser(Although, it's a NoSQL database).

![Browser DB](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3dlsyvgius0s1o49v020.jpg)

Since this DB lives in the browser and uses the local browser storage to save data, we can use it without needing to connect to an external DB server. Which is perfect for offline data storage.

> ⚠️ IndexeDB is reliant on the user's browser and system resources. Storing too much data here can cause slow page loads and even worse - crash the user's browser.

I dislike the native [API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). It's easy enough to use but very verbose, hence why I choose Dexie. 

## What is Dexie?

Dexie is a tiny wrapper around the native IndexedDB API, allowing for a much cleaner and ORM-style syntax.

Here is a small example demonstrating how simple it is to use Dexie:

```javascript
const db = new Dexie("MyAppDb");
const people = await db.people
    .where('age')
    .above(75)
    .toArray();
```

Besides being clean and easy to use, Dexie also provides a set of convenient packages for various JavaScript libraries including React, Vue, Angular, and so forth.

Learn more about Dexie [here](https://dexie.org/docs/Tutorial/Getting-started).

## Detecting if we are offline

In JavaScript, the "navigator" provides the "onLine" property which indicates if the user has an active internet connection or not.

I did, however, find that this approach was not very reliable - sometimes it returned a false positive, even though my network was disconnected.

The most effective approach I found, was to simply make a ping request to the API every 30 seconds. In addition to testing the internet connection, I could also perform some health checks on the API.

```javascript
const offlineManager = new OfflineManager();

async function pingAPI() {
  try {
    const response = await fetch(
       'https://mywebsite.com/api/pong'
    );

    if (response.ok) {
      offlineManager.setIsOnline();
    } else {
      offlineManager.setIsOffline();
    }
  } catch (error) {
    offlineManager.setIsOffline();
  }
}

setInterval(pingAPI, 30000);
```

> ℹ️ If you are wondering why this is necessary every 30 seconds instead of when an order is placed, this version is very "dumbed down" for illustration purposes. The manager does various other tasks - like returning delayed messages, filling menu caches, and more. Web sockets could also be used - however, they come at a cost, which was overkill for this particular application.

Some of the core functionality of the manager class:

1) When online - check if any orders are waiting to be sent to the backend. If "Yes", loop through those and send them to the API.

2) Manage the online/offline state.

3) Expose a placeOrder method. If we are offline - queue in the local IndexeDB, otherwise send to the backend API immediately.

4) Relay any API messages to the web app for display.

## Putting it all together


![Process flow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uw8skrwwut47lxx76wdq.png)

When an order is placed via the web app; the app is unaware of the network status, it simply doesn't care. This makes it easy to implement a plug-and-play offline mode, without too many changes to the application code.

The web app simply calls a function as follows:

```javascript
const result = offlineManager.placeOrder(order)
if (result.status == 'ok') {
....
} else {
....
}
```

In the background, the manager will check if there is an active connection to the API server. If all is okay - the order is posted to the API and the result is returned to the caller.

If there is no active connection, the manager inserts a row into the orders table locally and then returns a success message.

The success message is slightly different though, as the user placing the order needs to be aware, that the order is queued locally and has not been sent to the API yet.

Later when the order is finalized, a toast message will appear to give the user confirmation, or if something fails - some pathway to correct issues.

when the following function is called, in addition to setting the status, we also check if there are any orders in the queue.

```javascript
offlineManager.setIsOffline();
```

If we have orders waiting, a background process kicks off to send those orders to the server.

> ℹ️ As a side note, setInterval can cause overlapping function calls and therefore trigger the same order to be sent to the API multiple times. To prevent this - the manager implements an internal lock, similar to a [Mutex](https://dev.to/kwnaidoo/what-is-a-mutex-in-golang-p0e). Furthermore, there's also a "status" field set in indexeDB for each order as it moves through the process.