feat(callstatelib): add a callstate publishable lib

This commit is contained in:
thomas
2022-12-07 16:03:05 +01:00
parent 278e513538
commit 1ec45ce141
19 changed files with 1477 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
import { CustomError, ErrorHandler } from './external.model';
export type LoadingState = 'INIT' | 'LOADING' | 'LOADED';
export const UNKNOWN_ERROR_CAUSE = 'UNKNOWN_ERROR';
export const UNKNOWN_ERROR_MESSAGE = 'unknown error occured';
export class CallStateError {
name: string;
message: string;
stack?: string;
constructor(name = '', message = '') {
this.name = name;
this.message = message;
}
}
export class CallStateErrorHandler implements ErrorHandler<CallStateError> {
toError = (error: unknown): CallStateError => {
if (error instanceof CallStateError) return error;
if (error instanceof Error)
return new CallStateError(error.name, error.message);
return new CallStateError(UNKNOWN_ERROR_CAUSE, UNKNOWN_ERROR_MESSAGE);
};
getErrorMessage = (error?: CallStateError): string | undefined => {
return error?.message;
};
}
export interface EsuiteError {
code: string;
}
export interface ErrorState {
error: CustomError;
}
export type CallState = LoadingState | ErrorState;
export const getErrorCallState = (
callState: CallState
): CustomError | undefined => {
if (isErrorState(callState)) {
return callState.error;
}
return undefined;
};
export const isLoadedOrInError = (callState: CallState): boolean =>
callState === 'LOADED' || isErrorState(callState);
export const isErrorState = (callState: CallState): callState is ErrorState =>
Object.prototype.hasOwnProperty.call(callState, 'error');

View File

@@ -0,0 +1,92 @@
/* eslint-disable @typescript-eslint/ban-types */
import { inject, Inject, Injectable, InjectionToken } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Observable, of, switchMap } from 'rxjs';
import {
CallState,
CallStateError,
getErrorCallState,
} from './call-state.model';
import { ERROR_TOKEN } from './external.model';
import { nonFlickerLoader } from './non-flicker-loader';
export const INITIAL_TOKEN = new InjectionToken('initial data');
export interface CallStateComponentState {
callState: CallState;
}
@Injectable()
export class CallStateComponentStore<
U extends object | void = void,
T extends
| (U & CallStateComponentState)
| CallStateComponentState = U extends void
? CallStateComponentState
: U & CallStateComponentState
> extends ComponentStore<T> {
private error = inject(ERROR_TOKEN);
constructor(@Inject(INITIAL_TOKEN) initialState: U) {
super({ callState: 'INIT', ...initialState } as T);
}
readonly isLoading$: Observable<boolean> = this.select(
(state) => state.callState === 'LOADING'
);
readonly isLoadingWithFlicker$: Observable<boolean> = this.select(
(state) => state.callState === 'LOADING'
).pipe(switchMap((loading) => nonFlickerLoader(of(loading))));
readonly isLoaded$: Observable<boolean> = this.select(
(state) => state.callState === 'LOADED'
).pipe(switchMap((loaded) => of(loaded)));
readonly callState$ = this.select((state) => state.callState);
readonly error$: Observable<string | undefined> = this.select((state) =>
this.error.getErrorMessage(getErrorCallState(state.callState))
);
readonly updateCallState = this.updater(
(state, callState: CallState | undefined): T => {
return {
...(state as object),
callState: callState ?? 'LOADED',
} as T;
}
);
readonly startLoading = this.updater(
(state, patchedState: Partial<U> | void): T => {
return {
...(state as object),
...patchedState,
callState: 'LOADING',
} as T;
}
);
readonly stopLoading = this.updater(
(state, patchedState: Partial<U> | void): T => {
return {
...(state as object),
...patchedState,
callState: 'LOADED',
} as T;
}
);
protected handleError(
error: unknown,
patchedState: Partial<U> = {}
): CallStateError {
const err = this.error.toError(error);
this.patchState({
callState: { error: err },
...patchedState,
} as Partial<T>);
return err;
}
}

View File

@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
import { ClassProvider, InjectionToken, Type } from '@angular/core';
import { CallStateErrorHandler } from './call-state.model';
export interface ErrorHandler<T extends CustomError> {
toError: (error: unknown) => T;
getErrorMessage: (error?: T) => string | undefined;
}
export interface CustomError {}
export const ERROR_TOKEN = new InjectionToken<ErrorHandler<any>>('error', {
factory: () => new CallStateErrorHandler(),
});
export const provideErrorHandler = <T extends ErrorHandler<any>>(
errorHandlerClass: Type<T>
): ClassProvider => ({ provide: ERROR_TOKEN, useClass: errorHandlerClass });

View File

@@ -0,0 +1,20 @@
import { combineLatest, map, mapTo, Observable, startWith, timer } from 'rxjs';
/**
* Delay the first emition of data$ value. Instead, it emits "true" until duration is elapsed
*/
export const nonFlickerLoader = (
data$: Observable<boolean>,
duration = 300
): Observable<boolean> => {
const isTrueWhileDuration$ = timer(duration).pipe(
mapTo(false),
startWith(true)
);
return combineLatest([data$, isTrueWhileDuration$]).pipe(
map(([data, isTrueWhileDuration]) =>
isTrueWhileDuration ? isTrueWhileDuration : data
)
);
};