Skip to content

Instantly share code, notes, and snippets.

@oilsinwater
Last active February 13, 2018 11:42
Show Gist options
  • Save oilsinwater/b55a4fdb68dbffe66532bf05afa7d5c7 to your computer and use it in GitHub Desktop.
Save oilsinwater/b55a4fdb68dbffe66532bf05afa7d5c7 to your computer and use it in GitHub Desktop.
Working with Live Data in the Service Worker

[TOC]

Working with Live Data in the Service Worker

What's the best place to store data?

Imagining the example of an typical e-commerce site with multiple kinds of data, there are images and numeric data, as well as HTML and CSS resources, and probably JavaScript as well. The question here is, which data should go where?

General Guidelines

put in Cache resources addressed through a URL will be stored in the cache; i.e assets, static, URL addressable

App->Cache API: data (urls)
Network->App: data (urls)

put in IndexedDB dynamic data, json data

App->IndexedDb: data (json)
Network->App: data (json)

Tip: Start by storing data locally.

Put JSON data in indexedDB

Here we're using the cache API and the IndexedDB API, also known as IDB. Let's look at storing JSON data with IDB. So what does IDB look like? It does not look like a conventional SQL table. IDB data is stored as key value pairs in objects stores. A single IDB database can have multiple object stores. This could be a clothing object store inside a products database.

# Key (key-path 'id') Value
0 123 {id:123, name:'jacket', price: 22.13, quantity: 10}
1 987 {id:987, name:'sweater', price: 50.20, quantity: 20}
2 456 {id:456, name:'hoodie', price:45.65, quantity: 5}

Store data on activate

Now the activate event is a good place to create an IDB database.

self.addEventListener('activate',
  function (event) {
    event.waitUntil(
      createDB()
      );
    });
  1. you don't want to create it more often than needed for efficiency's sake.
  2. doing that during installation could cause issues with the existing service worker.

Note: event.waitUntil ensures that the service worker does not terminate preemptively during async actions.

Create an IDB database

function createDB() {
  productsDB = idb.open('products', 1
     function (upgradeDB) {
       var store = upgradeDB.createObjectStore(
         'clothing', {keyPath: 'id'}
         );
         store.put(
           {id: 123, name: 'jacket',
           price: 12.15, quantity: 20}
           );
         });
       }

Here we create an IDB products database. It is version one. Inside the products database we create a clothing object store. This will hold all of the clothing objects. The clothing object store has a key path of ID. Now this means that the objects in this store will be organized and accessed by the ID property of the clothing objects.

Note: that we're using Jake Archibald's IndexedDB promised library to enable promised syntax with IDB.

Cache assets on install

self.addEventListener('install',
function (event) {
  event.waitUntil(
    cacheAssets()
    );
  });

So let's look at storing resources with the cache API. Now, a common pattern is to cash assets on service worker installation.

Note: that event.waitUntil ensures that the service worker does not terminate preemptively during async actions

###Caching assets

function cacheAssets() {
  return caches.open('cache-v1')
  .then(function (cache) {
    return cache.addAll([
      'index.html',
      'styles/main.css',
      'img/jacket.jpg'
      ]);
    });
 }

In this example, we create a cache v1 cache and store static assets-- that's HTML, CSS, JavaScript, images and so on-- with the cache API.

Now we can get data from IDB instead of the network.

App->IndexedDb: data (json)
IndexedDb->App: data (json)
Network-->App: X

Get clothing data for UI

function readDB() {
  productsDB
  .then(function (db) {
    var tx = db.transaction(
      ['clothing'], 'readonly'
    );
    var store = tx.objectStore('clothing');
    return store.getAll();
    })
    .then(function(items) {
    /* Use clothing data */
    });
  }

Above in the example, we open the products database and create a new transaction on the clothing store of read only type. We don't need to write data. We can then access the store and retrieve all of the items. These items can then be used to update the UI, or whatever is needed.

note: a word on transactions. These are a wrapper around an operation, or group of operations, to ensure database integrity. If one of the actions within a transaction fails none of them are applied and the database returns to the state it was in before the transaction began. All read or write operations in IndexedDB must be part of a transaction. This allows for atomic read, modify, or write operations without worrying about other threads acting on the database at the same time.

Fetch from the Cache

App->Cache API: data (urls)
Cache API->App: data (urls)
Network-->App: X
self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request).then(
    function(response) {
      // check cache but fallback to network
      return response || fetch(event.request);
      })
    );
  });

Now we can also get resources like HTML, CSS, JavaScript, and images from the cache instead of the network. Here we add a fetch listener to the service worker. When a fetch is made for a resource, we try to find the resource in the cache and return it. However, if it isn't in the cache we still want to go to the network for it.

Offline user actions

App->IndexedDB: data (urls)
App-->Network: X
Network-->App: X

What can we do about actions that a user takes when they're offline? For example, purchasing a product. We can record them in IDB. It's possible to record actions a user takes while offline.

####Record failed requests

fetch('purchaseUrl', {
  method: 'post',
  body: 'item-123'}
  )
  .then(function (response) {
    // normal tx
  })
  .catch(function (error) {
  // save item/action in IDB
  });

In the example above a user is trying to make an item purchase via HTTP request, which will fail if offline. If the purchase fails, the catch block will execute. This is where we could store the item or user action in IDB.

Question: How do we use this once connectivity returns?

IndexedDB->App: data 
Network->App: data

Re-send recorded requests

// Get save ditesm from IDB, then 
fetch('purchaseUrl', {
  method: 'post',
  body: item
  })
  .then(function (response) {
  // Success. Delete IDB/action
});

Well, once connectivity returns the recorded items or actions in IDB can be retrieved and sent. If the requests are successful, the corresponding item action can be deleted from the IDB records.

Resources

  • Offline Storage PWA's
  • Detailed support for testing
  • support for IndexedDB
  • IndexedDB Promise
  • Pokedex.org
  • Cache API support
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment