128 lines
4.5 KiB
TypeScript
128 lines
4.5 KiB
TypeScript
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<Event>;
|
|
readonly online$: Observable<Event>;
|
|
readonly connectionStatus$: Observable<string | undefined>;
|
|
readonly #error$ = new Subject<ErrorType>();
|
|
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<string | undefined>();
|
|
// eslint-disable-next-line no-undef
|
|
#notificationTimeout: Record<string, NodeJS.Timeout | undefined> = {};
|
|
#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(),
|
|
);
|
|
}
|
|
}
|