Skip to content

Instantly share code, notes, and snippets.

@inexorabletash
Last active December 20, 2023 01:29
Show Gist options
  • Save inexorabletash/687e7c5914049536f5a3 to your computer and use it in GitHub Desktop.
Save inexorabletash/687e7c5914049536f5a3 to your computer and use it in GitHub Desktop.
Indexed DB URLs via Service Workers

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.

<!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">
<!DOCTYPE html>
<meta charset=utf-8>
<title>Indexed DB URLs via Service Worker</title>
<script>
// Copyright 2019 Google LLC.
// SPDX-License-Identifier: Apache-2.0
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>
// Copyright 2019 Google LLC.
// SPDX-License-Identifier: Apache-2.0
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));
};
})));
});
@YurySolovyov
Copy link

Would be nice to see a module for that.

@yangnianbing
Copy link

yangnianbing commented Apr 27, 2017

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';
         });
       });
   };

if service worker have been activated aready, worker is null and worker.addEventListener report error

@kordob29
Copy link

kordob29 commented Jul 5, 2022

Unfortunately it returns always an error :
"GET http://indexeddb.test/ net::ERR_NAME_NOT_RESOLVED"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment