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