All files service-worker.service.ts

2.7% Statements 1/37
0% Branches 0/6
0% Functions 0/13
2.85% Lines 1/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121                1x                                                                                                                                                                                                                                
import { ApplicationRef, DOCUMENT, inject, Injectable, NgZone } from '@angular/core';
import { MatSnackBar, MatSnackBarConfig, MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar';
import { SwUpdate } from '@angular/service-worker';
import { catchError, combineLatest, concat, defer, filter, first, from, interval, map, of, skip, switchMap, tap } from 'rxjs';
 
@Injectable({
  providedIn: 'root',
})
export class AppServiceWorkerService {
  private readonly appRef = inject(ApplicationRef);
 
  private readonly service = inject(SwUpdate);
 
  private readonly snackBar = inject(MatSnackBar);
 
  private readonly zone = inject(NgZone);
 
  private readonly document = inject(DOCUMENT);
 
  /**
   * Application state stream.
   */
  private readonly appStable$ = this.appRef.isStable.pipe(first(isStable => isStable));
 
  /**
   * Service worker update interval.
   */
  private get updateInterval() {
    const hours = 6;
    const minutes = 60;
    const seconds = 60;
    const miliseconds = 1000;
    return hours * minutes * seconds * miliseconds;
  }
 
  /**
   * Service worker update interval stream.
   */
  private readonly updateInterval$ = interval(this.updateInterval);
 
  /**
   * Application version update stream.
   * Streams version updates and displays messages in the UI.
   */
  private readonly versionUpdate$ = this.service.versionUpdates.pipe(
    map(event => {
      switch (event.type) {
        case 'VERSION_DETECTED':
          this.displaySnackBar(`Downloading new app version: ${event.version.hash}`);
          return null;
        case 'VERSION_INSTALLATION_FAILED':
          this.displaySnackBar(`Failed to install app version '${event.version.hash}': ${event.error}`);
          return null;
        case 'VERSION_READY':
          this.displaySnackBar(`New app version ready for use: ${event.latestVersion.hash}`);
          return event;
        default:
          return null;
      }
    }),
  );
 
  /**
   * Application update check stream.
   * Checks for available update and prompts the user to update the application.
   */
  private readonly checkForUpdates$ = concat(this.appStable$, this.updateInterval$).pipe(
    switchMap(() => from(defer(() => this.service.checkForUpdate()))),
    catchError((error: Error) => {
      this.displaySnackBar(`Failed to check for updates. ${error.message}`, void 0);
      return of(false);
    }),
    filter(update => update),
    skip(1),
    tap(() => {
      this.displaySnackBar(
        'There is an application update available. It is recommended to update to avoid unexpected behavior.',
        'Update',
        { duration: Number(Infinity) },
        true,
      );
    }),
  );
 
  /**
   * Should be used in the application root component.
   */
  public readonly subscribeToUpdates$ = combineLatest([this.versionUpdate$, this.checkForUpdates$]);
 
  /**
   * Displays a snackbar.
   * @param message message to display
   * @param action snackbar action
   * @param config snackbar config
   * @param refreshPage whether to refresh the page on snackbar action
   */
  private displaySnackBar(message: string, action?: string, config?: MatSnackBarConfig, refreshPage?: boolean): void {
    this.zone.run(() => {
      const bar = this.snackBar.open(message, action, config);
      if (refreshPage === true) {
        this.refreshPage(bar);
      }
    });
  }
 
  /**
   * Refreshes the page on snackbar action.
   * @param ref snackbar reference
   */
  private refreshPage(ref: MatSnackBarRef<TextOnlySnackBar>): void {
    void ref
      .onAction()
      .pipe(
        tap(() => {
          this.document.location.reload();
        }),
      )
      .subscribe();
  }
}