IndexedDB
Storing Browser Data: IndexedDB
What is IndexedDB?
View Answer:
Interview Response: IndexedDB is a database built into a browser that is much more powerful than localStorage. It has several powerful features that enhance client-side storage. IndexedDB can store practically every sort of value by key, and it supports numerous key types. It supports transactions for reliability and key range queries, and indexes. It can also store much larger volumes of data than localStorage, and it can be used in an asynchronous fashion (async/await) using promises. That power is usually excessive for traditional client-server apps. IndexedDB is mainly intended for offline apps combined with ServiceWorkers and other technologies.
Could you perhaps clarify quickly where the data in the IndexedDB is stored?
View Answer:
Interview Response: Technically, the data often saves in the visitor's home directory, with browser preferences, addons, and others. Different browsers and OS-level users have their storage.
How do you initially open an IndexedDB database?
View Answer:
Interview Response: To start working with IndexedDB, we first need to open (connect to) a database. The first step in opening an IndexedDB database is using window.indexedDB in conjunction with the open method. The open method has two parameters: the database name (required, string type), and version 1 by default (optional, positive integer). The call returns the declared object; we should listen to events on the opening request. The events include success, error, and upgradeneeded. Success means that the database is ready with an accessible database object, and the apparent error event means that the database has failed to open. The upgradeneeded handler triggers when the database does not yet exist (technically, its version is 0), so we can perform the initialization.
Code Example:
Syntax: indexedDB.open(name, version);
Syntax: indexedDB.open(name, version);
let openRequest = indexedDB.open('store', 1);
openRequest.onupgradeneeded = function () {
// triggers if the client had no database
// ...perform initialization...
};
openRequest.onerror = function () {
console.error('Error', openRequest.error);
};
openRequest.onsuccess = function () {
let db = openRequest.result;
// continue working with database using db object
};
What are the cross-domain rules that govern IndexedDB?
View Answer:
Interview Response: We can have many databases with different names, but all exist within the current origin (domain/protocol/port). Different websites cannot access each other’s databases. Some novice programmers may attempt to access the database within an ‹iframe›, but this approach does not meet the recommendation, because it is insecure.
How do we delete an IndexedDB database using JavaScript?
View Answer:
Interview Response: There are two ways to delete an IndexedDB database. The manual approach is to delete the database in the application manifest pane. The programmatic approach using JavaScript requires us to use the deleteDatabase method. The deleteDatabase() method of the IDBFactory interface requests the deletion of a database. The method returns an IDBOpenDBRequest object immediately and performs the deletion operation asynchronously. If the database successfully deletes, a success event fires on the request object returned from this method, resulting in undefined. If an error occurs while the database deletes, an error event fires on the request object returned from this method.
Code Example:
let openRequest = indexedDB.open('store', 1);
openRequest.onupgradeneeded = function () {
// triggers if the client had no database
// ...perform initialization...
};
openRequest.onerror = function () {
console.error('Error', openRequest.error);
};
openRequest.onsuccess = function () {
let db = openRequest.result;
// continue working with database using db object
};
What is the reason a user cannot open an IndexedDB database based on versioning?
View Answer:
Interview Response: If the current user database has a higher version than the one specified in the open call, for example, if the present DB version is 3 and we try to open(...2), an error generates, and openRequest.onerror is called. That's unusual, but it can happen if a visitor loads outdated JavaScript code through a proxy cache. So, while the code is ancient, his database is brand new.
Is there a way to handle potential versioning issues with IndexedDB?
View Answer:
Interview Response: Yes, we should do a version check programmatically to ensure that the user has the most updated version. We have to implement a parallel upgrade to ensure the correct version loads in the client. We achieve this by calling onversionchange to ensure that the client is updated correctly. These update collisions happen rarely, but we should at least have some handling for them, at least onblocked handler, to prevent our script from dying silently.
Code Example:
let openRequest = indexedDB.open("store", 2);
openRequest.onupgradeneeded = ...;
openRequest.onerror = ...;
openRequest.onsuccess = function() {
let db = openRequest.result;
db.onversionchange = function() {
db.close();
alert("Database is outdated, please reload the page.")
};
// ...the db is ready, use it...
};
openRequest.onblocked = function() {
// this event shouldn't trigger if we handle onversionchange correctly
// it means that there's another open connection to same database
// and it wasn't closed after db.onversionchange triggered for it
};
What do we need to use to store data in an IndexedDB database?
View Answer:
Interview Response: To store something in IndexedDB, we need an object store. An object store is a core concept in IndexedDB. Counterparts in other databases are called “tables” or “collections”, where the data is stored. A database may have multiple stores: one for users, another for goods, and more. Despite being named an “object-store” IndexedDB, primitives get stored too.
What types of values can an IndexedDB database store?
View Answer:
Interview Response: We can store almost any value (except objects with a circular reference), including complex objects. IndexedDB uses the standard serialization algorithm to clone-and-store an object. It is like JSON.stringify, but it is more powerful and can store much more data.
Could you kindly give an example of a non-IndexedDB object?
View Answer:
Interview Response: An example of an object that cannot be stored: an object with circular references. Such objects are not serializable, and JSON.stringify also fails for such objects.
Is there a specific type of key that we must use in IndexedDB?
View Answer:
Interview Response: Yes, there must be a unique key for every value in the store. A key must be a number, date, string, binary, or array. It is a unique identifier, so we can search/remove/update values by the key index.
How are keys implemented in an IndexedDB when we store objects?
View Answer:
Interview Response: When we add a value to the store, such as localStorage, we may provide a key. However, when storing objects, IndexedDB enables us to use an object attribute as the key, which is much more handier, or we may auto-generate keys. However, we must first establish an object store with the createObjectStore function.
Syntax: db.createObjectStore(name, options);
Explain the function and syntax of the createObjectStore JavaScript method?
View Answer:
Interview Response: The createObjectStore() method of the IDBDatabase interface creates and returns a new object store or index. The method takes the store's name and a parameter object that lets you define optional properties. You can use the property to identify individual objects in the store uniquely. As the property is an identifier, it should be unique to every object, and every object should have that property. The options have two optional parameters including the key-path and auto-increment. The key path is a path to an object property that IndexedDB uses as the key. If set to true, the auto-increment option parameter automatically generates a new auto-incrementing number for the key, like an id or number. If we do not supply keyOptions, we need to provide a key when storing an object explicitly.
Code Example:
Syntax: db.createObjectStore(name, options);
Syntax: db.createObjectStore(name, options);
// Create an objectStore for this database
let objectStore = db.createObjectStore('toDoList', { keyPath: 'taskTitle' });
When can an object store be created or modified in IndexedDB?
View Answer:
Interview Response: We can only create/modify an object store while updating the upgraded handler's DB version in the upgradeneeded handler. That is a technical limitation. Outside of the handler, we need to be able to add/remove/update the data, but we can only create/remove/alter object stores during a version update.
What are the two basic methods for upgrading an IndexedDB version?
View Answer:
Interview Response: There are two main approaches to perform a database version upgrade.
We can implement per-version upgrade functions: from 1 to 2, from 2 to 3, from 3 to 4, and onwards. Then, in upgradeneeded we can compare versions (e.g., old 2, now 4) and run per-version upgrades step by step, for every intermediate version (2 to 3, then 3 to 4).
Or we can examine the database: retrieve a list of existing object stores as db.objectStoreNames. The object is a DOMStringList that provides contains(name) method to check for the existence of the objects, and then we execute updates depending on what exists and what does not.
For small databases, the second variant may be simpler.
We can implement per-version upgrade functions: from 1 to 2, from 2 to 3, from 3 to 4, and onwards. Then, in upgradeneeded we can compare versions (e.g., old 2, now 4) and run per-version upgrades step by step, for every intermediate version (2 to 3, then 3 to 4).
Or we can examine the database: retrieve a list of existing object stores as db.objectStoreNames. The object is a DOMStringList that provides contains(name) method to check for the existence of the objects, and then we execute updates depending on what exists and what does not.
For small databases, the second variant may be simpler.
Code Example: Second Approach
let openRequest = indexedDB.open('db', 2);
// create/upgrade the database without version checks
openRequest.onupgradeneeded = function () {
let db = openRequest.result;
if (!db.objectStoreNames.contains('books')) {
// if there's no "books" store
db.createObjectStore('books', { keyPath: 'id' }); // create it
}
};
Can you define what a transaction is concerning a database?
View Answer:
Interview Response: A transaction is a group operation; they should either succeed or fail. For example, when a person buys something, we need to do a group of operations related to their activities, such as removing money from their account or adding an item to their shopping list.
Can you explain the function and syntax of the transaction method?
View Answer:
Interview Response: The transaction method of the IDBDatabase interface immediately returns a transaction object (IDBTransaction) containing the IDBTransaction.objectStore method, which you can use to access your object-store. We must make all data operations within a transaction in IndexedDB. The transaction method has three available arguments: store, mode/type, and options. The store/storeNames refer to the names of the object stores in the scope of the new transaction, declared as an array of strings. Specify only the object stores that you need to access. If you need to access only one object store, you can specify its name as a string. The mode or type relates to the types of access performed in the transaction. IndexedDB transactions open in one of three modes: readonly, readwrite and readwriteflush (non-standard, Firefox-only.) We should specify the object-store versionchange mode here. If you do not provide the parameter, the default access mode is readonly. Please do not open a readwrite transaction unless you need to write it into the database to avoid slowing things down. The options argument is a dictionary of option durability parameters including "default", "strict", or "relaxed". The default is "default". Using "relaxed" provides better performance but with fewer guarantees. Web applications are encouraged to use "relaxed" for transient data such as caches or quickly changing records and "strict" in cases where reducing the risk of data loss outweighs the impact on performance and power. We should note that the mode/type and options are optional arguments.
Code Example:
Syntax: IDBDatabase.transaction(storeNames, mode, options);
Syntax: IDBDatabase.transaction(storeNames, mode, options);
let transaction = db.transaction('books', 'readwrite'); // (1)
// get an object store to operate on it
let books = transaction.objectStore('books'); // (2)
let book = {
id: 'js',
price: 10,
created: new Date(),
};
let request = books.add(book); // (3)
request.onsuccess = function () {
// (4)
console.log('Book added to the store', request.result);
};
request.onerror = function () {
console.log('Error', request.error);
};
Why are there different types of IndexedDB transactions?
View Answer:
Interview Response: Performance is why transactions need to be labeled either readonly or readwrite. Many readonly transactions can access the same store concurrently, but readwrite transactions cannot. A readwrite transaction “locks” the store for writing. The following transaction must wait before the previous one finishes before accessing the same store.
Code Example:
let transaction = db.transaction('books', 'readwrite'); // (1)
What are the two methods used for data storage in an Object Store?
View Answer:
Interview Response: Object stores support two methods: the put() and add() methods that store values. The put(value, [key]) adds values to the store. The object store supplies the key only if the object store does not have keyPath or autoIncrement option. If there is already a value with the same key, it gets replaced. The add(value, [key]) function is the same as the put method, except if a value with the same key already exists, the request fails, and an error with the name "ConstraintError" gets created.
Syntax: let request = books.add(book);
How do we mark an IndexedDB transaction as finished, with no more requests to come?
View Answer:
Interview Response: There is no way to mark an IndexedDB transaction as finished (this is not the same as oncomplete) in version 2.0. When all transaction requests end and the microtasks queue is empty, it commits automatically. Usually, we can assume that a transaction commits when all its requests are complete and the current code finishes.
What is one of the side-effects of the transaction auto-commit principle?
View Answer:
Interview Response: The auto-commit principle of transactions has an interesting side effect. During a transaction, we cannot introduce an async operation like fetch or setTimeout. IndexedDB does not hold the transaction until these reach completion. This process is especially noticeable when using fetch and setTimeout combined with an IndexedDB transaction. The IndexedDB spec's authors feel that transactions should be short-lived. Primarily for reasons of performance. Readwrite transactions, in particular, "lock" the stores for writing. So, if one part of the program initiates readwrite on the books object store, another portion of the application that wishes to do the same must wait: the new transaction "hangs" until the previous one reaches completion. If transactions take a long time, this might cause unusual delays.
Code Example:
let request1 = books.add(book);
request1.onsuccess = function () {
fetch('/').then((response) => {
let request2 = books.add(anotherBook); // (*)
request2.onerror = function () {
console.log(request2.error.name); // TransactionInactiveError
};
});
};
Do we need onerror/onsuccess for every request?
View Answer:
Interview Response: Not every time. We can use event delegation instead. All events are DOM events, with capturing and bubbling, but we usually, only use the bubbling stage for event delegation. We can catch all errors using db.onerror handler for reporting or other purposes. If an error get handled, we do not want to report it. We can stop the bubbling and use db.onerror by using event.stopPropagation() in request.onerror.
Code Example:
request.onerror = function (event) {
if (request.error.name == 'ConstraintError') {
console.log('Book with such id already exists'); // handle the error
event.preventDefault(); // don't abort the transaction
event.stopPropagation(); // don't bubble error up, "chew" it
} else {
// do nothing
// transaction will be aborted
// we can take care of error in transaction.onabort
}
};
What are the two main types of searches in an object store?
View Answer:
Interview Response: There are two main types of search in an object store: searching by key-value or key range or another object field. This process requires an additional data structure named “index”.
Can you describe how a key range or value search works?
View Answer:
Interview Response: To search an IndexedDB database by key range or value, we must implement the IDBKeyrange object and call on the lowerbound and upperbound methods. lowerBound() generates a new key range with only a lower bound. It is closed by default and includes the lower endpoint value. The upperBound() function generates a new upper-bound key range, and it is closed by default and includes the upper endpoint value. The following methods include store get, getAll, getKey, getAllKeys, or count to perform the actual search. They accept a query argument that can be either an exact key or a key range.
Code Example:
// get one book
books.get('js');
// get books with 'css' <= id <= 'html'
books.getAll(IDBKeyRange.bound('css', 'html'));
// get books with id < 'html'
books.getAll(IDBKeyRange.upperBound('html', true));
// get all books
books.getAll();
// get all keys, where id > 'js'
books.getAllKeys(IDBKeyRange.lowerBound('js', true));
Author Note: You might want to add an additional question about query object fields.
By default, how does Object store sort values?
View Answer:
Interview Response: Internally, an object storage arranges values by key by default. As a result, requests for return values are returned in key order.
How do you delete values in an IndexedDB Object store?
View Answer:
Interview Response: In simple terms, the delete method looks up values to delete by a query. The call format is similar to the getAll() method. If we want to delete everything, we can use the clear method to clear the entire storage.
Code Example:
// find the key where price = 5
let request = priceIndex.getKey(5);
request.onsuccess = function () {
let id = request.result;
let deleteRequest = books.delete(id);
};
books.clear(); // clear the storage.
Can you briefly explain what a cursor is and its relation to the IndexedDB database?
View Answer:
Interview Response: A cursor is a pointer that iterates across all the documents in each data store or index, exposing the data for the page that the cursor is presently "pointing" at on each iteration.
It also contains a few pieces of additional metadata and a couple of methods, like continue or primaryKey. As an object store is sorted internally by key, a cursor walks the store in key order (ascending by default). The cursor also has two optional arguments, including the range and direction. The range query is a key or a key range, same as for getAll. The direction sets the order to use and includes two parameters prev, and nextunique or prevunique. The prev parameter is the reverse order: down from the record with the biggest key. The nextunique and prevunique are similar, but the skip records with the same key (only for cursors over indexes, e.g., for multiple books with price=5 only the first one returns). The main difference of the cursor is that request.onsuccess triggers multiple times: once for each result.
It also contains a few pieces of additional metadata and a couple of methods, like continue or primaryKey. As an object store is sorted internally by key, a cursor walks the store in key order (ascending by default). The cursor also has two optional arguments, including the range and direction. The range query is a key or a key range, same as for getAll. The direction sets the order to use and includes two parameters prev, and nextunique or prevunique. The prev parameter is the reverse order: down from the record with the biggest key. The nextunique and prevunique are similar, but the skip records with the same key (only for cursors over indexes, e.g., for multiple books with price=5 only the first one returns). The main difference of the cursor is that request.onsuccess triggers multiple times: once for each result.
Code Example:
const request = window.indexedDB.open('database', 1);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(['invoices'], 'readonly');
const invoiceStore = transaction.objectStore('invoices');
const getCursorRequest = invoiceStore.openCursor();
getCursorRequest.onsuccess = (e) => {
// Cursor logic here
};
};