Home | History | Annotate | Download | only in indexeddb
      1 <html><title>IndexedDB Tutorial</title>
      2 <script>
      3 
      4 // This is a tutorial that highlights many of the features of IndexedDB along witha number of
      5 // caveats that currently exist in Chromium/WebKit but which will hopefully be improved upon
      6 // over time.
      7 //
      8 // The latest version of the spec can be found here:
      9 // http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html but note that there are quite a
     10 // few bugs currently opened against it and some major unresolved issues (like whether dynamic
     11 // transactions should be in for v1). Many of the bugs are filed here:
     12 // http://www.w3.org/Bugs/Public/buglist.cgi?query_format=advanced&short_desc_type=allwordssubstr&short_desc=&component=Indexed+Database+API&longdesc_type=allwordssubstr&longdesc=&bug_file_loc_type=allwordssubstr&bug_file_loc=&status_whiteboard_type=allwordssubstr&status_whiteboard=&keywords_type=allwords&keywords=&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&emailtype1=substring&email1=&emailtype2=substring&email2=&bug_id_type=anyexact&bug_id=&votes=&chfieldfrom=&chfieldto=Now&chfieldvalue=&cmdtype=doit&order=Reuse+same+sort+as+last+time&known_name=IndexedDB&query_based_on=IndexedDB&field0-0-0=noop&type0-0-0=noop&value0-0-0=
     13 // Discussion happens on public-webapps (a] w3.org
     14 //
     15 // Although not user friendly, additional capabilities and example code can be found in the
     16 // tests for IndexedDB which are here:
     17 // http://trac.webkit.org/browser/trunk/LayoutTests/storage/indexeddb
     18 //
     19 // This document is currently maintained by Jeremy Orlow <jorlow (a] chromium.org>
     20 
     21 
     22 // This is not an ideal layout test since it doesn't verify things as thoroughly as it could,
     23 // but adding such content would make it much more cluttered and thus wouldn't serve its primary
     24 // goal of teaching people IndexedDB. That said, it does have a good amount of coverage and
     25 // serves as a living document describing what's expected to work and how within WebKit so it
     26 // seems well worth having checked in.
     27 if (window.layoutTestController) {
     28     layoutTestController.dumpAsText();
     29     layoutTestController.waitUntilDone();
     30 }
     31 
     32 
     33 function setup()
     34 {
     35     // As this API is still experimental, it's being shipped behind vendor specific prefixes.
     36     if ('webkitIndexedDB' in window) {
     37         indexedDB = webkitIndexedDB;
     38         IDBCursor = webkitIDBCursor;
     39         IDBKeyRange = webkitIDBKeyRange;
     40         IDBTransaction = webkitIDBTransaction;
     41     }
     42 
     43     // This tutorial assumes that Mozilla and WebKit match each other which isn't true at the
     44     // moment, but we can hope it'll become true over time.
     45     if ('moz_indexedDB' in window) {
     46         indexedDB = moz_indexedDB;
     47         // Not implemented by them yet. I'm just guessing what they'll be.
     48         IDBCursor = moz_IDBCursor;
     49         IDBKeyRange = moz_IDBKeyRange;
     50         IDBTransaction = moz_IDBTransaction;
     51     }
     52 }
     53 
     54 function log(txt)
     55 {
     56     document.getElementById("logger").innerHTML += txt + "<br>";
     57 }
     58 
     59 function logError(txt)
     60 {
     61     log("<font color=red>" + txt + "</font>");
     62 }
     63 
     64 function start()
     65 {
     66     setup();
     67 
     68     // This is an example of one of the many asynchronous commands in IndexedDB's async interface.
     69     // Each returns an IDBRequest object which has "success" and "error" event handlers. You can use
     70     // "addEventListener" if you'd like, but I'm using the simpler = syntax. Only one or the other
     71     // will fire. You're guaranteed that they won't fire until control is returned from JavaScript
     72     // execution.
     73     var request = indexedDB.open("tutorialDB");
     74     request.onsuccess = onOpen;
     75     request.onerror = unexpectedError;
     76 }
     77 
     78 function unexpectedError()
     79 {
     80     // If an asynchronous call results in an error, an "error" event will fire on the IDBRequest
     81     // object that was returned and the event's code and message attributes will be populated with
     82     // the correct values.
     83     logError("Error " + event.code + ": " + event.message);
     84 
     85     // Unfortunately, Chromium/WebKit do not implicitly abort a transaction when an error occurs
     86     // within one of its async operations. In the future, when an error occurs and the event is
     87     // not canceled, the transaction will be aborted.
     88     if (currentTransaction)
     89         currentTransaction.abort();
     90 }
     91 
     92 function onOpen()
     93 {
     94     // If an asynchronous call results in success, a "success" event will fire on the IDBRequest
     95     // object that was returned (i.e. it'll be the event target), which means that you can simply
     96     // look at event.target.result to get the result of the call. In some cases, the expected
     97     // result will be null.
     98     window.db = event.target.result;
     99 
    100     // The IDBDatabase object has a "version" attribute. This can only be set by calling
    101     // "setVersion" on the database and supplying a new version. This also starts a new
    102     // transaction which is very special. There are many details and gotchas surrounding
    103     // setVersion which we'll get into later.
    104     if (db.version == "1.0") {
    105         // We could skip setting up the object stores and indexes if this were a real application
    106         // that wasn't going to change things without changing the version number. But since this
    107         // is both a tutorial and a living document, we'll go on and set things up every time we run.
    108     }
    109     var request = db.setVersion("1.0");
    110     request.onsuccess = onSetVersion;
    111     request.onerror = unexpectedError;
    112 }
    113 
    114 function onSetVersion()
    115 {
    116     // We are now in a setVersion transaction. Such a transaction is the only place where one
    117     // can add or delete indexes and objectStores. The result (property of the request) is an
    118     // IDBTransaction object that has "complete" and "abort" event handlers which tell
    119     // us when the transaction has committed, aborted, or timed out.
    120     window.currentTransaction = event.target.result;
    121     currentTransaction.oncomplete = onSetVersionComplete;
    122     currentTransaction.onabort = unexpectedAbort;
    123 
    124     // Delete existing object stores.
    125     while (db.objectStoreNames.length)
    126         db.deleteObjectStore(db.objectStoreNames[0]);
    127 
    128     // Now that we have a blank slate, let's create an objectStore. An objectStore is simply an
    129     // ordered mapping of keys to values. We can iterate through ranges of keys or do individual
    130     // lookups. ObjectStores don't have any schema.
    131     //
    132     // Keys can be integers, strings, or null. (The spec also defines dates and there's talk of
    133     // handling arrays, but these are not implemented yet in Chromium/WebKit.) Values can be
    134     // anything supported by the structured clone algorithm
    135     // (http://dev.w3.org/html5/spec/Overview.html#internal-structured-cloning-algorithm) which
    136     // is a superset of what can be expressed in JSON. (Note that Chromium/WebKit does not fully
    137     // implement the structured clone algorithm yet, but it definitely handles anything JSON
    138     // serializable.)
    139     //
    140     // There are two types of objectStores: ones where the path is supplied manually every time a
    141     // value is inserted and those with a "key path". A keyPath is essentially a JavaScript
    142     // expression that is evaluated on every value to extract a key. For example, if you pass in
    143     // the value of "{fname: 'john', lname: 'doe', address: {street: 'Buckingham Palace", number:
    144     // 76}, siblings: ["Nancy", "Marcus"], id: 22}" and an objectStore has a keyPath of "id" then
    145     // 22 will be the key for this value. In objectStores, each key must be unique.
    146     //
    147     // Note that the exact syntax allowed for keyPaths is not yet well specified, but
    148     // Chromium/WebKit currently allows paths that are multiple levels deep within an object and
    149     // allows that to be intermixed with array dereferences. So, for example, a key path of
    150     // "address.number" or "siblings[0]" would be legal (provided every entry had an address with
    151     // a number attribute and at least one sibling). You can even go wild and say
    152     // "foo[0][2].bar[0].baz.test[1][2][3]". It's possible this will change in the future though.
    153     //
    154     // If you set autoIncrement (another optional parameter), IndexedDB will generate a key
    155     // for your entry automatically. And if you have a keyPath set, it'll set the value at
    156     // the location of the keyPath _in the database_ (i.e. it will not modify the value you pass
    157     // in to put/add). Unfortunately autoIncrement is not yet implemented in Chromium/WebKit.
    158     //
    159     // Another optional parameter, "evictable" is not yet implemented. When it is, it'll hint
    160     // which data should be deleted first if the browser decides this origin is using too much
    161     // storage. (The alternative is that it'll suggest the user delete everything from the
    162     // origin, so it's in your favor to set it approperately!) This is great for when you have
    163     // some absolutely critical data (like unset emails) and a bunch of less critical, (but
    164     // maybe still important!) data.
    165     //
    166     // All of these options can be passed into createObjectStore via its (optional) second
    167     // parameter. So, if you wanted to define all, You'd do {keyPath: "something",
    168     // evictable: true, autoIncrement: true}. You can also pass in subsets of all three or
    169     // omit the object (since it's optional).
    170     //
    171     // Let's now create an objectStore for people. We'll supply a key path in this case.
    172     var objectStore = db.createObjectStore("people", {keyPath: "id"});
    173 
    174     // Notice that it returned synchronously. The rule of thumb is that any call that touches (in
    175     // any way) keys or values is asynchronous and any other call (besides setVersion and open) are
    176     // asynchronous.
    177     //
    178     // Now let's create some indexes. Indexes allow you to create other keys via key paths which
    179     // will also point to a particular value in an objectStore. In this example, we'll create
    180     // indexes for a persons first and last name. Indexes can optionally be specified to not be
    181     // unique, which is good in the case of names. The first parameter is the name of the index.
    182     // Second is the key path. The third specifies uniqueness.
    183     var fname = objectStore.createIndex("fname", "fname", false);
    184     var lname = objectStore.createIndex("lname", "lname", false);
    185 
    186     // Note that if you wanted to delete these indexes, you can either call objectStore.deleteIndex
    187     // or simply delete the objectStores that own the indexes.
    188     //
    189     // If we wanted to, we could populate the objectStore with some data here or do anything else
    190     // allowed in a normal (i.e. non-setVersion) transaction. This is useful so that data migrations
    191     // can be atomic with changes to the objectStores/indexes.
    192     //    
    193     // Because we haven't actually made any new asynchronous requests, this transaction will
    194     // start committing as soon as we leave this function. This will cause oncomplete event handler
    195     // for the transaction will fire shortly after. IndexedDB transactions commit whenever control is
    196     // returned from JavaScript with no further work being queued up against the transaction. This
    197     // means one cannot call setTimeout, do an XHR, or anything like that and expect my transaction
    198     // to still be around when that completes.
    199     
    200 }
    201 
    202 function unexpectedAbort()
    203 {
    204     logError("A transaction aborted unexpectedly!");
    205 }
    206 
    207 function onSetVersionComplete()
    208 {
    209     // Lets create a new transaction and then not schedule any work on it to watch it abort itself.
    210     // Transactions (besides those created with setVersion) are created synchronously. Like
    211     // createObjectStore, transaction optionally takes in various optional parameters.
    212     //
    213     // First of all is the parameter "objectStoreNames". If you pass in a string, we lock just that
    214     // objectStore.  If you pass in an array, we lock those. Otherwise (for example, if you omit it
    215     // or pass in null/undefined) we lock the whole database. By specifying locks over fewer
    216     // objectStores you make it possible for browsers to run transactions concurrently. That said,
    217     // Chromium/WebKit does not support this yet.
    218     //
    219     // Next is "mode" which specifies the locking mode. The default is READ_ONLY (i.e. a shared lock).
    220     // That's fine for this case, but later we'll ask for IDBTransaction.READ_WRITE. At the moment,
    221     // Chromium/WebKit pretends every transaction is READ_WRITE, which is kind of bad.
    222     window.currentTransaction = db.transaction([], IDBTransaction.READ_WRITE);
    223     currentTransaction.oncomplete = unexpectedComplete;
    224     currentTransaction.onabort = onTransactionAborted;
    225 
    226     // Verify that "people" is the only object store in existance. The objectStoreNames attribute is
    227     // a DOMStringList which is somewhat like an array.
    228     var objectStoreList = db.objectStoreNames;
    229     if (objectStoreList.length != 1
    230         || !objectStoreList.contains("people")
    231         || objectStoreList.item(0) != "people"
    232         || objectStoreList[0] != "people") {
    233         logError("Something went wrong.");
    234     }
    235 
    236     // Let's grab a handle to the objectStore. This handle is tied to the transaction that creates
    237     // it and thus becomes invalid once this transaction completes.
    238     var objectStore = currentTransaction.objectStore("people");
    239     if (!objectStore)
    240         logError("Something went wrong.");
    241 
    242     // If we try to grab an objectStore that doesn't exist, IndexedDB throws an exception.
    243     try {
    244         currentTransaction.objectStore("x");
    245         logError("Something went wrong.");
    246     } catch (e) {
    247         // Note that the error messages in exceptions are mostly lies at the moment. The reason is
    248         // that the spec re-uses exception codes for existing exceptions and there's no way we can
    249         // disambiguate between the two. The best work-around at the moment is to look at
    250         // http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#the-idbdatabaseexception-interface
    251         // to figure out what the number corresponds to. We will try to resolve this soon in spec-land.
    252     }
    253 
    254     // Verify that fname and lname are the only indexes in existance.
    255     if (objectStore.indexNames.length != 2)
    256         logError("Something went wrong.");
    257 
    258     // Note that no async actions were ever queued up agianst our transaction, so it'll abort once
    259     // we leave this context.
    260 }
    261 
    262 function unexpectedComplete()
    263 {
    264     logError("A transaction committed unexpectedly!");
    265 }
    266 
    267 function onTransactionAborted()
    268 {
    269     // Now let's make a real transaction and a person to our objectStore. Just to show it's possible,
    270     // we'll omit the objectStoreNames parameter which means we'll lock everything even though we only
    271     // ever access "people".
    272     window.currentTransaction = db.transaction([], IDBTransaction.READ_WRITE);
    273     currentTransaction.onabort = unexpectedAbort;
    274 
    275     var people = currentTransaction.objectStore("people");
    276     var request = people.put({fname: 'John', lname: 'Doe', id: 1}); // If our objectStore didn't have a key path, the second parameter would have been the key.
    277     request.onsuccess = onPutSuccess;
    278     request.onerror = unexpectedError;
    279 
    280     // While we're at it, why not add a few more? Multiple queued up async commands will be executed
    281     // sequentially (though there is talk of prioritizing cursor.continue--see discussion below). Since
    282     // we don't care about the individual commands' successes, we'll only bother with on error handlers.
    283     //
    284     // Remember that our implementation of unexpectedError should abort the "currentTransaction" in the
    285     // case of an error. (Though no error should occur in this case.)
    286     people.put({fname: 'Jane', lname: 'Doe', id: 2}).onerror = unexpectedError;
    287     people.put({fname: 'Philip', lname: 'Fry', id: 3}).onerror = unexpectedError;
    288 
    289     // Not shown here are the .delete method and .add (which is
    290     // like .put except that it fires an onerror if the element already exists).
    291 }
    292 
    293 function onPutSuccess()
    294 {
    295     // Result is the key used for the put.
    296     if (event.target.result !== 1)
    297         logError("Something went wrong.");
    298 
    299     // We should be able to request the transaction via event.transaction from within any event handler
    300     // (like this one) but this is not yet implemented in Chromium/WebKit. As a work-around, we use the
    301     // global "currentTransaction" variable we set above.
    302     currentTransaction.oncomplete = onPutTransactionComplete;
    303 }
    304 
    305 function onPutTransactionComplete()
    306 {
    307     // OK, now let's query the people objectStore in a couple different ways. First up, let's try get.
    308     // It simply takes in a key and returns a request whose result will be the value. Note that here
    309     // we're passing in an array for objectStoreNames rather than a simple string.
    310     window.currentTransaction = db.transaction(["people"], IDBTransaction.READ_WRITE, 0);
    311     currentTransaction.onabort = unexpectedAbort;
    312 
    313     var people = currentTransaction.objectStore("people");
    314     var request = people.get(1);
    315     request.onsuccess = onGetSuccess;
    316     request.onerror = unexpectedError;
    317 
    318     // Note that multiple objectStore (or index) method calls will return different objects (that still
    319     // refer to the same objectStore/index on disk).
    320     people.someProperty = true;
    321     if (currentTransaction.objectStore("people").someProperty)
    322         logError("Something went wrong.");
    323 }
    324 
    325 function onGetSuccess()
    326 {
    327     if (event.target.result.fname !== "John")
    328         logError("Something went wrong.");
    329 
    330     // Requests (which are our event target) also have a source attribute that's the object that
    331     // returned the request. In this case, it's our "people" objectStore object.
    332     var people = event.target.source;
    333 
    334     // Now let's try opening a cursor from id 1 (exclusive/open) to id 3 (inclusive/closed). This means
    335     // we'll get the objects for ids 2 and 3. You can also create cursors that are only right or only
    336     // left bounded or ommit the bound in order to grab all objects. You can also specify a direction
    337     // which can be IDBCursor.NEXT (default) for the cursor to move forward, NEXT_NO_DUPLICATE to only
    338     // return unique entires (only applies to indexes with unique set to false), PREV to move backwards,
    339     // and PREV_NO_DUPLICATE.
    340     var keyRange = IDBKeyRange.bound(1, 3, true, false);
    341     var request = people.openCursor(keyRange, IDBCursor.NEXT);
    342     request.onsuccess = onObjectStoreCursor;
    343     request.onerror = unexpectedError;
    344 }
    345 
    346 function onObjectStoreCursor()
    347 {
    348     // The result of openCursor is an IDBCursor object or null if there are no (more--see below)
    349     // records left.
    350     var cursor = event.target.result;
    351     if (cursor === null) {
    352         cursorComplete(event.target.source); // The soruce is still an objectStore.
    353         return;
    354     }
    355 
    356     // We could use these values if we wanted to.
    357     var key = cursor.key;
    358     var value = cursor.value;
    359 
    360     // cursor.count is probably going to be removed.
    361     // cursor.update and .remove are not yet implemented in Chromium/WebKit.
    362 
    363     // cursor.continue will reuse the same request object as what openCursor returned. In the future,
    364     // we MAY prioritize .continue() calls ahead of all other async operations queued up. This will
    365     // introduce a level of non-determinism but should speed things up. Mozilla has already implemented
    366     // this non-standard behavior, from what I've head.
    367     event.target.result.continue();
    368 }
    369 
    370 function cursorComplete(objectStore)
    371 {
    372     // While still in the same transaction, let's now do a lookup on the lname index.
    373     var lname = objectStore.index("lname");
    374 
    375     // Note that the spec has not been updated yet, but instead of get and getObject, we now
    376     // have getKey and get. The former returns the objectStore's key that corresponds to the key
    377     // in the index. get returns the objectStore's value that corresponds to the key in the
    378     // index.
    379     var request = lname.getKey("Doe");
    380     request.onsuccess = onIndexGetSuccess;
    381     request.onerror = unexpectedError;
    382 }
    383 
    384 function onIndexGetSuccess()
    385 {
    386     // Because we did "getKey" the result is the objectStore's key.
    387     if (event.target.result !== 1)
    388         logError("Something went wrong.");
    389 
    390     // Similarly, indexes have openCursor and openKeyCursor. We'll try a few of them with various
    391     // different IDBKeyRanges just to demonstrate how to use them, but we won't bother to handle
    392     // the onsuccess conditions.
    393     var lname = event.target.source;
    394     lname.openCursor(IDBKeyRange.lowerBound("Doe", false), IDBCursor.NEXT_NO_DUPLICATE);
    395     lname.openCursor(null, IDBCursor.PREV_NO_DUPLICATE);
    396     lname.openCursor(IDBKeyRange.upperBound("ZZZZ"));
    397     lname.openCursor(IDBKeyRange.only("Doe"), IDBCursor.PREV);
    398     lname.openCursor();
    399     lname.openKeyCursor();
    400 
    401     // We should be able to request the transaction via event.transaction from within any event handler
    402     // (like this one) but this is not yet implemented in Chromium/WebKit. As a work-around, we use the
    403     // global "currentTransaction" variable we set above.
    404     currentTransaction.oncomplete = onAllDone;
    405 }
    406 
    407 function onAllDone()
    408 {
    409     log("Everything worked!");
    410     if (window.layoutTestController)
    411         layoutTestController.notifyDone();
    412 }
    413 
    414 // The way setVersion is supposed to work:
    415 //   To keep things simple to begin with, objectStores and indexes can only be created in a setVersion
    416 // transaction and one can only run if no other connections are open to the database. This is designed
    417 // to save app developers from having an older verison of a web page that expects a certain set of
    418 // objectStores and indexes from breaking in odd ways when things get changed out from underneith it.
    419 // In the future, we'll probably add a more advanced mechanism, but this is it for now.
    420 //   Because a setVersion transaction could stall out nearly forever until the user closes windows,
    421 // we've added a "blocked" event to the request object returned by setVersion. This will fire if the
    422 // setVersion transaction can't begin because other windows have an open connection. The app can then
    423 // either pop something up telling the user to close windows or it can tell the other windows to call
    424 // .close() on their database handle. .close() halts any new transactions from starting and waits for
    425 // the existing ones to finish. It then closes the connection and any indexedDB calls afterwards are
    426 // invalid (they'll probably throw, but this isn't specified yet). We may specify .close() to return
    427 // an IDBRequest object so that we can fire the onsuccess when the close completes.
    428 //   Once inside a setVersion transaction, you can do anything you'd like. The one connection which
    429 // was allowed to stay open to complete the setVersion transaction will stay alive. Multiple
    430 // setVersion transactions can be queued up at once and will fire in the order queued (though
    431 // this obviously only works if they're queued in the same page).
    432 //
    433 // The current status of setVersion in Chromium/WebKit:
    434 //   In Chromium/WebKit we currently don't enforce the "all connections must be closed before a
    435 // setVersion transaction starts" rule. We also don't implement database.close() or have a blocked
    436 // event on the request .setVersion() returns.
    437 //
    438 // The current status of workers:
    439 //   Chromium/WebKit do not yet support workers using IndexedDB. Support for the async interface
    440 // will likely come before the sync interface. For now, a work-around is using postMessage to tell
    441 // the page what to do on the worker's behalf in an ad-hoc manner. Anything that can be serialized
    442 // to disk can be serialized for postMessage.
    443 
    444 </script>
    445 <body onload="start()">
    446 Please view source for more information on what this is doing and why...<br><br>
    447 <div id="logger"></div>
    448 </body>
    449 </html>
    450