© 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_6

6. Upgrading Applications

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

So far you have created an Angular application, registered service workers, and cached application resources. This chapter details how to discover an update to the application, communicate with users, and handle events to gracefully upgrade to the next version.

The chapter extensively uses Angular’s SwUpdate service, which provides ready-made features to identify and upgrade the application. It begins with instructions to include (import and inject) the SwUpdate service . Next, it details how to identify an available upgrade and activate the upgrade. It also details how to check at regular intervals for an upgrade. Toward the end, the chapter details how to handle an edge case, namely, an error scenario when browsers clean up unused scripts.

Considering the Web Arcade application is installable, you need a mechanism to look for updates, notify the user about new versions of the application, and perform an upgrade. A service worker manages installing and caching the Angular application. This chapter details working with SwUpdate, an out-of-the-box service provided by Angular to ease service worker communication. It gives access to events when a new version of the application is available, downloaded, and activated. You may use the functions in this service to do periodic checks for updates.

Getting Started with SwUpdate

This section shows you how to get started with the SwUpdate service by importing and injecting the service. The SwUpdate service is part of the Angular module ServiceWorkerModule, which was referenced in the import list of AppModule already. Verify the code in the sample application in the app.module.ts file. It was included when you ran the Angular CLI ng add @angular/pwa command in Chapter 2. Consider Listing 6-1, lines 15 and 22.
01: import { NgModule } from '@angular/core';
02: import { BrowserModule } from '@angular/platform-browser';
03: import { environment } from '../environments/environment';
04: import { ServiceWorkerModule } from '@angular/service-worker';
05: import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
06:
07: import { AppComponent } from './app.component';
08:
09: @NgModule({
10:   declarations: [
11:     AppComponent,
12:   ],
13:   imports: [
14:     BrowserModule,
15:     ServiceWorkerModule.register('ngsw-worker.js', {
16:       enabled: environment.production,
17:       // Register the ServiceWorker as soon as the app is stable
18:       // or after 30 seconds (whichever comes first).
19:       registrationStrategy: 'registerWhenStable:30000'
20:     }),
21:     BrowserAnimationsModule
22:   ],
23:   providers: [],
24:   bootstrap: [AppComponent]
25: })
26: export class AppModule { }
27:
Listing 6-1

ServiceWorkerModule Imported in AppModule

Ensure ServiceWorkerModule is imported as shown (in bold). This enables the SwUpdate service to be readily usable. Create a new Angular service to encapsulate the code for identifying a new version of the service worker, communicating with the user, and managing the details. This helps with code reusability and separation of concerns. To install the service, run the following command:
ng g s common/sw-communication
Note

In the snippet, g is short for “generate,” and s is for “service.”

You can rewrite the previous command as ng generate service common/sw-communication.

The Angular CLI command creates a new service called SwCommunicationService in the directory common. By default it is provided at the root level. Import and inject the SwUpdate service into SwCommunicationService, as shown in Listing 6-2.
01: import { Injectable } from '@angular/core';
02: import { SwUpdate } from '@angular/service-worker';
03:
04: @Injectable({
05:   providedIn: 'root'
06: })
07: export class SwCommunicationService {
08:   constructor(private updateSvc: SwUpdate)
09:   }
10: }
Listing 6-2

Scaffolded SwCommunicationService

Lines 2 and 8 import and inject the SwUpdate Angular service. Line 5 provides the service at the root level. Although the service is provided at the root level, it is not yet used in the application. Unlike other services created in Web Arcade, it needs to run in the background, when you launch the application or at regular intervals. Hence, import and inject swCommunicationService in the root component, the AppComponent, as shown in Listing 6-3, lines 2 and 9.
01: import { Component } from '@angular/core';
02: import { SwCommunicationService } from 'src/app/common/sw-communication.service';
03: @Component({
04:   selector: 'app-root',
05:   templateUrl: './app.component.html',
06:   styleUrls: ['./app.component.sass']
07: })
08: export class AppComponent {
09:   constructor(private commSvc: SwCommunicationService){
10:   }
11: }
Listing 6-3

Import and Inject SwCommunicationService in AppComponent

Identifying an Update to the Application

Next, update SwCommunicationService to identify if an updated version of the application is available. The SwUpdate service provides an observable named available. Subscribe to this observable. It is invoked when the service worker identifies an updated version of the application.

Note

At this point, you have the information that an upgrade is available on the server. You have not downloaded and activated it yet.

Consider Listing 6-4.
1: export class SwCommunicationService {
2:   constructor(private updateSvc: SwUpdate
3:    ){
4:     this.updateSvc.available.subscribe( i => {
5:       console.log('A new version of the application available', i.current, i.available);
6:     });
7:   }
8: }
Listing 6-4

Identify a New Version of the Application

Consider the following explanation:
  • See line 4 for how to use the object available on updateSvc (an object of SwUpdate). It is an observable, which sends values when a new version of the application is available.

  • Line 5 prints current and available objects. Consider the result in Figure 6-1. Notice the version number in the message.

Figure 6-1

Result on the available observable

Note

Listing 6-4 does not initiate a check for a new version. The subscribe callback (line 5) runs when a new version is identified. See the section “Checking for a New Version” to initiate a check for a new version at regular intervals.

Also, the new version is not downloaded and activated yet.

  • Remember the appData field in ngsw-config.json for the application. (Refer to Chapter 4.) The objects current and available include data from appData. In the sample application, we added a single field name that describes changes to the application. The result in Figure 6-1 prints the current and available objects. They are the fields from ngsw-config.json in the current and new versions of the application. See Listing 6-5 for ngsw-config.json.

01: {
02:   "appData": {"name": "New games available and few bug fixes"},
03:   "$schema": "./node_modules/@angular/service-worker/config/schema.json",
04:   "index": "/index.html",
05:   "assetGroups": [
06:     {
07:       // Removed code for brevity. See code sample for the complete file.
42:     }]
43: }
Listing 6-5

ngsw-config.json with appData

As mentioned earlier, the available observer verifies if a new version of the service worker is ready and available. It does not mean it is in use yet. You may prompt the user to update the application. This gives the user a chance to complete the current workflow. For example, in Web Arcade, you do not want to reload and upgrade to a new version while the user is playing a game.

Identifying When an Update Is Activated

So far, you have identified if an upgrade is available. This step allows you to identify an activated upgrade. The activated observable is triggered once a service worker starts serving content from the new version of the application.

Consider Listing 6-6 in SwCommunicationService that uses the activated observable .
01: export class SwCommunicationService {
02:
03:     constructor(private updateSvc: SwUpdate) {
04:       this.updateSvc
05:         .available
06:         .subscribe( i => {
07:             console.log('A new version of the application available', i.current, i.available);
08:       });
09:       this.updateSvc
10:         .activated
11:         .subscribe( i =>
12:             console.log('A new version of the application activated', i.current, i.previous));
13:     }
14:
15: }
Listing 6-6

Activated Observable on SwUpdate

See lines 9 to 13. Notice that you subscribe to the activated observable. It is triggered on activating a new version of the application. See the console.log on line 12. This prints current and previous. It is similar to the current and available objects on the available observable (see Listing 6-4), which is before activation. As the activated observable is triggered after activation, the available version is now the current version. See the result in Figure 6-2.
Figure 6-2

Result on the activated observable

Note

Similar to the available observable, the current and previous objects include appData from ngsw-config.json for the respective versions of the application.

Activating with the SwUpdate Service

When the user opens the application in a new window, service workers check if a new version is available. The service worker might still load the application from the cache. This is because the new version may not have been downloaded and activated yet. It triggers the available event. The subscribe callback on the available observable will be invoked (similar to Listing 6-4). Typically, the next time a user attempts to open the application in a new window, a newer version of the service worker and the application are served. The precise behavior depends on the configuration and a few other factors.

However, you may choose to activate the newer version as soon as you know a newer version is available. The SwUpdate service provides the activateUpdate() API to activate new versions of the application. The function returns a promise<void>. The success callback of the promise is invoked after activating the update. Consider Listing 6-7, which activates the update.

Note

This section does not prompt the user to choose to update the new version yet. As you progress to the next section, you will add the code to alert the user about the availability of a new version of the application.

01: export class SwCommunicationService {
02:
03:     constructor(private updateSvc: SwUpdate) {
04:       this.updateSvc
05:         .available
06:         .subscribe( i => {
07:             console.log('A new version of the application available', i.current, i.available);
08:             this.updateSvc
09:                 .activateUpdate()
10:                 .then( () => {
11:                     console.log("activate update is successful");
12:                     window.location.reload();
13:                 });
14:       });
15:
16:       this.updateSvc
17:         .activated
18:         .subscribe( i => console.log('A new version of the application activated', i.current, i.previous));
19:     }
20:   }
Listing 6-7

Activate Update with the SwUpdate Service

See lines 8 and 13. The activateUpdate() function is called in line 9. It returns a promise. The then() function is called on the returned promise. You provide a callback that is invoked after the promise is resolved. See line 11, which prints a message that the “activate update is successful.”

Notice that activateUpdate() is called when an update is available. The activateUpdate() method is called within the available observable subscription. See lines 6 and 14. This is a subscription callback for the available observable.

Line 12 reloads the application (the browser window) after the update has been activated. It is a good practice to reload the browser on a successful update. In a few instances, routing with lazy loading may break if the window does not reload after the service worker and the cache are refreshed.

Checking for a New Version

The SwUpdates service can check for a new version of the application (service worker configuration) on the server. As mentioned earlier, so far you have received events if a new version was available or activated. You activated the new version if it was available. However, we are dependent on the browser and service worker built-in mechanisms to look for updates.

The function checkForUpdates() can be called on the instance of SwUpdate to look for updates on the server. It will trigger the available observable if a new version is available. The function is especially useful if you anticipate users keeping the application open for a long duration, sometimes days. It is also possible that you anticipate frequent updates and deployments to the application. You may set an interval and regularly check for updates.

Considering checkForUpdates() is used with an interval, it is important you ensure the Angular application is fully bootstrapped and stable before checking for an update. Using this function too often may not allow the Angular application to be stable. Hence, it is a good practice to check if the application is stable and use checkForUpdates, as shown in Listing 6-8. It is added to the SwCommunicationService.
01: import { SwUpdate } from '@angular/service-worker';
02: import { ApplicationRef, Injectable } from '@angular/core';
03: import { first} from 'rxjs/operators';
04: import { interval, concat } from 'rxjs';
05:
06: @Injectable({
07:   providedIn: 'root'
08: })
09: export class SwCommunicationService {
10:
11:   constructor(private updateSvc: SwUpdate,
12:     private appRef: ApplicationRef) {
13:
14:     let isApplicationStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
15:     let isReadyForVersionUpgrade$ = concat( isApplicationStable$, interval (12 * 60 * 60 * 1000)); // //twelve hours in milliseconds
16:     isReadyForVersionUpgrade$.subscribe( () => {
17:       console.log("checking for version upgrade...")
18:       this.updateSvc.checkForUpdate();
19:     });
20:   }
21: }
22:
Listing 6-8

Invoking CheckForUpdates at Regular Intervals

Consider the following explanation:
  • You inject ApplicationRef to verify if the Angular app is stable. Lines 2 and 12 import and inject the class, respectively.

  • Line 13 verifies if the application is stable. The field isStable is of type Observable<boolean>. When subscribed, it returns true when the application is stable. Line 14 assigns the observable to the local variable isApplicationStable$.

  • In line 15, the interval() function returns an Observable<number>. The function accepts a time duration in milliseconds as a parameter. The subscribe callback is invoked after the specified time interval. Notice that the code snippet specifies 12 hours in milliseconds.

  • Line 15 concats isApplicationStable$ with the observable returned by interval(). The resultant observable is set to isReadyForVersionUpgrade$. Subscribe to this observable. The success callback is invoked when the application is stable and the specified interval (12 hours) has passed.

Note

The concat function has now been deprecated. If you are on RxJS version 8, use the concatWith() function to concatenate observables.

  • In line 18, in the subscribe callback for the observable isReadyForVersionUpgrade$, you check for updates using the SwUpdate instance.

  • As a quick recap, you check for upgrades every 12 hours when the application is stable. The checkForUpdate() function may trigger the available subscriber, which calls activateUpdate() that triggers the activate subscriber after successfully activating the new version of the application.

Notifying the User About the New Version

Notice that the current code reloads the application when a new version of the service worker is identified. It does not yet alert the user and allow her to choose when to reload. To provide this option, integrate the application with a Snackbar component. This is a ready-made component available in the Angular Material library. Angular Material provides Material Design implementations for Angular applications.

Why did we choose the Snackbar component? A typical alert blocks the user workflow. Users cannot continue to use the application until an action is taken on the alert. Such a paradigm works well when a workflow cannot continue without the user’s decision. For example, it could be an error scenario, which is important for a user to acknowledge and then make a correction.

On the other hand, when a new version of the service worker (and the application) is available, you do not want to disrupt the current user session. Users may continue to use the current version until the task at hand is complete. For example, if the user is playing a game on Web Arcade, continue until the game is complete. When the user deems it appropriate to reload the window, she may choose to respond to the alert.

A Snackbar component fits well with our scenario. It shows the alert in a corner of the application without interfering with the rest of the page. By default, the alert is shown at the bottom of the page, toward the center. See Figure 6-3. As mentioned earlier, you should allow users to click the button on the Snackbar component to install the new version of the application.
Figure 6-3

A Snackbar component alerting the user that a new version of the application is available

To install the Snackbar component, add Angular Material to the Web Arcade application. Run the following command:
ng add @angular/material
Angular Material is a UI component with other components and stylesheets conforming to Google’s Material Design. To begin with, the CLI prompts you to select a theme. See Figure 6-4.
Figure 6-4

Selecting an Angular Material theme

Next, the CLI does the following:
  1. 1.

    Prompts you to choose Angular Material typography. This is a decision about using Angular Material fonts, default font sizes, etc.

     
  2. 2.

    Prompts you to include browser animations interacting with Angular Material components. Animations provide visual feedback for user actions such as animating the click a button, transitioning a tab of content, etc.

     
See Listing 6-9 for the result.
✔ Package successfully installed.
? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink        [ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? Yes
? Set up browser animations for Angular Material? Yes
UPDATE package.json (1403 bytes)
✔ Packages installed successfully.
UPDATE src/app/app.module.ts (1171 bytes)
UPDATE angular.json (3636 bytes)
UPDATE src/index.html (759 bytes)
UPDATE node_modules/@angular/material/prebuilt-themes/indigo-pink.css (77575 bytes)
Listing 6-9

Result Installing Angular Material

Adding Angular Material to the Web Arcade allows you to use various Angular Material components. You will import and use only the required components. The entire component library does not add to the bundle size.

To use the Snackbar component, import MatSnackBarModule to the AppModule in Web Arcade. Consider Listing 6-10.
01: import { MatSnackBarModule } from '@angular/material/snack-bar';
02:
03: @NgModule({
04:   declarations: [
05:     AppComponent,
06:     // You may have more components
07:   ],
08:   imports: [
09:     BrowserModule,
10:     AppRoutingModule,
11:     MatSnackBarModule,
12:     // You may have additional modules imported
13:   ],
14:   providers: [],
15:   bootstrap: [AppComponent]
16: })
17: export class AppModule { }
Listing 6-10

Add the Snackbar Module to the Web Arcade Application

See the first line, which imports MatSnackBarModule from the Angular Material module for the Snackbar component. Also see line 11 that imports the module to the AppModule on Web Arcade.

Next, import and inject the Snackbar component to the SwCommunication service. Remember, you show an alert to the user when a new version of the application is available. It is identified in the constructor of the SwCommunication service . See Listing 6-11.
01: import { Injectable } from '@angular/core';
02: import { SwUpdate } from '@angular/service-worker';
03: import { MatSnackBar } from '@angular/material/snack-bar';
04:
05: @Injectable({
06:   providedIn: 'root'
07: })
08: export class SwCommunicationService {
10:   constructor(private updateSvc: SwUpdate,
11:     private snackbar: MatSnackBar) {
13:     this.updateSvc.available.subscribe( i => {
14:       let message = i?.available?.appData as { "name": string };
15:       console.log('A new version of the application available', i.current, i.available);
16:       let snackRef = this.snackbar
17:         .open(`A new version of the app available. ${message.name}. Click to install the application`, "Install new version");
18:     });
19:   }
20: }
Listing 6-11

Alert with a Snackbar that a new version of the application is available

Consider the following explanation:
  • Line 3 imports the Snackbar component from Angular Material’s snackbar module. It is injected into the service on line 11.

  • Notice the success callback between lines 14 and 18. It is on the available observable. As mentioned earlier, when an update is ready, this observer function is triggered.

  • See line 16. The open() function shows a Snackbar component. You provide two parameters, the message to show on the alert (Snackbar component) and the title for the action (or the button) on the Snackbar component. Revisit Figure 6-3 to match the code to the result.

  • Notice the open function returns a Snackbar reference object as well. You are using this object to perform an action when the user clicks the button on the Snackbar component.

  • Notice message.name is interpolated on line 17. The message object is obtained on the prior line 14. Notice it is the appData object on ngsw-config.json. This is one way to provide a friendly message for each version upgrade of the application and show the information to the user while she chooses to reload and install the new version. See Figure 6-3. The message from appData says, “we added more games to the arcade.”

Next, use the Snackbar component reference returned by the open() function, as shown in line 16. The reference object is named snackRef. You use the function onAction() on snackRef. This returns another observable. As the name suggests, the observer callback function is triggered when the user performs an action on the Snackbar component. Notice, in the previous code sample, that you have a single action, the button “Install new version” on the Snackbar component. Hence, when this observer is invoked, you know the user clicked the button and can perform the install. See Listing 6-12. With the modified code, you perform the install only after the user chooses to install by clicking the button on the Snackbar component.
01:
02: // include this snippet after snackRef created in the SwCommunicationService
03: snackRef
04: .onAction()
05: .subscribe (() => {
06:   console.log("Snackbar action performed");
07:   this.updateSvc.activateUpdate().then( () => {
08:     console.log("activate update invoked");
09:     window.location.reload();
10:   });
11: });
Listing 6-12

Add Snackbar Module to Web Arcade

Consider the following explanation:
  • Line 4 invokes the onAction() function on the snackRef object. You chain the subscribe() function on the returned object. As mentioned earlier, the onAction() function returns an observable.

  • The success callback provided as an observer invokes the activateUpdate() function on the SwUpdate object. The observer callback between lines 6 and 10 are called when the user performs an action on the Snackbar component.

  • Remember, the code between lines 6 and 10 is the same as Listing 6-6, which performed the install as soon as it identified a new version.

Managing Errors in Unrecoverable Scenarios

It is possible that users have not returned to the application for a while on a machine. Browsers will clear the cache and claim the disk space. When the user returns to the application (after the cache was cleaned), the service worker may not have all the scripts it needs. Imagine, meanwhile, that a new version of the application was deployed on the server. Now, the browser cannot obtain the deleted script (from the cache), not even from the server. This results in an unrecoverable state for the application. To the user, it leaves only one option: to upgrade to the latest version.

The SwUpdate service provides an observable called unrecoverable to handle such scenarios, as shown in Listing 6-13. Add an error handler when an unrecoverable state occurs. Notify the user and reload the browser window to clear the error. Similar to the earlier code samples, add the code in the SwCommunicationService constructor.
01: export class SwCommunicationService {
02:
03:   constructor(private updateSvc: SwUpdate,
04:     private snackbar: MatSnackBar) {
05:     this.updateSvc.unrecoverable.subscribe( i => {
06:       console.log('The application is unrecoverable', i.reason);
07:       let snackRef = this.snackbar
08:       .open(`We identified an error loading the application. Use the following reload button. If the error persists, clear cache and reload the application`,
09:         "Reload");
10:     snackRef
11:       .onAction()
12:       .subscribe (() => {
13:         this.updateSvc.activateUpdate().then( () => {
14:           window.location.reload();
15:         });
16:      });
17:     });
18:   }
19: }
Listing 6-13

Handle the Unrecoverable State

Consider the following explanation:
  • See line 5. It subscribes to the unrecoverable observer on the instance of SwUpdate (namely, updateSvc). The success callback for the observable is between lines 6 and 17.

  • You open a Snackbar component, which alerts the user about the error at the bottom center of the page. See Figure 6-5.
    Figure 6-5

    A Snackbar component alerting the user of the unrecoverable error

  • Notice the reason field on line 6. It has additional information about the unrecoverable state. You may print and use this information in the browser console or log it to a central location to investigate further.

Summary

Installable and cached web applications are powerful. They enable users to access the application in low-bandwidth and offline scenarios. However, you also need to build features to seamlessly upgrade the applications. As you know, for Web Arcade, service workers manage caching and offline access features.

This chapter detailed how to seamlessly upgrade an Angular application and communicate with service workers. It extensively uses Angular’s SwUpdate service, which provides many out-of-the-box features for identifying and activating a new version of the application. It uses observables; you subscribe when new versions of the applications are available or activated.

Exercise
  • The chapter uses a Snackbar component to alert when a new version of the application is available. Show an alert after the upgrade is activated. Include information from appData in ngsw-config.json.

  • Extend ngsw-config.json to show additional information about the release. Include details of enhancements and bug fixes. Take it close to the real-world use case. Users expect to see a summary of details about an upgrade.

  • Explore positioning the Snackbar component at a different location on the page (as opposed to the default center bottom).

  • Explore alerting users with more components other than a Snackbar component. Build an experience that suits a different use case better. Remember, we used the Snackbar component as it does not interrupt and block an active user’s workflow. However, a Snackbar component has been used for alerting the unrecoverable error scenario as well. Hence, choose an appropriate alert component and mechanism for such error conditions.

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

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