import { inject, Injectable } from '@angular/core'; import { fromEvent, merge, Observable, Subject } from 'rxjs'; import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; import { LoadingService } from '../loading'; import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators'; import { NavigationStart, Router } from '@angular/router'; import { shareLast } from '../utils'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export class CustomError { readonly label: string; readonly actionLabel: string; readonly actionIcon: string; readonly message?: string; readonly action?: () => void; constructor(label: string, actionLabel: string, actionIcon: string, message?: string, action?: () => void) { this.label = label; this.actionLabel = actionLabel; this.actionIcon = actionIcon; this.message = message; this.action = action; } } export type ErrorType = HttpErrorResponse | CustomError | undefined; const BANDWIDTH_LIMIT_EXCEEDED = 509 as const; const OFFLINE_STATUSES = [ HttpStatusCode.BadGateway, HttpStatusCode.ServiceUnavailable, HttpStatusCode.GatewayTimeout, BANDWIDTH_LIMIT_EXCEEDED, ] as const; const isOffline = (error?: ErrorType) => error instanceof HttpErrorResponse && OFFLINE_STATUSES.includes(error.status); const isSameEventType = (previous: Event | string | undefined, current: Event | string | undefined) => previous instanceof Event && current instanceof Event ? previous.type === current.type : false; @Injectable({ providedIn: 'root' }) export class ErrorService { readonly offline$: Observable; readonly online$: Observable; readonly connectionStatus$: Observable; readonly #error$ = new Subject(); readonly error$ = this.#error$.pipe(filter(error => !error || !isOffline(error))); readonly #online$ = new Subject(); readonly #loadingService = inject(LoadingService); readonly #router = inject(Router); readonly #displayNotification$ = new Subject(); // eslint-disable-next-line no-undef #notificationTimeout: Record = {}; #displayedNotificationType: string | undefined; constructor() { this.#router.events .pipe( filter(event => event instanceof NavigationStart), tap(() => this.clear()), takeUntilDestroyed(), ) // eslint-disable-next-line rxjs/no-ignored-subscription .subscribe(); this.offline$ = this.#offline(); this.online$ = this.#online(); this.connectionStatus$ = merge(this.online$, this.offline$, this.#displayNotification$).pipe( distinctUntilChanged(isSameEventType), filter(value => { if (!(value instanceof Event)) return true; this.#clearNotificationTimeouts(); this.#setNotificationTimeout(value.type); return false; }), map(value => value as string | undefined), tap(value => (this.#displayedNotificationType = value)), ); } set(error: ErrorType): void { this.#loadingService.stop(); this.#error$.next(error); } setOnline(): void { this.#online$.next(true); } clear(): void { this.#error$.next(undefined); } #clearNotificationTimeouts() { Object.keys(this.#notificationTimeout).forEach(key => { clearTimeout(this.#notificationTimeout[key]); this.#notificationTimeout[key] = undefined; }); } #setNotificationTimeout(status: string) { if (status === 'online' && this.#displayedNotificationType !== 'offline') return; this.#notificationTimeout[status] ??= setTimeout(() => { this.#displayNotification$.next(status); if (status === 'online') { setTimeout(() => this.#displayNotification$.next(undefined), 3000); } }, 3000); } #offline() { return merge( fromEvent(window, 'offline'), this.#error$.pipe( filter(isOffline), map(() => new Event('offline')), shareLast(), ), ); } #online() { return merge(fromEvent(window, 'online'), this.#online$.pipe(map(v => (v instanceof Event ? v : new Event('online'))))).pipe( shareLast(), ); } }