The Question Mark - blog by Mark Volkmann

IndexedDB

PWA logo

Overview

IndexedDB is a JavaScript-based, object-oriented database that is supported by modern web browsers.

Unlike relational databases that are composed of tables with records that are composed of columns, IndexedDB databases are composed of stores that hold records with properties.

Objects within stores can be retrieved by their key, and by optional indexes.

Modifications occur within transactions that can be committed or rolled back.

All accesses to IndexedDB databases are performed asynchronously, so they do not impact the interactivity of a web application by blocking the main thread.

Storage Quotas

The storage quota available to IndexedDB varies based on the web browser being used and the total amount of storage on the device.

In Chrome and Chromium-based browsers (including Edge), each domain (or origin) can use up to 60% of total storage space. For example, on a phone with 128 GB of memory, a maximum of 76.8 GB can be used per domain. The available memory is typically much lower due to the operating system and applications consuming a large part of the memory.

In Safari, each domain can use up to 20% of the total storage space. If a PWA is saved to the home screen, the limit is increased to 60% of the total storage space. There is also a quota of 80% of the total storage space across all domains.

Web applications can obtain an estimate of the storage space available to their domain with the following:

const estimate = await navigator.storage.estimate();

The estimate variable holds an object with three properties. The quota property gives the maximum number of bytes available. The usage property gives the number of bytes currently being used. The usageDetails property is an object that describes how the memory is currently being used.

If an attempt is made to use more storage space than the quota allows, a QuotaExceedError will be thrown.

Previously stored data can be evicted if the available storage is close to being exhausted or the quota across all domains is exceeded.

In Safari, if the setting “Prevent Cross-Site” Tracking” is turned on, data for any origin that hasn’t had an user interactions in the last week will be evicted. This is clearly bad web apps that intended to store data for long periods of time. TODO: Does Safari keep the data permanently if the user adds the app to their home screen?

DevTools

To see the contents of IndexedDB databases in Chrome:

  • open the DevTools
  • click the “Application” tab
  • in the left nav under the “Storage” section, expand “indexedDB”
  • click the name of a store
  • all the record keys and values will be displayed
  • records can be deleted, but not modified

To delete a record in Chrome, right-click the record and select “Delete”.

To see the contents of IndexedDB databases in Safari:

  • open the “Web Inspector”
  • click the “Storage” tab
  • in the left nav, expand “Indexed Databases”
  • expand a specific database
  • click a specific store
  • all the record keys and values will be displayed

IndexedDB Interfaces

The IndexedDB API defines many interfaces that implementations implement. The following subsections summarized the most important properties and methods of these interfaces.

Also see the Promise-based library idb from Jake Archibald.

IDBFactory

The IDBFactory interface provides access to databases.

Instances support the following operations.

open database

const version = 1;
// indexedDB is a global property on the window object.
const request = indexedDB.open('db-name', version);

request.onsuccess = () => {
  const db = request.result;
  // Use the database.
};

request.onerror = event => {
  console.error('failed to open database:', event);
};

// This is called the first time a database is used
// and again each time the version number changes.
request.onupgradeneeded = event => {
  // The event may be an instance of the IDBVersionChangeEvent interface
  // and have the properties "oldVersion" and "newVersion".
  const db = request.result;
};

delete database

A database can be deleted from the Chrome DevTools in two ways.

  1. From the Application tab, select the database in the left nav under Storage … IndexedDB and click the “Delete Database” button.
  2. From the Console tab, enter indexedDB.deleteDatabase('db-name').

A database can be deleted from the Safari Web Inspector by clicking the “Console” tab and entering indexedDB.deleteDatabase('db-name').

In code, a database can be deleted as follows:

const request = indexedDB.deleteDatabase('db-name');

request.onsuccess = event => {
  console.log('deleted database');
  // event.result should be undefined
};

request.onerror = event => {
  console.error('failed to delete database:', event);
};

IDBDatabase

The IDBDatabase interface provides a connection to a database.

Instances support the following operations.

create store

// Options are optional.  autoIncrement defaults to false.
const options = {autoIncrement: true, keyPath: 'property-name'};
const store = db.createObjectStore('store-name', options);

delete store

db.deleteObjectStore('store-name');

create transaction

const mode = 'readwrite'; // or 'readonly' (default)
const stores = ['db-name']; // can be one string or an array of them
const txn = db.transaction(stores, mode);

close connection

db.close();

IDBTransaction

The IDBTransaction interface provides an asynchronous transaction over a set of stores in a common database.

Instances have the following properties.

PropertyDescription
dbassociated database
objectStoreNamesassociated store names

Instances support the following operations.

get existing store

const store = txn.objectStore('store-name');

delete existing store

const store = txn.objectStore('store-name');

commit transaction

This is not normally needed because transactions automatically commit when all requests are satisfied.

txn.commit();

abort transaction (rolls back)

txn.abort();

listen for events

txn.onabort = () => {
  console.log('transaction aborted');
};

txn.oncomplete = () => {
  console.log('transaction completed');
};

txn.onerror = event => {
  console.error('transaction error:', event);
};

IDBObjectStore

The IDBObjectStore interface represents a database store which is similar to a table in a relational database.

Instances have the following properties.

PropertyDescription
indexNamesassociated index names
keyPathassociated key path
namestore name
transactionassociated transaction

Instances support the following operations.

add record to store

const request = store.add(value);

request.onsuccess = event => {
  console.log('added record');
};

request.onerror = event => {
  console.error('failed to add record:', event);
};

delete all records from store

const request = store.clear();

request.onsuccess = event => {
  console.log('cleared store');
};

request.onerror = event => {
  console.error('failed to clear store:', event);
};

get number of records in store

The count method can:

  • get the number records in the store (no argument passed)
  • determine if a record with a given key exists, returning 0 or 1 (string key passed)
  • get the number of records whose keys fall in a given range (instance of IDBKeyRange passed)
const query = ...;
const request = store.count(query); // query is optional

request.onsuccess = event => {
  const count = request.result;
  console.log('count =', count);
};

request.onerror = event => {
  console.error('failed to count records:', event);
};

create index for store

Creating an index creates a new list of records that are sorted on a given property. Selecting an index in the Chrome DevTools Application tab displays the new list.

const breedIndex = store.createIndex('breed-index', 'breed', {unique: false});

// This creates a "compound index".
const nameBreedIndex = store.createIndex(
  'name-breed-index',
  ['name', 'breed'],
  {unique: false}
);

delete records

const request = store.delete('some-key'); // or IDBKeyRange

request.onsuccess = event => {
  console.log('records were deleted');
};

request.onerror = event => {
  console.error('failed to delete records:', event);
};

delete index

store.deleteIndex('index-name');

get record with key

const request = store.get('some-key');

request.onsuccess = event => {
  const record = request.result;
  console.log('record =', record);
};

request.onerror = event => {
  console.error('failed to get record:', event);
};

get records

The getAll method can:

  • get all the records in a store (no argument passed)
  • get the record with a given key (string key passed)
  • get all the records whose keys fall in a given range (instance of IDBKeyRange passed) This is useful for pagination.
const query = ...;
const request = store.getAll(query); // query is optional
// Optionally pass a count as the second argument
// to limit the number of records returned.

request.onsuccess = event => {
  const records = request.result; // an array
  console.log('records =', records);
};

request.onerror = event => {
  console.error('failed to get record:', event);
};

get keys

The getAllKeys method gets an array of all the keys of existing records that fall in a specified IDBKeyRange.

open index over all records

const index = store.index('index-name');
index.openCursor().onsuccess = event => {
  const cursor = event.target.result;
  // See IDBCursor methods.
};

open cursor over records in a key range

const request = store.openCursor();
request.onsuccess = event => {
  const cursor = event.target.result;
  // See IDBCursor methods.
};

open cursor to iterate over keys in a key range

The openKeyCursor method is similar to the openCursor method, but iterates over keys instead of records.

upsert a record

If you have a cursor that refers to an existing record, it is preferable to use the cursor method update instead of this.

const request = store.put(newRecord);

request.onsuccess = event => {
  const key = request.result;
  console.log('upserted record with key', key);
};

request.onerror = event => {
  console.error('failed to upsert record:', event);
};

IDBRequest

The IDBRequest interface represents the result of an asynchronous operation. It is common to set the onsuccess and onerror properties to a callback function that is passed an event object.

Instances have the following properties.

PropertyDescription
errorassociated error, if any
resultassociated result
sourceassociated store or index
transactionassociated transaction

Instances do not support any operations.

IDBCursor

The IDBCursor interface is used to iterate over records in a store.

Instances support the following operations.

iterate over all records

if (cursor) {
  const record = cursor.value;
  // Use record properties.
  cursor.continue();
} else {
  console.log('processed all records');
}

get store or index associated with cursor

const source = cursor.source;

get key of current record

const key = cursor.key;

get request that created cursor

const request = cursor.request;

advance to next record

cursor.continue();

advance to record with given key

cursor.continue(key);

delete referenced record

const request = cursor.delete();

request.onsuccess = event => {
  console.log('deleted record');
};

request.onerror = event => {
  console.error('failed to delete record:', event);
};

update referenced record

const request = cursor.update(newRecord);

request.onsuccess = event => {
  console.log('updated record');
};

request.onerror = event => {
  console.error('failed to update record:', event);
};

IDBIndex

The IDBIndex interface provides efficient retrieval of records in a store based on one or more properties of the records.

Instances have the following properties.

PropertyDescription
keyPathassociated key path
nameindex name
storeassociated store
uniqueboolean indicating whether only unique values are allowed

Instances do not support any operations.

IDBKeyRange

The IDBKeyRange interface describes a range of keys. It is used to retrieve matching keys or records from a store.

To create a range, call one of the following static methods:

  • lowerBound: lower bound can be inclusive or exclusive; no upper bound
  • upperBound: upper bound can be inclusive or exclusive; no lower bound
  • bound: lower and upper bound can both be inclusive or exclusive
  • only: single key

Instances have the following properties.

PropertyDescription
lowerlower bound
lowerOpenboolean indicating whether the lower bound is exclusive
upperupper bound
upperOpenboolean indicating whether the upper bound is exclusive

test if a key is in range

const included = range.includes(key);

CRUD Example

TODO: Add this detail. TODO: See Documents/dev/pwas/pwa-cloudflare-demo/public/setup.js