• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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@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@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.
27if (window.layoutTestController) {
28    layoutTestController.dumpAsText();
29    layoutTestController.waitUntilDone();
30}
31
32
33function 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
54function log(txt)
55{
56    document.getElementById("logger").innerHTML += txt + "<br>";
57}
58
59function logError(txt)
60{
61    log("<font color=red>" + txt + "</font>");
62}
63
64function 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
78function 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
92function 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
114function 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
202function unexpectedAbort()
203{
204    logError("A transaction aborted unexpectedly!");
205}
206
207function 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
262function unexpectedComplete()
263{
264    logError("A transaction committed unexpectedly!");
265}
266
267function 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
293function 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
305function 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
325function 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
346function 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
370function 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
384function 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
407function 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()">
446Please view source for more information on what this is doing and why...<br><br>
447<div id="logger"></div>
448</body>
449</html>
450