1. Introduction
User agents need to store large numbers of objects locally in order to satisfy off-line data requirements of Web applications. [WEBSTORAGE] is useful for storing pairs of keys and their corresponding values. However, it does not provide in-order retrieval of keys, efficient searching over values, or storage of duplicate values for a key.
This specification provides a concrete API to perform advanced key-value data management that is at the heart of most sophisticated query processors. It does so by using transactional databases to store keys and their corresponding values (one or more per key), and providing a means of traversing keys in a deterministic order. This is often implemented through the use of persistent B-tree data structures that are considered efficient for insertion and deletion as well as in-order traversal of very large numbers of data records.
The following example uses the API to access a "library"
database. It has a "books"
object store that holds books records stored by their "isbn"
property as the primary key.
Book records have a "title"
property. This example artificially requires that book titles are unique. The code enforces this by creating an index named "by_title"
with the [unique](#dom-idbindexparameters-unique)
option set. This index is used to look up books by title, and will prevent adding books with non-unique titles.
Book records also have an "author"
property, which is not required to be unique. The code creates another index named "by_author"
to allow look-ups by this property.
The code first opens a connection to the database. The upgradeneeded
event handler code creates the object store and indexes, if needed. The success
event handler code saves the opened connection for use in later examples.
const request= indexedDB. open( "library" ); let db; - request
. onupgradeneeded= function () { // The database did not previously exist, so create object stores and indexes. const db= request. result; const store= db. createObjectStore( "books" , { keyPath: "isbn" }); const titleIndex= store. createIndex( "by_title" , "title" , { unique: true }); const authorIndex= store. createIndex( "by_author" , "author" ); // Populate with initial data. - store
. put({ title: "Quarry Memories" , author: "Fred" , isbn: 123456 }); - store
. put({ title: "Water Buffaloes" , author: "Fred" , isbn: 234567 }); - store
. put({ title: "Bedrock Nights" , author: "Barney" , isbn: 345678 }); }; - request
. onsuccess= function () { - db
= request. result; };
The following example populates the database using a transaction.
const tx= db. transaction( "books" , "readwrite" ); const store= tx. objectStore( "books" ); - store
. put({ title: "Quarry Memories" , author: "Fred" , isbn: 123456 }); - store
. put({ title: "Water Buffaloes" , author: "Fred" , isbn: 234567 }); - store
. put({ title: "Bedrock Nights" , author: "Barney" , isbn: 345678 }); - tx
. oncomplete= function () { // All requests have succeeded and the transaction has committed. };
The following example looks up a single book in the database by title using an index.
const tx= db. transaction( "books" , "readonly" ); const store= tx. objectStore( "books" ); const index= store. index( "by_title" ); const request= index. get( "Bedrock Nights" ); - request
. onsuccess= function () { const matching= request. result; if ( matching!== undefined ) { // A match was found. - report
( matching. isbn, matching. title, matching. author); } else { // No match was found. - report
( null ); } };
The following example looks up all books in the database by author using an index and a cursor.
const tx= db. transaction( "books" , "readonly" ); const store= tx. objectStore( "books" ); const index= store. index( "by_author" ); const request= index. openCursor( IDBKeyRange. only( "Fred" )); - request
. onsuccess= function () { const cursor= request. result; if ( cursor) { // Called for each matching record. - report
( cursor. value. isbn, cursor. value. title, cursor. value. author); - cursor
. continue (); } else { // No more matching records. - report
( null ); } };
The following example shows one way to handle errors when a request fails.
const tx= db. transaction( "books" , "readwrite" ); const store= tx. objectStore( "books" ); const request= store. put({ title: "Water Buffaloes" , author: "Slate" , isbn: 987654 }); - request
. onerror= function ( event) { // The uniqueness constraint of the "by_title" index failed. - report
( request. error); // Could call event.preventDefault() to prevent the transaction from aborting. }; - tx
. onabort= function () { // Otherwise the transaction will automatically abort due the failed request. - report
( tx. error); };
The database connection can be closed when it is no longer needed.
- db
. close();
In the future, the database might have grown to contain other object stores and indexes. The following example shows one way to handle migrating from an older version.
const request= indexedDB. open( "library" , 3 ); // Request version 3. let db; - request
. onupgradeneeded= function ( event) { const db= request. result; if ( event. oldVersion< 1 ) { // Version 1 is the first version of the database. const store= db. createObjectStore( "books" , { keyPath: "isbn" }); const titleIndex= store. createIndex( "by_title" , "title" , { unique: true }); const authorIndex= store. createIndex( "by_author" , "author" ); } if ( event. oldVersion< 2 ) { // Version 2 introduces a new index of books by year. const bookStore= request. transaction. objectStore( "books" ); const yearIndex= bookStore. createIndex( "by_year" , "year" ); } if ( event. oldVersion< 3 ) { // Version 3 introduces a new object store for magazines with two indexes. const magazines= db. createObjectStore( "magazines" ); const publisherIndex= magazines. createIndex( "by_publisher" , "publisher" ); const frequencyIndex= magazines. createIndex( "by_frequency" , "frequency" ); } }; - request
. onsuccess= function () { - db
= request. result; // db.version will be 3. };
A single database can be used by multiple clients (pages and workers) simultaneously — transactions ensure they don’t clash while reading and writing. If a new client wants to upgrade the database (via the upgradeneeded
event), it cannot do so until all other clients close their connection to the current version of the database.
To avoid blocking a new client from upgrading, clients can listen for the versionchange
event. This fires when another client is wanting to upgrade the database. To allow this to continue, react to the versionchange
event by doing something that ultimately closes this client’s connection to the database.
One way of doing this is to reload the page:
- db
. onversionchange= function () { // First, save any unsaved data: - saveUnsavedData
(). then( function () { // If the document isn’t being actively used, it could be appropriate to reload // the page without the user’s interaction. if ( ! document. hasFocus()) { - location
. reload(); // Reloading will close the database, and also reload with the new JavaScript // and database definitions. } else { // If the document has focus, it can be too disruptive to reload the page. // Maybe ask the user to do it manually: - displayMessage
( "Please reload this page for the latest version." ); } }); }; function saveUnsavedData() { // How you do this depends on your app. } function displayMessage() { // Show a non-modal message to the user. }
Another way is to call the connection‘s [close()](#dom-idbdatabase-close)
method. However, you need to make sure your app is aware of this, as subsequent attempts to access the database will fail.
- db
. onversionchange= function () { - saveUnsavedData
(). then( function () { - db
. close(); - stopUsingTheDatabase
(); }); }; function stopUsingTheDatabase() { // Put the app into a state where it no longer uses the database. }
The new client (the one attempting the upgrade) can use the blocked
event to detect if other clients are preventing the upgrade from happening. The blocked
event fires if other clients still hold a connection to the database after their versionchange
events have fired.
const request= indexedDB. open( "library" , 4 ); // Request version 4. let blockedTimeout; - request
. onblocked= function () { // Give the other clients time to save data asynchronously. - blockedTimeout
= setTimeout( function () { - displayMessage
( "Upgrade blocked - Please close other tabs displaying this site." ); }, 1000 ); }; - request
. onupgradeneeded= function ( event) { - clearTimeout
( blockedTimeout); - hideMessage
(); // ... }; function hideMessage() { // Hide a previously displayed message. }
The user will only see the above message if another client fails to disconnect from the database. Ideally the user will never see this.