import { BehaviorSubject, from, merge, NEVER, Observable, of } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { IProstoreSsrContext } from './IProstoreSsrContext'; import { BehaviorStore } from './BehaviorStore'; import { SubscriptionManager } from './SubscriptionManager'; export interface IRequestState { /** Request result */ data: Data | null; /** Whether the request is ongoing */ loading: boolean; /** Whether the request has been completed at least once */ loaded: boolean; /** If request errored out, holds the error */ error: any; /** Latest variables used for requesting the data */ variables: Vars | null; } export const initialRequestState: IRequestState = { data: null, loading: false, loaded: false, error: null, variables: null }; export type ISkipQueryFn = (vars: Vars) => Data | null | undefined; export type IUpdateDataFn = ( data: Data | null, oldData: Data | null, params: { store: any; variables: Vars; options: any } ) => Data | null; export interface IVariablesSubjectType { variables: Vars; options?: Options; } export interface IRxRequestStoreParams { /** Initial data to be held in the store state as the request result */ initialData?: Data | null; /** * This function may be used to skip the request based on the variable values. * * It should return undefined if the request should go as usual. * If something else is returned, the request is skipped and the result of the function * is saved as the store state. */ skipQuery?: ISkipQueryFn; /** * This function may be used to customize the store state after the request is completed. * * It accepts new request result, old saved data and request params and should return * new data to be saved in the store state. * * Can be used for paginated requests (see example in the docs) */ updateData?: IUpdateDataFn; /** Global SsrContext which contains states of all stores */ ssrContext?: IProstoreSsrContext | null; /** Store identifier which is used to store data in the SsrContext */ ssrId?: string | null; /** Observable which provides variables and options to the request */ variables$?: Observable>; } export interface IRequestStoreOptions { /** Optional state overrides */ changeState?: Partial>; } export type IObservableData = Promise | Observable; export type IVariablesObservable = | Observable> | undefined; export class RxRequestStore< Vars = any, Data = any, Options extends IRequestStoreOptions = any > extends BehaviorStore> { /** Identifies the store in the SsrContext */ public ssrId?: string | null; /** SsrContext to be used in the SSR-enabled environments */ public ssrContext?: IProstoreSsrContext | null; /** Subject which holds the variables Observable */ public variables$$: BehaviorSubject>; /** Observable of the request state */ public state$: BehaviorSubject>; /** Skip query callback. See docs for IRequestStoreParams */ protected skipQuery: ISkipQueryFn; /** Update data callback. See docs for IRequestStoreParams */ protected updateData: IUpdateDataFn; /** Indicates whether the request data was loaded from SsrContext */ protected loadedValueFromContext: boolean = false; /** Tracks all subscriptions during onSubscribe and cancels them in onUnsubscribe */ _sub = new SubscriptionManager(); constructor({ initialData = null, skipQuery = (x) => undefined, ssrId = null, updateData = (data) => data, ssrContext = null, variables$ }: IRxRequestStoreParams) { super({ ...initialRequestState, data: initialData }); this.ssrId = ssrId; this.ssrContext = ssrContext; this.tryLoadContextState(); this.skipQuery = skipQuery; this.updateData = updateData; this.variables$$ = new BehaviorSubject>( variables$ ); } get state() { return this.state$.value; } set state(newValue) { this.state$.next(newValue); } setVariables$(variables$: IVariablesObservable) { this.variables$$.next(variables$); } onSubscribe() { this._sub.subscribe( this.variables$$.pipe(switchMap(this.projectVariables)), this.state$ ); } onUnsubscribe() { this._sub.destroy(); } projectVariables = (variables$: IVariablesObservable) => { if (!variables$) { return NEVER; } return variables$.pipe( switchMap(this.projectRequest), map( (change) => ({ ...this.state, ...change } as IRequestState) ), tap((state) => { this.updateContextState(state); }) ); }; projectRequest = ({ variables, options: optionsProp }: IVariablesSubjectType): Observable< Partial> > => { const options = optionsProp || ({} as Options); const state = { loading: true, variables, error: null, ...options.changeState }; return merge( of(state), from(this.performSkippableRequest(variables, options)).pipe( map(this.mapRequestResult(variables, options)), catchError((error) => of(this.handleError(error))) ) ); }; performSkippableRequest( variables: Vars, options: Options ): IObservableData { const skipped = this.skipQuery(variables); if (skipped !== undefined) { return of(skipped); } return this.performRequest(variables, options); } performRequest( variables: Vars, options: Options ): IObservableData { throw new Error( 'Please override performRequest function in your RxRequestStore extension' ); } mapRequestResult = (variables: Vars, options: Options) => ( data: Data ): IRequestState => ({ data: this.updateData(data, this.state.data, { store: this, variables, options }), loading: false, loaded: true, error: null, variables }); handleError = (error: any): Partial> => { console.error(error); return { error, loading: false }; }; tryLoadContextState() { if (this.ssrContext && this.ssrId) { const ssrState = this.ssrContext.getState(this.ssrId); if (ssrState) { this.state$.next(ssrState); this.loadedValueFromContext = true; } } } updateContextState(state: IRequestState = this.state) { if (this.ssrId && this.ssrContext && this.ssrContext.isServer) { this.ssrContext.setState(this.ssrId, state); } } registerSsrPromise(promise: Promise>) { if (this.ssrId && this.ssrContext && this.ssrContext.isServer) { this.ssrContext.registerPromise(promise); } } } export type GetRxRequestStoreVars = Store extends RxRequestStore< infer Vars, any, any > ? Vars : never; export type GetRxRequestStoreData = Store extends RxRequestStore< any, infer Data, any > ? Data : never; export type GetRxRequestStoreOptions = Store extends RxRequestStore< any, any, infer Options > ? Options : never; export type GetRxRequestStoreState = IRequestState< GetRxRequestStoreVars, GetRxRequestStoreData >;