Earlier in the book, we started integrating IndexedDB in an Angular application. We also established a use case for creating data, namely, user comments, offline. Imagine that a user is on a game details page and attempts to add a comment. However, the user loses network access. The Web Arcade application is resilient because it saves the comments on the client device/browser temporarily. When the user is back online, the application synchronizes the comments online.
This chapter elaborates on how to create data offline. It begins with instructions to identify if the application is online or offline. You use the status to determine how to reach server-side services or use the local IndexedDB store. Next, the chapter details how to add a comment to IndexedDB when offline. It details how to provide feedback to the user that the application is offline but the data is temporarily saved.
Then, the chapter covers how to synchronize offline comments with the server-side services once the application is back online. Remember, the server-side database and the services are the source of truth for the data. IndexedDB is temporary and provides a seamless experience to the user.
Adding Comments Online and Offline
The previous chapter described how to add a comment. The submit action calls the server-side HTTP endpoint. If the device is offline and has lost network connectivity, a typical web application shows an error. Once online, users might have to retry the operation. As mentioned earlier, Web Arcade uses IndexedDB, persists data temporarily, and synchronizes when the remote service is available.
Identifying the Online/Offline Status with a Getter
To identify whether the device (and the browser) is online, use the JavaScript API on the navigator object. It is a read-only property on the window object. The field onLine returns the current status, which is true if online and false if offline.
You can run a similar command on a browser of your choice. Figure 9-1 shows Google Chrome, which was chosen arbitrarily.
Remember, we created a service called IdbStorageAccessService to encapsulate access to IndexedDB. The online/offline status determines the components that can access IndexedDB. Hence, you should include lines of code to determine the online/offline status in the service.
Inject the Window Service
Provide the Window Service
IsOnline Getter as Part of IdbStorageAccessService
Adding Online/Offline Event Listeners
It is possible that you will encounter a scenario where an action needs to be performed when the application goes online or offline. The window object (and hence the window service) provides the events online and offline. Add these events to IdbStorageAccessService at the time of initialization. The event handler callback function is invoked any time the event occurs.
Online and Offline Events
Adding Comments to IndexedDB
IdbStorageAccessService
In line 4, an IndexedDB reference is set on a class variable, namely, idb.
Next, in the init() function (which initializes the service and appears on line 11), run the open() function on the idb object. It returns an object of the IDBOpenDBRequest object.
- If this is the first time a user has 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 (a value of 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.
- 1.The IndexedDB APIs are asynchronous. The open action does not complete in line 11. You provide a function callback for the success and failure scenarios. They are invoked as a result of the open action.
- a.
Notice the onsuccess() function callback on lines 13 to 16, which is invoked if the open database action is successful.
- b.
The onerror() function callback on lines 18 to 20 is invoked if the open database action fails.
- c.
The open function call returns IDBOpenDBRequest. The previous callback functions onsuccess and onerror are provided to this returned object.
- 2.See the code on lines 22 to 28, where onupgradeneeded is triggered after creating or opening IndexedDB. You provide a callback function, which is invoked by the browser when this event occurs. What is the significance of the onupgradeneeded event?
- a.
In the case of a new database, the callback function is a good place to create object stores. In the current use case, you create an object store to save game comments. You name it gameComments.
- b.
For a pre-existing database, if an upgrade is required, you may perform design changes here.
- 3.
Finally, on lines 30 to 38, see the function callback for online and offline events when the browser goes online/offline.
Access the web-arcade Database Reference
See line 5. indexedDB is a class variable accessible across the service.
A value is assigned on the successful opening of the web-arcade database, as shown on line 12. The database instance is available in the result variable on the target property of the event object (event.target.result).
Add a New Record in IndexedDB
Consider the following explanation:
- First, create a new transaction with the class variable indexedDB (created in Listing 9-6). See the transaction function on line 3. It takes two parameters:
- a.
One or more object stores in which a transaction needs to be created. In this case, you create a transaction on an object store gameComments.
- b.
Specify the transaction mode, readwrite. IndexedDB supports three modes of transactions, namely, readonly, readwrite, and versionchange. As you can imagine, readonly helps with retrieve operations, and readwrite helps with create/update/delete operations. However, versionchange mode helps create and delete object stores on an IndexedDB.
Next, perform the add record action on IndexedDB. Use the transaction object to access the object store on which the add action needs to be performed. See line 5, which uses the objectStore function to access the object store().
See lines 6 and 15. You store a JavaScript object including the fields for the comment title, username, time commented, comment description, game ID on which the comment was added, and a unique comment ID. To ensure uniqueness, you use a time value. You may use any unique value.
- As you have seen with IndexedDB, the database actions are asynchronous. The add() function does not immediately add a record. It eventually invokes a success or error callback function. A transaction has the following callback functions:
- a.
oncomplete: This is invoked on success. See line 18. It prints the status on the console.
- b.
onerror: This is invoked on error. See line 19.
The User Experience of Adding Comments
Remember from the previous chapter that UserDetailsComponent adds a comment by calling the GameService function named addComments. This invokes the server-side POST call to add a comment. If the application is offline, it will error out. You show an error feedback to the user and request the user to retry.
Add a Comment in the Game Details Component
Consider the following explanation:
At the beginning, inject IdbStorageAccessService. See line 4. The service instance is named idbSvc.
- Line 11 checks if the application is online. Notice that you use the IsOnline getter created in Listing 9-3.
- a.
If true, continue to call the game service function, addComments(). It invokes the server-side service.
- b.
If offline, use the IdbStorageAccessService function addComment(), which adds the comment to IndexedDB. See the implementation in Listing 9-7.
Notice on line 21 that you show a Snackbar component message that the application is offline. Figure 9-4 shows the result.
Synchronizing Offline Comments with the Servers
When the application is offline, you cache the comments within the browser in persistent storage using IndexedDB. Eventually, once the application is back online, when the user launches the application again, the comments need to be synchronized with the server side. This section details the implementation to identify that the application is online and synchronize the comment records.
The two events online and offline on window objects are triggered when the browser gains or loses connectivity. The IdbStorageAccessService service includes event handlers for the online and offline events. See Listing 9-4.
- 1.
Retrieve all the cached comments from IndexedDB.
- 2.
Invoke a server-side HTTP service, which updates the primary database for the user comments.
- 3.
Finally, clear the cache. Delete comments synchronized with the remote service.
Let’s begin with the first step in the previous list, retrieving all the cached comments from IndexedDB. The following section details various options and the available API to retrieve data from IndexedDB.
Retrieving Data from IndexedDB
getAll(): Retrieves all records in the object store
As mentioned earlier, the CRUD operations run in the scope of a transaction. Hence, you will create a read-only transaction (considering it is a data retrieval operation) on the object store. Call the getAll() API, which returns IDBRequest, as shown in Listing 9-9.
Using getAll()
get(key): Retrieves a record by key. The get() function is run on an object store.
Similar to getAll(), create a read-only transaction for get() on the object store. The get() API returns IDBRequest, as shown in Listing 9-10.
Using get()
openCursor(): A cursor allows you to iterate through the results. It lets you act on one record at a time. We chose this option for the comments use case. It provides flexibility to transform the data format as and when you read from IndexedDB. The other two APIs, getAll() and get(), require an additional code loop to transform the data.
As mentioned earlier, the CRUD operations run in the scope of a transaction. Hence, you will create a read-only transaction (considering it is a data retrieval operation) on the object store. Call the openCursor() API, which returns IDBRequest.
Again, code handling the result or an error remains the same. On the IDBRequest object, provide the onsuccess and onerror callback function definitions. The data retrieval with openCursor() is asynchronous, which invokes above mentioned onsuccess or onerror callback functions.
Retrieve Cached Comments from IndexedDB
The function creates and returns a promise. See line 2. Considering the data retrieval is asynchronous, you cannot instantly return comment records from the getAllCachedComments() function . The promise is resolved once the cursor finishes retrieving data from IndexedDB.
Lines 9 and 12 create a read-only transaction, access object store gameComments, and open a cursor. This statement returns an IDBRequest object, which is assigned to a local variable query.
Remember, the onsuccess callback is invoked if the cursor is able to retrieve data from the object store. Otherwise, the onerror callback is invoked (lines 28 and 30).
See onsuccesscallback() defined in lines 14 to 26.
Access the results at event.target.result. See line 16.
The ?. syntax in evt?.target?.result performs a null check. If a property is undefined, it will return null, instead of throwing an error and crashing the entire function workflow. The previous statement may return the results or null.
If the results are defined, transform the data to key-value pair format. Add the object to a local variable called result.
Remember, the cursor works on a single comment record at a time (unlike get() and getAll()). To move the cursor to the next record, call the continue function on the query object. Remember, the query object is an IDBRequest object returned by openCursor().
The if condition on line 17 results in a true value until all the records in the cursor are exhausted.
When false, as the entire dataset (comment records) is retrieved and added to the local variable result, resolve the promise. The calling function uses the results successfully resolved from getAllCachedComments() .
- 1.
Retrieve all the cached comments from the IndexedDB.
Next, let’s proceed to the other two steps:
- 2.
Invoke a server-side HTTP service, which updates the primary database for the user comments.
- 3.
Finally, clear the cache. Delete comments synchronized with the remote service.
Bulking Updating Comments on the Server Side
A user might have added multiple comments while the application was offline. It is advisable to upload all the comments in a single call. The server-side HTTP POST endpoint /comments accepts an array of comments.
Add Bulk Comments
The function addBulkComments() uses anonymous data type as a parameter. The comments variable is of type Array<{title: string, userName: string, comments: string, gameId: number, timeCommented: Date}>. The highlighted type is without a name. You may use this technique for one-off data types.
You may choose to create a new entity and use it.
The service function is now available, but it has not been called yet. However, you have the service function available to bulk update the cached comments. Before we start using this function, consider adding a function to delete.
- 1.
Retrieve all the cached comments from the IndexedDB.
- 2.
Invoke a server-side HTTP service, which updates the primary database for the user comments.
- 3.
Finally, clear the cache. Delete comments synchronized with the remote service.
Next, add code to clean up IndexedDB.
Deleting Data from IndexedDB
delete(): Removes records in the object store. This selects the record to be deleted by its record ID.
As mentioned earlier, the CRUD operations run in the scope of a transaction. Hence, you will create a read-write transaction on the object store. Call the getAll() API, which returns IDBRequest.
Using delete()
- 1.
Retrieve all the cached comments from the IndexedDB.
- 2.
Invoke a server-side HTTP service, which updates the primary database for the user comments.
- 3.
Finally, clear the cache. Delete comments synchronized with the remote service.
The Online Event Handler
Consider the following explanation:
First, you retrieve all cached comments. See line 2, which calls the getAllCachedComments() service function. See Listing 9-11 to review retrieving cached comments from IndexedDB.
The function returns a promise. When the promise is resolved, you have access to the comment records from IndexedDB. You use this data to add comments in the back end, synchronizing server-side services and databases.
- Before you call the server-side service, transform the comment record to the request object structure. You loop through all the comments and change the field names as required by the server-side service.
- a.
Listing 9-15 defines a private function called transformCommentDataStructure() . Notice the forEach() on the array of comments obtained from the IndexedDB object store. The comments are transformed and added to a local variable, comments. This is returned at the end of the function.
Next call the GameService function addBulkComments() , which in turn calls the server-side service. To review the addBulkComments() function, see Listing 9-12.
Remember, the function addBulkComments() returns an observable. You subscribe to the observable, which has handlers for success and failure. The success handler indicates the comments are added/synchronized with the server side. Hence, you can now delete cached comments in IndexedDB.
- Invoke a private function deleteSynchronizedComments() defined as part of the service IdbStorageAccessService. It loops through each comment record and deletes the comments from the local database. See Listing 9-16 for the deleteSynchronizedComments() function definition.
- a.
Notice that the forEach loop uses an anonymous type with a key-value pair. See line 3 (r: {key: number; value: any}). It defines the expected structure for the comments data.
- b.
deleteComment() deletes each record by its ID. To review the function again, see Listing 9-13.
Transform Comments Data
Delete Synchronized Comments
Synchronized Comments with Online Event Handler
Updating Data in IndexedDB
put(): Updates records in the object store. This selects the record to be updated by its record ID.
As mentioned earlier, the CRUD operations run in the scope of a transaction. Hence, you will create a read-write transaction on the object store. Call the put() API, which returns IDBRequest.
Update Records in IndexedDB
You create a new function to update comments. Imagine a form that allows users to edit a comment. The previous function can perform this action.
Note The current use case does not include an edit comment use case. The previous function is for demonstrating the put() API on IndexedDB.
Notice the commented lines of code between lines 2 and 9. This provides an arbitrary structure for updated comment data. However, the calling function provides the updated comment in an updatedRecord variable.
- See line 14. The put function takes two parameters.
- a.
updatedRecord: This is the new object to replace the current one.
- b.
recordId: This identifies the record to be updated by the second parameter, recordId.
Summary
This chapter provided an elaborate explanation for adding records to IndexedDB. In the Web Arcade use case with the game details page, the application allows users to add comments offline. The data is temporarily cached in IndexedDB, which is eventually synchronized with server-side services.
You have seen how to use the put() API to update a record in IndexedDB. Add the ability to edit comments. If the application is offline, provide the ability to temporarily save edits in IndexedDB.
Notice that the deleteComment() function deletes records one at a time. Provide error handling to identify and correct failures.
Provide a visual indicator when the application is offline. You may choose to change the color of the toolbar and the title.