dyaa
6/10/2017 - 9:50 PM

Indexed DB URLs via Service Workers

Indexed DB URLs via Service Workers

var dbs = new Map(); // name --> Promise<IDBDatabase>

self.addEventListener('fetch', event => {
  if (event.request.method !== 'GET') return;
  var url = new URL(event.request.url);
  if (url.hostname !== 'indexeddb.test') return;

  var parts = url.pathname.split('/');
  var database = parts[1];
  var store = parts[2];
  var index = parts[3];
  var query = new Map(url.search.substring(1).split('&').map(kv => kv.split('=')));
  var key = query.get('key');
  var path = query.get('path');

  if (!dbs.has(database)) {
    dbs.set(database, new Promise((resolve, reject) => {
      var request = indexedDB.open(database);
      // Abort the open if it was not already populated.
      request.onupgradeneeded = e => request.transaction.abort();
      request.onerror = e => reject(request.error);
      request.onsuccess = e => resolve(request.result);
    }));
  }

  event.respondWith(
    dbs.get(database).then(db => new Promise((resolve, reject) => {
      var tx = db.transaction(store);
      var request = !index
            ? tx.objectStore(store).get(key)
            : tx.objectStore(store).index(index).get(key);

      request.onerror = e => reject(request.error);
      request.onsuccess = e => {
        var result = request.result;
        if (path) path.split('.').forEach(id => { result = result[id]; });
        resolve(new Response(result));
      };
    })));
});
<!DOCTYPE html>
<meta charset=utf-8>
<title>Indexed DB URLs via Service Worker</title>
<script>
var records = [
  {name: 'alex', url: 'https://avatars0.githubusercontent.com/u/97331?v=3&s=200'},
  {name: 'jungkee', url: 'https://avatars1.githubusercontent.com/u/1331169?v=3&s=200'},
  {name: 'jake', url: 'https://avatars1.githubusercontent.com/u/93594?v=3&s=200'}
];

// Fetch the images
Promise.all(records.map(
  record => fetch(record.url)
    .then(response => response.blob())
    .then(blob => { record.image = blob; })
))
  .then(() => {

    indexedDB.deleteDatabase('resources');
    var open = indexedDB.open('resources');

    // Set up the database schema
    open.onupgradeneeded = () => {
      var db = open.result;
      var store = db.createObjectStore('records', {autoIncrement: true});
      store.createIndex('by_name', 'name');
    };

    open.onsuccess = () => {
      var db = open.result;

      // Store the images into the database
      var tx = db.transaction('records', 'readwrite');
      var store = tx.objectStore('records');
      records.forEach((record) => store.put(record));

      tx.oncomplete = () => {
        db.close();

        // Register the service worker
        navigator.serviceWorker.register('sw.js', {scope: '/'})
          .then(registration => registration.installing)
          .then(worker => {
            worker.addEventListener('statechange', () => {
              if (worker.state !== 'activated') return;

              // Add a controlled iframe.
              document.querySelector('iframe').src = 'frame.html';
            });
          });
      };
    };
});

</script>
<iframe id="frame" style="width: 700px; height: 300px;"></iframe>
<!DOCTYPE html>
<meta charset=utf-8>
<title>demo iframe</title>

<h1>The usual suspects:</h1>

<img src="http://indexeddb.test/resources/records/by_name?key=alex&amp;path=image">
<img src="http://indexeddb.test/resources/records/by_name?key=jungkee&amp;path=image">
<img src="http://indexeddb.test/resources/records/by_name?key=jake&amp;path=image">

URLs into Indexed DB, via Service Workers

Let's say you're using Indexed DB for the offline data store for a catalog. One of the object stores contains product images. Wouldn't it be great if you could just have something like this in your catalog page?

<img src="indexeddb/database/store/id">

You can do this with Service Workers without browsers having to implement it! All you need to do is carve out the URL namespace intercepted by the fetch handler in your service worker and decide how to map URLs to database queries.

This example uses the imaginary host indexeddb.test to provide URLs of the form:

  • http://indexeddb.test/$DATABASE/$STORE?key=$KEY
  • http://indexeddb.test/$DATABASE/$STORE?key=$KEY&path=$PATH
  • http://indexeddb.test/$DATABASE/$STORE/$INDEX?key=$KEY
  • http://indexeddb.test/$DATABASE/$STORE/$INDEX?key=$KEY&path=$PATH

The path query parameter is optional. If used, it is evaluated as key path against the record returned by the database, allowing only part of the record to be served up (e.g. an image stored as a Blob as part of a record).

The example here sets up a database named resources which contains an object store named records with an index by_name. The following URL is used to retrieve records from that index, and respond with the image property of the record.

<img src="http://indexeddb.test/resources/records/by_name?key=alex&amp;path=image">

Notes on defining schemes

Indexed DB keys are typed, e.g. they can be strings, numbers, etc. The key "1" (string) and the key 1 (number) are distinct, so if you're using numeric keys you'll need to convert URL substrings to number e.g. k = Number(s) before querying the database. If your use case requires both numeric and string keys in same application, one approach would be to use JSON.parse() on the key values, so string keys would look like /db/store?key="foo" while numeric keys would simply be /db/store?key=123

Using / as a delimiter puts restrictions on the names for databases/stores/indexes that aren't present in the spec. So be sure to come up with a scheme that will support your naming conventions.