© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
V. K. KotaruBuilding Offline Applications with Angularhttps://doi.org/10.1007/978-1-4842-7930-4_7

7. Introduction to IndexedDB

Venkata Keerti Kotaru1  
(1)
-, Hyderabad, Telangana, India
 

So far you have cached the application skeleton and HTTP GET service calls. A RESTful service provides GET calls for data retrieval. However, HTTP also supports POST to create entities, PUT and PATCH for updates, and DELETE to remove entities. The sample application Web Arcade does not yet support offline access to service calls beyond the GET calls.

This chapter introduces IndexedDB for more advanced offline actions. In this chapter, you will get a basic understanding of IndexedDB, which runs on the client side on browsers. You will learn to get started with IndexedDB in an Angular application. JavaScript provides APIs to integrate with IndexedDB. You can create, retrieve, update, and delete data to/from IndexedDB, which is supported by most modern browsers. The chapter focuses on structuring the database including creating object stores, indices, etc. In the next chapter, you will work with data by creating and deleting records.

Traditionally, web applications used various features for client-side storage including cookies, session storage, and local storage. Even today they are highly useful for storing reasonably small amounts of data. IndexedDB, on the other hand, provides an API for more sophisticated client-side storage and retrieval. The JavaScript API is natively supported by most modern browsers. IndexedDB provides persistent storage for relatively large amounts of data including JSON objects. However, no database supports storing an unlimited amount of data. The browsers set an upper limit on the amount of data stored in IndexedDB relative to the size of the disk and the device.

IndexedDB is useful for persisting structured data. It saves data in key-value pairs. It works like a NoSQL database, which supports using object stores that contain records of data. The object stores are comparable to tables in a relational database. The traditional relational databases largely use tables with a predefined structure in terms of columns and constraints (primary key, foreign key, etc.). However, IndexedDB uses object stores to persist records of data.

IndexedDB supports high-performance searches. Data is organized with the help of indexes (defined on an object store), which help to retrieve data faster.

Terminology

Consider the following terminology working with IndexedDB:
  • Object store: An IndexedDB may have one or more object stores. Each object store acts as a container for key-value pairs of data. As mentioned, an object store is comparable to tables in relational databases.
    • An object store provides structure to the IndexedDB. Create one or more object stores as logical containers of the application data. For example, you may create an object store named users to store user details and another named games to persist a list of game-related objects.

  • Transactions: Data operations on IndexedDB are performed in the context of a transaction. This helps maintain data consistency. Remember, IndexedDB stores and retrieves data on the client side in a browser. It is possible that more than one instance of the applications are open by the user. It can create scenarios, where create/update/delete operations are partially performed by each instance of the browser. One of the browsers may retrieve stale data while an update operation is in-progress.
    • Transactions help avoid the previously mentioned problems. A transaction locks data records until the operation is complete. The data access and modification operations are atomic. That is, an create/update/delete operation is either fully done or completely rolled back. A retrieve operation is performed only after a data modification operation is completed or rolled back. Hence, retrieve never returns inconsistent and stale data objects.

    • IndexedDB supports three modes of transactions, namely, readonly, readwrite, and versionchange. As you can imagine, readonly helps with retrieve operations and readwrite with create/update/delete operations. However, versionchange mode helps create and delete object stores on an IndexedDB.

  • Index: An index helps retrieve data faster. An object store sorts data in the ascending order of the key. A key implicitly does not allow duplicate values. You can create additional indices that also act as a uniqueness constraint. For example, adding an index on a Social Security number or national ID ensures there are no duplicates in IndexedDB.

  • Cursor: A cursor helps iterate over records in an object store. It is useful while iterating over the data records during the query and retrieval process.

Getting Started with IndexedDB

IndexedDB is supported by major browsers. The API enables applications to create, store, retrieve, update, and delete records in a local database, within the browser. This chapter details using IndexedDB with the native browser API.

The following are the typical steps when working with IndexedDB:
  1. 1.

    Create and/or open a database: For the first time, create a new IndexedDB database. As the user comes back to the web application, open the database to perform actions on the database.

     
  2. 2.

    Create and/or use an object store: Create a new object store the first time a user accesses the functionality. As mentioned earlier, an object store is comparable to a table in a relational database management system (RDBMS). You may create one or more object stores. You create, retrieve, update, and delete documents in an object store.

     
  3. 3.

    Start a transaction: Actions are performed on an IndexedDB object store as a transaction. It enables you to maintain a consistent state. For example, it is possible the user closes the browser while an action is being performed on the IndexedDB database. As the action is performed in the context of a transaction, if it is not complete, the transaction is aborted. A transaction ensures an error or an edge condition does not leave the database in an inconsistent or unrecoverable state.

     
  4. 4.

    Perform CRUD: Like any database, you create, retrieve, update, or delete documents in an IndexedDB.

     

Consider the following Web Arcade use case, taking advantage of IndexedDB.

In an earlier chapter, you have seen a page showing a list of board games. Consider supporting a new use case with a game details page. Figure 7-1 details all the information about a game. Below the game description, you show a list of user comments and a form allowing users to add new comments. As a user types in a new comment and submits the form, you post this data to a remote HTTP service. The service ideally persists the user comment in a permanent storage/database like MongoDB or Oracle or Microsoft SQL Server. Considering the server-side code is not in the scope of this book, we will keep it simple. In the next chapter, the code samples showcase a service that stores the user comments in a file.

Figure 7-1 shows the section of the page with a current list of comments and a form that allows users to submit new comments.
Figure 7-1

List and submit comments on a game details page

The submit action creates a new comment. The service endpoint is an HTTP POST method. As mentioned, the Web Arcade supports offline access on HTTP GET calls. Imagine losing connectivity as a user types in a comment and clicks Submit. A typical web application returns an error or a message similar to “Page can’t be displayed.” Web Arcade is designed to be resilient to the loss of network connectivity. Hence, Web Arcade caches user comments and synchronizes with a server-side service when the user returns to the application.

Angular Service for IndexedDB

Create a new service by running the following command:
ng generate service common/idb-storage-access
The command creates a new service called IdbStorageAccessService in the directory src/app/common. The service is to abstract code statements accessing IndexedDB. It is a central service that uses a browser API to integrate with IndexedDB. During initialization, the service performs one-time activities like creating a new IndexedDB store or opening the database if it already exists. See Listing 7-1.
01: @Injectable()
02: export class IdbStorageAccessService {
03:
04:   idb = this.windowObj.indexedDB;
05:
06:   constructor(private windowObj: Window) {
07:   }
08:
09:   init() {
10:     let request = this.idb
11:       .open('web-arcade', 1);
12:
13:     request.onsuccess = (evt:any) => {
14:       console.log("Open Success", evt);
15:     };
16:
17:     request.onerror = (error: any) => {
18:       console.error("Error opening IndexedDB", error);
19:     }
20:   }
21:
22: }
23:
Listing 7-1

Initialize IndexedDB with the IdbStorageAccessService

Note

By default, the ng generate service command provides the service at the root level. In the context of the Web Arcade application, you may want to remove the provideIn: 'root' statement on line 1. Just leave the inject() decorator , as shown in the first line.

This is explained in detail in the following section along with Listing 7-2.

Consider the following explanation:
  • Line 4 creates the class variable idb (short for IndexedDB). It is set to the indexedDB instance on the global window object. The indexedDB object has an API that helps open or create a new IndexedDB. Line 4 runs while initializing IdbStorageAccessService, similar to constructor.

Note

Notice, the global window object is accessed through a Window service. See the constructor on line 6. It injects the window service. The instance variable is named windowObj. The Window service is provided in the AppModule.

  • See lines 9 to 20 for the init() function initializing the service.

  • See lines 10 and 11 that run the open() function on the idb object. If it is the first time a user opened the application on a browser, it creates a new database.
    1. a.

      The first parameter is the name of the database, web-arcade.

       
    2. b.

      The second parameter (value 1) specifies a version for the database. As you can imagine, new updates to the application cause changes to the IndexedDB structure. The IndexedDB API enables you to upgrade the database as the version changes.

       

To return a user, the database was already created and available on a browser. The open() function attempts to open the database. It returns an object of the IDBOpenDBRequest object.

Figure 7-2 shows a new IndexedDB web-arcade that was created. The image was captured using Google Chrome’s developer tools. Similar functionality is available for developers on all major browsers including Firefox and Microsoft Edge.
Figure 7-2

IndexedDB in Google Chrome Dev Tools

Almost all the IndexedDB APIs are asynchronous. An action like open does not attempt to complete the operation immediately. You specify a callback function, which is invoked after completing the action. As you can imagine, the open action can be successful or error out. Hence, define a callback function for each outcome, onsuccess or onerror. See lines 13 to 15 and lines 17 to 19 in Listing 7-1. For the moment, you just print the result on the console (lines 14 and 18). We will further enhance handling the result in the upcoming code snippets.

When is the init() function invoked? It is one of the methods on the Angular service. You may invoke it in a component, which means IndexedDB is initialized only when you load (or navigate) to the component. On the other hand, an application like Web Arcade is highly dependent on IndexedDB. You may need to make use of the service from multiple components. The service needs to complete initialization and be ready for CRUD operations. Hence, it is a good idea to initialize the service along with the application, while the primary module AppModule starts. Consider Listing 7-2.
03: import { NgModule, APP_INITIALIZER } from '@angular/core';
15: import { IdbStorageAccessService } from './common/idb-storage-access.service';
18:
19: @NgModule({
20:   declarations: [
21:     AppComponent,
25:   ],
26:   imports: [
27:     BrowserModule,
40:   ],
41:   providers: [
42:     IdbStorageAccessService,
43:     {
44:       provide: APP_INITIALIZER,
45:       useFactory: (svc: IdbStorageAccessService) => () => svc.init(),
46:       deps: [IdbStorageAccessService], multi: true
47:     }
48:   ],
49:   bootstrap: [AppComponent]
50: })
51: export class AppModule { }
52:
Listing 7-2

Initialize IndexedDB with IdbStorageAccessService

Considering the following explanation:
  • See lines 42 to 48. The first line (line 42) in the block provides a newly created IDBStorageAccessService. Why do we need it? As you have seen, we did not provide the service at the root level. We removed the line of code provideIn: 'root' in IdbStorageAccessService (Listing 7-1).

  • See lines 43 to 47, which provide APP_INITIALIZER and use the factory function, which invokes init().

  • In summary, we provide and initialize the IdbStorageService at a module level. In this example, you do it in AppModule . It could have been any module.

    It creates and/or opens the Web-Arcade IndexedDB on the browser. It keeps the database ready for further operations (e.g., CRUD). This code eliminates the need to inject the service into a component (or another service) and call the init() function. The service initializes along with the AppModule.

Creating Object Store

While the database is the highest level in IndexedDB, it can have one or more object stores. You provide a unique name to each object store within a database. An object store is a container that persists data. In the current example with Web Arcade, you will see how to save JSON objects. For ease of understanding, an object store is comparable to tables in a relational database.

Using “onupgradeneeded” Event

An event called onupgradeneeded is triggered after creating or opening an IndexedDB. You provide a callback function that is invoked by the browser when this event occurs. In the case of a new database, the callback function is a good place to create object stores. For a pre-existing database, if an upgrade is required, you may perform design changes here. For example, you may create new object stores, delete unused object stores, and modify an existing object store by deleting and re-creating it. Consider Listing 7-3.
01: init() {
02:     let request = this.idb
03:         .open('web-arcade', 1);
04:
05:     request.onsuccess = (evt: any) => {
06:         console.log("Open Success", evt);
07:     };
08:
09:     request.onerror = (error: any) => {
10:         console.error("Error opening IndexedDB", error);
11:     }
12:
13:     request.onupgradeneeded = function (event: any) {
14:         console.log("version upgrade event triggered");
15:         let dbRef = event.target.result;
16:         dbRef
17:             .createObjectStore("gameComments", { autoIncrement: true });
18:     };
19: }
Listing 7-3

onupgradeneeded Event Callback

Considering the following explanation:
  • Notice that the code snippet repeated the init() function from Listing 7-1. In addition to the onsuccess and onerror callbacks, an event handler called onupgradeneeded is included. See lines 13 to 18.

  • The event is provided as a parameter to the function callback.

  • You can access a reference to IndexedDB on the event target on an object, namely, target.

  • Use the db reference to create an object store. In this example, you name the object store gameComments. As explained earlier, you use IndexedDB and the object store to cache user comments if the user loses connectivity.

An object store persists data in key-value pairs. As you will see in the next few sections, data is retrieved using the key. It is a primary key uniquely identifying a value stored in IndexedDB. The following are the two options to create a key (for the values stored in an object store). This is decided at the time of creating an object store. See line 17 in Listing 7-3. Notice the second parameter on the createObjectStore() function . You specify one of the following two options:
  • Auto increment: IndexedDB manages the key. It creates a numeric value and increments for every new object added in the object store.
    dbRef.createObjectStore("gameComments", {
    autoIncrement: true });
  • Key path: Specify a key path within the JSON object being added. As the key value is provided explicitly, ensure you provide unique values. A duplicate value causes the insert to fail.

    A field called commentId is provided as a keypath. If used, ensure you provide a unique value for commentId.
    dbRef.createObjectStore("gameComments", {
    keypath: 'commentId' });
Note

A key path can be supplied only for a JavaScript object. Hence, creating an object store with a key path constrains it to store only the JavaScript objects. However, with auto increment, considering the key is managed by IndexedDB, you may store any type of object including a primitive type.

See Figure 7-3 with the newly created gameComments object store.
Figure 7-3

gameComments object store and a sample value

Creating Index

While defining an object store, you can create additional indices that also act as a uniqueness constraint. The index is applied on a field in the JavaScript object persisted in the object store. Consider the following code snippet. It explains the createIndex API on the object store reference.
objectStoreReference.createIndex('indexName', 'keyPath', {parms})
Consider the following explanation:
  • Index name: The first parameter is an index name (arbitrary).

  • Key path: The second parameter, keypath, specifies that the index needs to be created on the given field.

  • Params: You may specify the following parameters for creating an index:
    1. a. 

      unique: This creates an uniqueness constraint on a field provided at the keypath.

       
    2. b. 

      multiEntry: This is applied on an array.

       

If true, the constraint ensures each value in the array is unique. An entry is added to the index for each element in the array.

If false, the index adds a single entry for the entire array. The uniqueness is maintained at the array object level.

In the gameComments object store, imagine each comment will have an ID. To ensure the ID is unique, add an index. Consider Listing 7-4.
1: request.onupgradeneeded = function(event: any){
2:     console.log("version upgrade event triggered");
3:     let dbRef = event.target.result;
4:     let objStore = dbRef
5:       .createObjectStore("gameComments", { autoIncrement: true })
6:
7:     let idxCommentId = objStore.createIndex('IdxCommentId', 'commentId', {unique: true})
8:   };
Listing 7-4

Create Index IdxCommentId for the Comment ID

Notice that line 7 creates an index using an object store reference, objStore. The index is named IdxCommentId. The index is added to the commentId field. You can see that the parameter unique is set to true, which ensures commentId is distinct for each record. Figure 7-4 showcases the object store with the new index.
Figure 7-4

Index IdxCommentId on an object store

Browser Support

Figure 7-5 depicts the browser support for the global indexedDB object (windowObj.indexedDB). Notice that the data is captured on the Mozilla website, at https://developer.mozilla.org/en-US/docs/Web/API/indexedDB. It is a reliable and open source platform for web technologies. Mozilla has been an advocate of the open web and has pioneered safe and free Internet technologies including the Firefox browser.
Figure 7-5

window.indexedDB browser support

Also refer to CanIUse.com, which is a reliable source of browser compatibility data. For IndexedDB, use the URL https://caniuse.com/indexeddb.

Limitations of IndexedDB

While IndexedDB provides a good solution for client-side persistence and querying in a browser, it is important to be aware of the following limitations:
  • It does not support internationalized sorting, so sorting non-English strings could be tricky. Few languages sort strings differently from English. At the time of writing this chapter, localized sorting is not fully supported by IndexedDB and all the browsers. If this feature is important, you might have to retrieve data from the database and write additional custom code to sort.

  • There is no support for full-text search yet.

  • IndexedDB cannot be treated as a source of truth for data. It is temporary storage. Data could be lost or cleared in the following scenarios:
    1. a.

      The user resets the browser or clears the database manually.

       
    2. b.

      The user launches the application in a Google Chrome Incognito window or a private browsing session (on other browsers). As the browser window is closed, considering it was a private session, the database will be removed.

       
    3. c.

      Disk quota for persistent storage is calculated based on a few factors including available disk space, settings, device platform, etc. It is possible the application crossed the quota limit and further persistence failed.

       
    4. d.

      Miscellaneous situations including corrupt databases, a bug upgrading the database caused by an incompatible change, etc.

       

Summary

This chapter provided a basic understanding of IndexedDB, which runs on the client side on browsers. JavaScript provides a native API to work with IndexedDB. It is supported by most modern browsers.

The chapter also explained how to initialize the Angular service along with AppModule. In the initialization process, you create or open IndexedDB store for Web Arcade. You create a new IndexedDB store if it is the first time a user is accessing the application on a browser. You open the pre-existing database if it is already there.

Next, the chapter explained how to use the onupgradeneeded function callback for creating an object store and indices. These are one-time activities for the first time a user accesses the application.

Exercise
  • Create an additional object store for creating new games. Perform the action while loading the application (or the Angular module).

  • Create the object store to use a designated ID as the key (primary). Do not use auto increment.

  • Create an additional index on the game title. Ensure it is unique.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.146.71