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
- 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.
- 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.
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.
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.
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.
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
Initialize IndexedDB with the IdbStorageAccessService
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.
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.
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.
- a.
The first parameter is the name of the database, web-arcade.
- 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.
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.
Initialize IndexedDB with IdbStorageAccessService
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
onupgradeneeded Event Callback
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.
- 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' });
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.
Creating Index
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:
- a.
unique: This creates an uniqueness constraint on a field provided at the keypath.
- 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.
Create Index IdxCommentId for the Comment ID
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
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:
- a.
The user resets the browser or clears the database manually.
- 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.
- 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.
- 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.
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.