Google News
logo
JavaScript IndexedDB Interview Questions
* IndexedDB is a large-scale object store built into the browser.

* The IndexedDB allows you to persistently store the data using key-value pairs.

* The values can be any JavaScript type including boolean, number, string, undefined, null, date, object, array, regex, blob, and files.
* IndexedDB allows you to create web applications that can work both online and offline.

* It’s useful for applications that store a large amount of data and don’t need a persistent internet connection.

* For example, Google Docs uses the IndexedDB to store the cached documents in the browser and synchronizes with the server once in a while. This allows Google Docs to increase performance while enhancing user experiences.

* And you’ll find other types of applications that heavily use IndexedDB like online notepads, quizzes, todo lists, code sandboxes, and CMS.
The IndexedDB's fundamental concepts are briefly explained below:

1) IndexedDB databases keep key-value pairs.
* The values stored in the IndexedDB, as opposed to localStorage and sessionStorage, can be complicated structures like objects and blobs.
* Keys can also be binary items, or they can be the characteristics of these things.
* Any property of the objects can be used to generate indexes for easy searching and sorting.

2) IndexedDB supports transactions
* A transaction always takes place while reading from or writing to an IndexedDB database.
* Users who launch a web-based application in two tabs or windows at once and read from the database and write the same database are protected by the transactional model, which guarantees data integrity.

3) The IndexedDB API is primarily asynchronous.
* Asynchronous operations are used by IndexedDB. When an operation is finished, and the output is accessible, it notifies you using DOM events.

4) The NoSQL system IndexedDB
* NoSQL technology is used by IndexedDB. To query data, it does not utilize SQL, in other words.
* It makes use of the cursor-returning query instead. Then, you may iterate the result set using the pointer.

5) IndexedDB adheres to the same-origin rule
* An origin is the URL of the page that contains the protocol, domain, and port where the code is executed.
* domain: freetimelearning.com
* protocol: https
* port: 443
* IndexedDB follows the same-origin principle. It implies that every origin has a unique collection of databases. Additionally, one origin is unable to access databases from different sources.
Using IndexedDB in web applications offers several advantages :

* Powerful Client-Side Storage : IndexedDB allows storing large amounts of structured data directly in the user's browser. This enables web applications to handle substantial datasets without relying on server-side storage.

* Asynchronous API : IndexedDB provides an asynchronous API, allowing database operations to be performed without blocking the main thread of the web application. This asynchronous nature helps maintain a responsive user interface, especially when dealing with long-running tasks.

* Transaction Support : IndexedDB supports transactions, ensuring data integrity and consistency. Transactions allow developers to perform multiple database operations as a single unit of work, ensuring that changes are either committed entirely or rolled back if an error occurs.

* Indexed Access : IndexedDB supports indexes, which enable efficient retrieval of data based on specific keys or ranges of keys. Indexes improve query performance, especially when dealing with large datasets, by reducing the need for full table scans.

* Cross-Origin Access : IndexedDB supports cross-origin access control, allowing web applications served from different origins to access the same IndexedDB database, provided that appropriate security measures are in place. This enables sharing data between different parts of a web application or between different applications altogether.

* Offline Capabilities : IndexedDB enables web applications to work offline by allowing them to store data locally within the browser. This is particularly useful for applications that need to continue functioning even when an internet connection is unavailable, such as mobile apps or productivity tools.

* Improved Performance : By storing data locally and reducing the need for server round trips, IndexedDB can significantly improve the performance of web applications, especially those that rely heavily on data retrieval and manipulation.

* Reduced Server Load : By offloading data storage and processing to the client-side, IndexedDB can help reduce the load on server infrastructure, leading to lower hosting costs and better scalability for web applications.
IndexedDB differs from other client-side storage options like cookies, localStorage, and WebSQL in several key aspects :

Data Structure and Capacity :
* IndexedDB: Supports storing structured data in key-value pairs, including complex data structures like arrays and objects. It is designed for storing large amounts of data, making it suitable for complex applications.
* Cookies: Limited to storing small amounts of data (usually up to 4KB per cookie) and are primarily used for session management and client-side tracking.
* localStorage: Stores data as key-value pairs in a simple string format and is limited to around 5-10MB of storage per domain. It's suitable for simpler applications that require persistent storage of small amounts of data.
* WebSQL: Uses a relational database model similar to SQL databases and provides a SQL-based query interface for data manipulation. However, it has been deprecated in favor of IndexedDB due to standardization issues.

Asynchronous Operations :
* IndexedDB: Provides an asynchronous API, allowing database operations to be performed without blocking the main thread of the web application. This is essential for maintaining a responsive user interface, especially for long-running tasks.
* Cookies, localStorage, and WebSQL: Operate synchronously, which can potentially block the main thread and lead to a degraded user experience, especially for operations involving large datasets.

Transaction Support :
* IndexedDB: Supports transactions, ensuring data integrity and consistency by allowing developers to perform multiple database operations as a single unit of work.
* Cookies, localStorage, and WebSQL: Lack transaction support, making it challenging to maintain data integrity, especially in complex applications with multiple data manipulation operations.

Indexed Access :
* IndexedDB: Supports indexes, enabling efficient retrieval of data based on specific keys or ranges of keys. Indexes improve query performance, especially for large datasets.
* Cookies, localStorage, and WebSQL: Do not support indexes, making data retrieval less efficient, particularly when dealing with large datasets.

Cross-Origin Access :
* IndexedDB: Supports cross-origin access control, allowing web applications served from different origins to access the same IndexedDB database, subject to appropriate security measures.
* Cookies, localStorage, and WebSQL: Are subject to stricter same-origin policies, limiting their use for cross-origin data sharing.
6 .
How does IndexedDB differ from LocalStorage?
Unlike LocalStorage which stores data as strings, IndexedDB stores data as JavaScript objects. Also, IndexedDB allows for more storage space and supports transactions.
IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs. This API uses indexes to enable high-performance searches of this data.

The key components of IndexedDB include :

1. Database : This is the actual container for data storage. An IndexedDB database has a version and a set of object stores.

2. Object Stores : These are the IndexedDB equivalent of tables. Each database can have multiple object stores, and each object store can contain different types of data.

3. Keys : These are used to identify records in an object store. Every record in an object store must have a unique key. Keys can be simple types like integers, dates, and strings, or they can be arrays.

4. Indexes : Indexes are essentially object stores that store a key for every record in another object store. An index on a specific field in an object store allows efficient lookup of records based on that field, even if it's not the key.

5. Transactions : All interactions with data in IndexedDB happen in the context of a transaction. A transaction is a wrapper around an operation, or group of operations, with three possible modes: readonly, readwrite, and versionchange.

6. Versioning : IndexedDB uses a versioning model. Whenever the structure of the database changes (e.g., when object stores or indexes are created or removed), the version number of the database is increased.

7. Cursors : Cursors are used to iterate over multiple records in database. They offer a way to retrieve and update data in a specific sequence.

8. Requests : Almost every operation in IndexedDB is asynchronous and returns a request object. These requests are not the actual data but act as a placeholder for the data.

9. Queries : These are requests for data that meet certain criteria. Queries can retrieve data from both object stores and indexes.

10. Events : IndexedDB operations communicate success or failure by firing events at request objects. There are three types of events: success events, error events, and blocked events.
Object Stores in IndexedDB are like tables in a relational database, containing records of data. Each record includes a key for identification and a value, which can be of any data type.

Code Example :
let openRequest = indexedDB.open("myDatabase", 1);

openRequest.onupgradeneeded = function(e) {
  let db = e.target.result;

  // Create an object store named "books", with a key path of "isbn"
  let store = db.createObjectStore("books", {keyPath: "isbn"});

  // Create an index on the "author" property of the objects in the store
  store.createIndex("author", "author", {unique: false});
};

openRequest.onsuccess = function(e) {
  console.log("Success! Got a handle on the database.");
};

openRequest.onerror = function(e) {
  console.error("Unable to open database: ", e.target.errorCode);
};?
In IndexedDB, opening a database involves a few steps. Below is a basic example of how to open a database:
// Step 1: Open or create a database
var request = indexedDB.open("myDatabase", 1);

// Step 2: Handle the database events
request.onerror = function(event) {
    // Handle errors
    console.log("Error opening database");
};

request.onsuccess = function(event) {
    // Get reference to the opened database
    var db = event.target.result;
    console.log("Database opened successfully");
    
    // Further database operations can be performed here
};

// Step 3: Define the database structure in case of upgrade
request.onupgradeneeded = function(event) {
    // Get reference to the opened database
    var db = event.target.result;

    // Create an object store (table) to store data
    var objectStore = db.createObjectStore("myObjectStore", { keyPath: "id" });

    // Define indexes if needed
    objectStore.createIndex("name", "name", { unique: false });
    objectStore.createIndex("email", "email", { unique: true });
};?
Explanation :

* We use indexedDB.open() method to open or create a database. It takes two parameters: the name of the database and the version number. If the database doesn't exist, it will be created. If it does exist, the version number will determine whether an upgrade is needed.

* We attach event handlers to handle different events during the database opening process:
* request.onerror : This event is triggered if there is an error opening the database.
* request.onsuccess : This event is triggered when the database is successfully opened. We can get a reference to the opened database using event.target.result.
* request.onupgradeneeded : This event is triggered when the database needs to be upgraded (e.g., when the version number provided during the opening operation is greater than the existing version). Here, we can define the database structure, such as creating object stores and indexes.

* Inside the onupgradeneeded event handler, we define the structure of the database by creating object stores (similar to tables in relational databases) using db.createObjectStore(). We can also create indexes on object stores using objectStore.createIndex() if needed.

* Once the database is successfully opened (onsuccess event), further database operations can be performed within the callback function.
In IndexedDB, an object store is a container within a database where data is stored. It can be thought of as similar to a table in a relational database or a collection in a NoSQL database.

Object stores hold structured data in the form of key-value pairs, allowing developers to store and retrieve data efficiently.

Key characteristics of an object store in IndexedDB include :

* Structure
* Key-Value Pairs
* Schema-less
* Indexed Access
* Transaction Scope
* Object Store Options
In IndexedDB, indexes provide a way to efficiently query and retrieve data based on specific keys or ranges of keys within an object store. They are similar to indexes in relational databases, allowing for faster data access by pre-sorting and organizing data based on specified criteria.

Here's a breakdown of the concept of indexes in IndexedDB :

* Efficient Data Retrieval : Indexes improve the performance of data retrieval operations by creating a sorted data structure based on specific keys or properties of the stored objects. This allows for faster lookups, especially when searching for data by keys other than the primary key.

* Creating Indexes : Indexes are created on specific properties or keys of objects within an object store. When creating an index, developers specify the name of the index, the key path (property) to index, and optionally, whether the index should enforce uniqueness for the indexed values.

* Types of Indexes : IndexedDB supports two types of indexes:

* Single-Entry Indexes : These indexes map each unique key or property value to one or more corresponding records. They are useful for ensuring uniqueness or performing exact-match queries.
* Multi-Entry Indexes : These indexes allow multiple records to be associated with the same index key or property value. They are useful for querying data based on ranges, partial matches, or for properties with array values.

Usage : Once created, indexes can be used to perform efficient queries and data retrieval operations within the object store. Queries using indexes can be performed using methods such as get(), getAll(), getAllKeys(), openCursor(), and openKeyCursor().

* Transaction Scope : Like other operations in IndexedDB, index operations are performed within the scope of a transaction. This ensures that index updates are atomic and consistent with other database operations.

* Updating Indexes : Indexes are automatically updated whenever data within the associated object store is added, modified, or deleted. IndexedDB handles index updates transparently, ensuring that indexes remain consistent with the underlying data.
In IndexedDB, key paths are used to specify which property within an object should be used as the key when storing data in an object store. There are different types of key paths that can be used:

String Key Path : This is the most common type of key path. It specifies the name of a property within the stored object that should be used as the key. For example:
var objectStore = db.createObjectStore("storeName", { keyPath: "id" });?

In this example, the property named "id" within the stored objects will be used as the key.


Array Key Path : An array key path allows specifying a compound key composed of multiple properties. The values of these properties, when combined, form a composite key. For example:
var objectStore = db.createObjectStore("storeName", { keyPath: ["lastName", "firstName"] });?

In this example, the compound key is composed of the "lastName" and "firstName" properties of the stored objects.

Implicit Key Path : If no key path is specified, IndexedDB automatically generates keys for objects stored in the object store. These keys are numeric and monotonically increasing. This is useful when the data does not have a natural key or when the key is not explicitly specified. For example:
var objectStore = db.createObjectStore("storeName", { autoIncrement: true });?

In this case, the keys for stored objects will be automatically generated.

Each of these key path types has its use cases and considerations. Developers should choose the appropriate key path type based on the requirements of their application and the structure of the data being stored in IndexedDB.
To create an object store in IndexedDB, you typically perform this operation within the onupgradeneeded event handler of the indexedDB.open() method. Here's how you can create an object store:
// Step 1: Open or create a database
var request = indexedDB.open("myDatabase", 1);

// Step 2: Handle the database events
request.onerror = function(event) {
    // Handle errors
    console.log("Error opening database");
};

request.onsuccess = function(event) {
    // Get reference to the opened database
    var db = event.target.result;
    console.log("Database opened successfully");
    
    // Further database operations can be performed here
};

// Step 3: Define the database structure in case of upgrade
request.onupgradeneeded = function(event) {
    // Get reference to the opened database
    var db = event.target.result;

    // Create an object store (table) to store data
    var objectStore = db.createObjectStore("myObjectStore", { keyPath: "id" });

    // Define indexes if needed
    objectStore.createIndex("name", "name", { unique: false });
    objectStore.createIndex("email", "email", { unique: true });
};?

Explanation :

Open or create a database using indexedDB.open("myDatabase", 1). The first parameter is the name of the database, and the second parameter is the version number. If the database doesn't exist, it will be created. If it does exist and the version number is greater than the existing version, the onupgradeneeded event will be triggered.

Handle database events such as error and success. In case of an error, you can handle it within the request.onerror event handler. In case of success, you can further perform database operations within the request.onsuccess event handler.

Define the database structure within the onupgradeneeded event handler. This is where you create object stores and define their structure. In this example, we create an object store named "myObjectStore" with a key path of "id". We also define two indexes named "name" and "email" on the "name" and "email" properties of the stored objects, respectively.

Once the onupgradeneeded event handler is executed, the object store "myObjectStore" will be created with the specified key path and indexes within the database. You can then use this object store to store and retrieve data within your IndexedDB database.
To add data to an object store in IndexedDB, you typically perform this operation within a transaction. Here's a step-by-step guide on how to add data to an object store:
// Step 1: Open or create a database
var request = indexedDB.open("myDatabase", 1);

// Step 2: Handle the database events
request.onerror = function(event) {
    // Handle errors
    console.log("Error opening database");
};

request.onsuccess = function(event) {
    // Get reference to the opened database
    var db = event.target.result;
    console.log("Database opened successfully");
    
    // Step 3: Start a transaction
    var transaction = db.transaction(["myObjectStore"], "readwrite");

    // Step 4: Get the object store
    var objectStore = transaction.objectStore("myObjectStore");

    // Step 5: Add data to the object store
    var data = { id: 1, name: "John Doe", email: "john@example.com" };
    var addRequest = objectStore.add(data);

    // Step 6: Handle the result of the add operation
    addRequest.onsuccess = function(event) {
        console.log("Data added successfully");
    };

    addRequest.onerror = function(event) {
        console.log("Error adding data");
    };
};

// Step 7: Define the database structure in case of upgrade
request.onupgradeneeded = function(event) {
    // Get reference to the opened database
    var db = event.target.result;

    // Create an object store if it doesn't exist
    var objectStore = db.createObjectStore("myObjectStore", { keyPath: "id" });

    // Define indexes if needed
    objectStore.createIndex("name", "name", { unique: false });
    objectStore.createIndex("email", "email", { unique: true });
};?

Explanation :

* Open or create a database using indexedDB.open("myDatabase", 1).

* Handle database events such as error and success.

* Start a transaction with the object store you want to add data to. In this case, we use "myObjectStore".

* Get a reference to the object store within the transaction using transaction.objectStore("myObjectStore").

* Prepare the data you want to add to the object store. This can be in the form of a JavaScript object.

* Use the add() method on the object store to add the data. The add() method takes the data as a parameter.

* Handle the result of the add operation using onsuccess and onerror event handlers.

* Optionally, define the database structure within the onupgradeneeded event handler.
15 .
What is the importance of "Keys" in IndexedDB?
Keys in IndexedDB uniquely identify records in an object store, serving as an essential element for efficient data retrieval and manipulation. They can be simple types or arrays, providing flexibility in data organization.
Cursors in IndexedDB are objects used to traverse and interact with the data in object stores and indexes. They allow you to iterate over records, retrieve data from specific ranges, and can navigate in any direction.

Code Example :
let openRequest = indexedDB.open("myDatabase", 1);

openRequest.onsuccess = function(e) {
  let db = e.target.result;
  let transaction = db.transaction("books", "readonly");
  let objectStore = transaction.objectStore("books");
  let request = objectStore.openCursor();

  request.onsuccess = function(e) {
    let cursor = e.target.result;
    
    if (cursor) {
      console.log("Key: ", cursor.key);
      console.log("Value: ", cursor.value);
      
      // Continue to the next item
      cursor.continue();
    } else {
      console.log("End of data");
    }
  };

  request.onerror = function(e) {
    console.error("Error opening cursor: ", e.target.errorCode);
  };
};

openRequest.onerror = function(e) {
  console.error("Unable to open database: ", e.target.errorCode);
};?
No, IndexedDB doesn't support join operations like SQL. However, you can manually implement relationships by storing keys from one object store in another and using separate requests to fetch related data.

Code Example :

Sure, here's a simple example of how you might manually implement relationships in IndexedDB:

Suppose you have two object stores, authors and books. Each book object has an authorId field, which is the key of the author in the authors object store.
let db; // Assuming db is the opened IndexedDB database

// Get the transaction
let transaction = db.transaction(["books", "authors"]);

// Get the object stores
let bookStore = transaction.objectStore("books");
let authorStore = transaction.objectStore("authors");

// Let's find the details of the book and its author with bookId=1
let bookId = 1;

// Request to get the book
let bookRequest = bookStore.get(bookId);

bookRequest.onsuccess = function(e) {
  let book = e.target.result;
  console.log("Book: ", book.title);

  // Request to get the author using the authorId from the book
  let authorRequest = authorStore.get(book.authorId);

  authorRequest.onsuccess = function(e) {
    let author = e.target.result;
    console.log("Author: ", author.name);
  };

  authorRequest.onerror = function(e) {
    console.error("Error fetching author: ", e.target.error);
  };
};

bookRequest.onerror = function(e) {
  console.error("Error fetching book: ", e.target.error);
};?

This code gets a book and its corresponding author from the database. First, it fetches the book from the "books" object store. Then, using the authorId from the book, it fetches the author from the "authors" object store. This simulates a join operation.
Errors in IndexedDB can be handled by listening to the "error" event on the request or transaction objects.

Simple Code Example :
authorRequest.onerror = function(e) {
  console.error("Error fetching author: ", e.target.error);
};?
Versioning in IndexedDB allows for control over database structure modifications. Each database has a version number that increments after structural changes.

Code Example :
// Open the database
const dBOpenRequest = window.indexedDB.open("toDoList", 4);

dBOpenRequest.onupgradeneeded = (event) => {
  const db = event.target.result;
  console.log(`Upgrading to version ${db.version}`);

  // Create an objectStore for this database
  const objectStore = db.createObjectStore("toDoList", {
    keyPath: "taskTitle",
  });

  // define what data items the objectStore will contain
  objectStore.createIndex("hours", "hours", { unique: false });
  objectStore.createIndex("minutes", "minutes", { unique: false });
  objectStore.createIndex("day", "day", { unique: false });
  objectStore.createIndex("month", "month", { unique: false });
  objectStore.createIndex("year", "year", { unique: false });
};?

Overall, this code example demonstrates how to open an IndexedDB database, handle the upgrade event, create an object store, and define indexes on the object store.

NOTE : It's important to handle versioning properly to ensure smooth database upgrades and migrations in real-world scenarios.
IndexedDB is primarily designed to be used asynchronously. The API is asynchronous to prevent blocking the main thread of the web application, ensuring that the user interface remains responsive even during long-running database operations.

Most operations in IndexedDB, such as opening a database, creating or deleting object stores, adding, retrieving, updating, or deleting data, and executing queries, are performed asynchronously. They typically involve using callbacks, promises, or event handlers to handle the results of these operations asynchronously.

For example, when opening a database or performing database operations, you would typically handle success and error events asynchronously:
// Asynchronous usage
var request = indexedDB.open("myDatabase", 1);

request.onerror = function(event) {
    console.log("Error opening database");
};

request.onsuccess = function(event) {
    var db = event.target.result;
    console.log("Database opened successfully");
    
    // Further database operations can be performed here
};?

However, in some cases, you may encounter scenarios where you want to perform IndexedDB operations synchronously, such as within a Web Worker or in specific situations where synchronous access is required. While the standard IndexedDB API does not directly support synchronous access, you can use techniques like wrapping IndexedDB operations in synchronous APIs provided by libraries or frameworks, or using synchronous features provided by modern JavaScript runtimes (such as async/await with IndexedDB promises).

However, it's important to note that forcing synchronous behavior in IndexedDB operations can potentially lead to blocking the main thread, which can degrade the performance and responsiveness of your web application. Therefore, it's generally recommended to follow the asynchronous nature of IndexedDB and design your application logic accordingly to ensure a smooth user experience.
In IndexedDB, a transaction is a mechanism used to group database operations into atomic units of work. Transactions ensure data integrity and consistency by allowing multiple operations to be performed as a single, isolated unit. This means that either all operations within a transaction are successfully completed, or none of them are, preventing data corruption or inconsistencies.

Key characteristics of transactions in IndexedDB include :

* Atomicity
* Isolation
* Consistency
* Durability
* Read and Write Operations

Transactions in IndexedDB are created explicitly by the application code and are typically scoped to a specific object store or a set of object stores within the database. Once a transaction is created, database operations (such as adding, retrieving, updating, or deleting data) are performed within the context of that transaction.


Example of starting a read-write transaction :
var transaction = db.transaction(["objectStoreName"], "readwrite");?

Example of starting a read-only transaction :
var transaction = db.transaction(["objectStoreName"], "readonly");?

Transactions are a fundamental aspect of IndexedDB's design, providing a mechanism for ensuring data integrity, consistency, and concurrency control in client-side web applications. They play a crucial role in maintaining a reliable and robust data storage system within the browser environment.
IndexedDB is transactional. Transactions ensure data integrity and consistency by allowing atomic operations. They provide isolation for multiple database operations and allow for rollback in case of errors, ensuring data integrity and reliability.
23 .
Can IndexedDB be used across different browsers?
Yes, IndexedDB is a standard web API supported by all modern browsers. However, implementation details may vary.
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);
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
};?
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.
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 deleteRequest = indexedDB.deleteDatabase("myDatabase");

deleteRequest.onsuccess = function() {
  console.log("Database deleted successfully");
};

deleteRequest.onerror = function(event) {
  console.error("Error deleting database:", event.target.errorCode);
};?
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.
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();
    console.log("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
};?
To store data in an IndexedDB database, you need the following components: database object, object store, key for records, transaction for data operations, and requests to perform CRUD operations on the data.
Technical Details: 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.

Code Example :
let openRequest = indexedDB.open("myDatabase", 1);

openRequest.onupgradeneeded = function(e) {
  let db = e.target.result;

  // Create an object store named "books" with "isbn" as the key path
  let store = db.createObjectStore("books", { keyPath: "isbn" });
};

openRequest.onsuccess = function(e) {
  let db = e.target.result;

  // Start a transaction on the "books" object store
  let transaction = db.transaction("books", "readwrite");
  let store = transaction.objectStore("books");

  // Add a book to the object store
  let book = { isbn: "9781234567890", title: "Sample Book" };
  let request = store.add(book);

  request.onsuccess = function() {
    console.log("Book added successfully");
  };

  request.onerror = function(event) {
    console.error("Error adding book:", event.target.errorCode);
  };

  // Commit the transaction
  transaction.oncomplete = function() {
    console.log("Transaction completed");
  };

  transaction.onerror = function(event) {
    console.error("Transaction error:", event.target.errorCode);
  };

  // Close the database connection
  db.close();
};

openRequest.onerror = function(e) {
  console.error("Unable to open database:", e.target.errorCode);
};?
30 .
Is there a specific type of key that we must use in IndexedDB?
No, there isn't a specific type of key that must be used in IndexedDB. Keys can be of various types, including numbers, strings, dates, and even arrays. The choice of key type depends on the data and requirements of your application.
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);
// Create an objectStore for this database
let objectStore = db.createObjectStore('toDoList', { keyPath: 'taskTitle' });?
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.

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
  }
};?
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);
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);
};?
The two methods for storing data in an IndexedDB object store are `put()` and `add()`. Both store key-value pairs, but behave differently on key conflict.

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);
Values in an IndexedDB object store can be deleted using the `delete` method, which takes a key or key range as argument, or by calling `clear` to remove all records from the store. 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.?
By default, an IndexedDB Object store sorts values in ascending order by key. The key is a unique identifier for each record in the object store. If two records have the same key, they will be sorted by their value. This ordering is followed during cursor-based traversal or when getting multiple records using key ranges.
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));?
There are two ways of searching in an object store : searching by key-value or key range or another object field. This process requires an additional data structure named “index”.

Code Example :

Here's an example of both types of searches :
let request = indexedDB.open("myDB");
request.onsuccess = function(e) {
    let db = e.target.result;
    let tx = db.transaction("ObjectStore");
    let store = tx.objectStore("ObjectStore");
 
    // Get operation
    let getRequest = store.get(1);
    getRequest.onsuccess = function() {
        console.log("Get operation:", getRequest.result);
    };

    // Cursor operation
    let cursorRequest = store.openCursor();
    cursorRequest.onsuccess = function() {
        let cursor = cursorRequest.result;
        if (cursor) {
            console.log("Cursor operation:", cursor.value);
            cursor.continue();
        }
    };
};?

In this example, the "get" operation retrieves the object with key 1, and the "cursor" operation iterates over all the objects in the store.
No, it's not necessary to have `onerror`/`onsuccess` handlers for every request in IndexedDB. You can use a single set of handlers at the transaction level to handle success or error events for multiple requests within the transaction.

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
  }
};?
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
    };
  });
};?