From f353bd9b50f5031d13e5b1faba7b8bd2aad0b1c1 Mon Sep 17 00:00:00 2001 From: Dan Percic Date: Fri, 17 Dec 2021 15:19:30 +0200 Subject: [PATCH] hide disconnected indicator when request passed --- src/lib/error/error.service.ts | 18 ++++++---- .../full-page-error.component.ts | 6 ++-- src/lib/error/server-error-interceptor.ts | 36 ++++++++++++------- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/lib/error/error.service.ts b/src/lib/error/error.service.ts index 61175e8..3467b95 100644 --- a/src/lib/error/error.service.ts +++ b/src/lib/error/error.service.ts @@ -1,9 +1,10 @@ import { Injectable } from '@angular/core'; -import { BehaviorSubject } from 'rxjs'; +import { Subject } from 'rxjs'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { LoadingService } from '../loading'; -import { filter, mapTo } from 'rxjs/operators'; +import { filter, map, skip } from 'rxjs/operators'; import { NavigationStart, Router } from '@angular/router'; +import { shareLast } from '../utils'; const BANDWIDTH_LIMIT_EXCEEDED = 509 as const; const OFFLINE_STATUSES = [ @@ -17,9 +18,14 @@ const isOffline = (error?: HttpErrorResponse) => !!error && OFFLINE_STATUSES.inc @Injectable({ providedIn: 'root' }) export class ErrorService { - readonly error$ = new BehaviorSubject(undefined); - readonly offlineError$ = this.error$.pipe(filter(isOffline), mapTo(new Event('offline'))); - readonly serverError$ = this.error$.pipe(filter(error => !isOffline(error))); + readonly error$ = new Subject(); + readonly offlineError$ = this.error$.pipe( + skip(1), + filter(error => !error || isOffline(error)), + map(error => new Event(error ? 'offline' : 'online')), + shareLast(), + ); + readonly serverError$ = this.error$.pipe(filter(error => !error || !isOffline(error))); constructor(private readonly _loadingService: LoadingService, private readonly _router: Router) { _router.events.pipe(filter(event => event instanceof NavigationStart)).subscribe(() => { @@ -33,6 +39,6 @@ export class ErrorService { } clear(): void { - this.error$.next(undefined); + this.error$.next(); } } diff --git a/src/lib/error/full-page-error/full-page-error.component.ts b/src/lib/error/full-page-error/full-page-error.component.ts index 4450101..b123ad1 100644 --- a/src/lib/error/full-page-error/full-page-error.component.ts +++ b/src/lib/error/full-page-error/full-page-error.component.ts @@ -3,7 +3,7 @@ import { IconButtonTypes } from '../../buttons'; import { ErrorService } from '../error.service'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { fromEvent, merge, Observable } from 'rxjs'; -import { delay, map, mapTo } from 'rxjs/operators'; +import { delay, filter, map, mapTo } from 'rxjs/operators'; import { shareLast } from '../../utils'; import { connectionStatusTranslations } from '../../translations/connection-status-translations'; @@ -29,7 +29,9 @@ export class FullPageErrorComponent { constructor(readonly errorService: ErrorService) { const offline$ = merge(fromEvent(window, 'offline'), this.errorService.offlineError$); const online$ = fromEvent(window, 'online').pipe(shareLast()); - const removeIndicator$ = online$.pipe(delay(3000), mapTo(undefined)); + + const errorGone$ = this.errorService.offlineError$.pipe(filter(error => !error)); + const removeIndicator$ = merge(online$, errorGone$).pipe(delay(3000), mapTo(undefined)); this.connectionStatus$ = merge(online$, offline$, removeIndicator$).pipe(map(event => event?.type)); } diff --git a/src/lib/error/server-error-interceptor.ts b/src/lib/error/server-error-interceptor.ts index df43788..8d9976c 100644 --- a/src/lib/error/server-error-interceptor.ts +++ b/src/lib/error/server-error-interceptor.ts @@ -1,6 +1,6 @@ import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpStatusCode } from '@angular/common/http'; import { Inject, Injectable, Optional } from '@angular/core'; -import { MonoTypeOperatorFunction, Observable, throwError, timer } from 'rxjs'; +import { MonoTypeOperatorFunction, Observable, pipe, throwError, timer } from 'rxjs'; import { catchError, mergeMap, retryWhen, tap } from 'rxjs/operators'; import { MAX_RETRIES_ON_SERVER_ERROR } from './max-retries.token'; import { ErrorService } from './error.service'; @@ -16,24 +16,25 @@ function updateSeconds(seconds: number) { function backoffOnServerError(maxRetries = 3): MonoTypeOperatorFunction> { let seconds = 0; - return retryWhen(attempts => - attempts.pipe( - tap(() => (seconds = updateSeconds(seconds))), - mergeMap((error: HttpErrorResponse, index) => { - if ((error.status < HttpStatusCode.InternalServerError && error.status !== 0) || index === maxRetries) { - return throwError(error); - } + const timerExpiration = pipe( + tap(() => (seconds = updateSeconds(seconds))), + mergeMap((error: HttpErrorResponse, index) => { + if ((error.status < HttpStatusCode.InternalServerError && error.status !== 0) || index === maxRetries) { + return throwError(error); + } - console.error('An error occurred: ', error); - console.error(`Retrying in ${seconds} seconds...`); - return timer(seconds * 1000); - }), - ), + console.error('An error occurred: ', error); + console.error(`Retrying in ${seconds} seconds...`); + return timer(seconds * 1000); + }), ); + return retryWhen(errors => errors.pipe(timerExpiration)); } @Injectable() export class ServerErrorInterceptor implements HttpInterceptor { + private readonly _urlsWithError = new Set(); + constructor( private readonly _errorService: ErrorService, private readonly _keycloakService: KeycloakService, @@ -47,13 +48,22 @@ export class ServerErrorInterceptor implements HttpInterceptor { if (error.status === HttpStatusCode.Unauthorized) { this._keycloakService.logout(); } + // server error if (error.status >= HttpStatusCode.InternalServerError) { this._errorService.set(error); + this._urlsWithError.add(req.url); } + return throwError(error); }), backoffOnServerError(this._maxRetries || 3), + tap(() => { + if (this._urlsWithError.has(req.url)) { + this._errorService.clear(); + this._urlsWithError.delete(req.url); + } + }), ); } }