IndexedDB in Dart

WARNING: As of 2012-02-27, and after further hacking, there are two blocking issues preventing IndexedDB from working properly. You can't delete an object by a key, and retrieved objects aren't actually objects.

IndexedDB is the new way to store key and indexed objects for offline retrieval in web apps. Learn how to use IndexedDB in your Dart application.

(Warning: this code isn't pretty, and many bugs were filed. This should get cleaned up soon. Regardless, it works! Also, you'll need the bleeding_edge branch of code, as of 2012-02-23.)

Why offline data storage?

Use client side local data storage to reduce bandwidth bills, decrease latency, increase UI performance, and allow your application to run offline.

Local data storage options

There are three options for storing local data for your web apps.

Local Storage (aka Web storage) is a very simple key/value store, however it can only store strings and is a blocking/synchronous API. Local Storage is better than cookies, but try to avoid it. Local Storage does have good cross browser coverage.

WebSQL is a SQLite database in the browser, supported by Webkit based browsers. WebSQL is powerful and fairly well understood, but will not be implemented in Firefox or Internet Explorer, and the spec is in limbo.

IndexedDB is on a standards track, and does have Chrome, Firefox, and (soon) Internet Explorer implementations. IndexedDB is the future of offline, local object storage for your web apps.

Ideally, you should use something like Lawnchair as an interface for object storage, and let the library implementation choose the best storage available. Lawnchair isn't ported to Dart... yet... so we'll look at direct IndexedDB code.

IndexedDB Reference

IndexedDB is a bit of a moving target. I used a combination of the Simple Todo List Using HTML5 IndexedDB from HTML5 Rocks and the MDN reference docs for IndexedDB. Of course, I also consulted the Dart API Docs.

The Bugs

During this exercise, I encountered and filed the following bugs:
Even with all the bugs above, you can still run IndexedDB in Dart. A big thanks to the Dart engineers that quickly fixed the blocking bugs.

The Code

IndexedDB currently works best with dart:dom. I used types so the Dart Editor helped me out with code completion and early error checks.

If you're not familiar with IndexedDB, this will look verbose to you. The API has been designed to use callbacks whenever possible to help ensure the main UI thread of your web app isn't blocked on slow IO calls.

-----------------------------------------------

#import('dart:dom', prefix:'dom');
#import('dart:html');

String VERSION = "1";
String TODOS_STORE = 'todos';

initDb(db) {
  if (VERSION != db.version) {
    dom.IDBVersionChangeRequest versionChange = db.setVersion(VERSION);
    versionChange.addEventListener('success', (e) {
      print("Set version");
      db.createObjectStore(TODOS_STORE);
      displayItems(db);
    });
    versionChange.addEventListener('error', (e) {
      print("Could not set version: $e");
    });
  } else {
    print("DB is at version ${db.version}");
    displayItems(db);
  }
}

displayItems(dom.IDBDatabase db) {
  dom.IDBTransaction txn = db.transaction(TODOS_STORE, dom.IDBTransaction.READ_ONLY);
  dom.IDBObjectStore objectStore = txn.objectStore(TODOS_STORE);
  dom.IDBRequest cursorRequest = objectStore.openCursor();
  cursorRequest.addEventListener("success", (e) {
    dom.IDBCursor cursor = e.target.result;
    if (cursor != null) {
      renderItem(cursor.value);
      cursor.continueFunction();
    }
  });
  cursorRequest.addEventListener('error', (e) {
    print("Could not open cursor: $e");
  });
}

renderItem(value) {
  var ul = document.query('#items');
  var li = new Element.tag("li");
  li.text = value;
  ul.elements.add(li);
}

addItem(db) {
  var msg = "Stuff ${new Date.now().value}";
  dom.IDBTransaction txn = db.transaction(TODOS_STORE, dom.IDBTransaction.READ_WRITE);
  dom.IDBObjectStore objectStore = txn.objectStore(TODOS_STORE);
  dom.IDBRequest addRequest = objectStore.put(msg, new Date.now().value);
  addRequest.addEventListener("success", (e) {
    renderItem(msg);
  });
  addRequest.addEventListener("error", (e) => print("Could not add: $e"));
}

registerButtons(db) {
  var add = document.query("#add");
  add.on.click.add((e) {
    addItem(db);
  });
}

// Chrome only for now,
// see bug http://code.google.com/p/chromium/issues/detail?id=108223
void main() {
  dom.IDBRequest request = dom.window.webkitIndexedDB.open('todo');
  request.addEventListener('success', (e) {
    print("Opened DB");
    dom.IDBDatabase db = e.target.result;
    // the following is Chrome only for now :(
    initDb(db);
    registerButtons(db);
  });
  request.addEventListener('error', (e) {
    print('Could not open db: $e');
  });
}

-----------------------------------------------

The above code only works in Chrome right now, as unfortunately Chrome hasn't caught up with the spec yet. With just a tiny bit of tweaking, you'll be able to get this to work in Firefox, too.

You can fork this code from the original Gist.

Summary

IndexedDB is a key/object store with support for indexes, and it is the eventual replacement for WebSQL database for complex object storage requirements. The API is heavily callback driven, ensuring the main browser UI thread isn't blocked by expensive I/O calls.

IndexedDB (at least, creating a database, reading entries, and adding entries) now works with Dart when compiled to JavaScript! Though we're still waiting for direct Dartium support, you can begin to add IndexedDB to your modern web apps.

Popular posts from this blog

Lists and arrays in Dart

Converting Array to List in Scala

Null-aware operators in Dart