ngrx-loading-state
Version:
NgRx Loading State consistently manages loading actions such as API fetches.
1 lines • 65.8 kB
Source Map (JSON)
{"version":3,"file":"ngrx-loading-state.mjs","sources":["../../../projects/ngrx-loading-state/src/lib/lodash/lodash.functions.ts","../../../projects/ngrx-loading-state/src/lib/loading-state/loading-state-types.ts","../../../projects/ngrx-loading-state/src/lib/loading-state/loading-state-functions.ts","../../../projects/ngrx-loading-state/src/lib/functions.ts","../../../projects/ngrx-loading-state/src/lib/global-failure-reducer.ts","../../../projects/ngrx-loading-state/src/lib/id-loading-state/id-loading-state-actions.ts","../../../projects/ngrx-loading-state/src/lib/id-loading-state/id-loading-state-creators.ts","../../../projects/ngrx-loading-state/src/lib/loading-state/loading-state-actions.ts","../../../projects/ngrx-loading-state/src/lib/loading-state/loading-state-creators.ts","../../../projects/ngrx-loading-state/src/lib/loading-state/loading-state-effects.ts","../../../projects/ngrx-loading-state/src/public-api.ts","../../../projects/ngrx-loading-state/src/ngrx-loading-state.ts"],"sourcesContent":["// This file is in the \"lr-lodash\" directory so that we can use the barrel file\n// to create the lodash namespace. If we put this into the \"functions\" folder, then\n// other functions in there that uses lodash won't have the lodash namespace.\n// The internet says this style of import improves bundle size. At least we only have to\n// do this in a single place here.\nexport { isArray, isEqual, isPlainObject, pick } from 'lodash';\n","import { ActionCreator, Creator, NotAllowedCheck, ReducerTypes } from '@ngrx/store';\nimport { TypedAction } from '@ngrx/store/src/models';\n\n/**\n * Using this constant for the maxAge parameter will issue an API load if there is no\n * on going API call, irrespective of the current age of the previous load action.\n */\nexport const MAX_AGE_LATEST = 0;\n\n/**\n * These are used by the global error reducer to keep track of whether error events have\n * already been handled\n */\nexport enum FailureHandlerState {\n INIT = 'INIT',\n GLOBAL = 'GLOBAL',\n LOCAL = 'LOCAL'\n}\n\n// TODO Make this generic\nexport type LoadingStateError = any;\n\n/**\n * String constant for dynamic type checking.\n *\n * Alternatively, we could use a class instead of interfaces.\n *\n */\nexport interface LoadingState {\n /** For dynamic type checking */\n isLoadingState: true;\n /** API is loading */\n loading: boolean;\n /** Api returned successfully */\n success: boolean;\n /** True if we should issue an API call. Mostly for internal use */\n issueFetch: boolean;\n /** Tells the global error reducer how to handle this error. */\n failureHandlerState: FailureHandlerState;\n /** Millisecond unix timestamp of when data is loaded */\n successTimestamp?: number;\n /** Last error returned by the API */\n error?: LoadingStateError; // Api returned error\n}\n\nexport interface LoadAction {\n /**\n * If there is existing data and the time since it was loaded does not exceed\n * the maxAge in milliseconds, then we can consider the value in the store valid.\n * The loadingState.issueFetch will be set to false in this case.\n *\n * If this parameter is not provided, then an API call is issued immediately even\n * if there is already an API in progress.\n */\n maxAge?: number;\n /**\n * If true, any loading errors will not generate global error notifications.\n * Set this to true if your component wants to handle the error.\n * If there are multiple calls to dispatch load action, and if any of the\n * actions has localError == true, then the next failure action will be handled\n * locally instead of showing a global error.\n */\n localError?: boolean;\n}\n\nexport interface FailureAction {\n /**\n * Error from the API\n */\n error?: LoadingStateError;\n}\n\n/**\n * Typing for the set of reducers created as a part of creating a loading action.\n */\nexport type LoadingActionsReducerTypes<State> = ReducerTypes<\n State,\n ActionCreator<string, Creator<any[], object>>[]\n>;\n\n/**\n * Key is the 'type' of the action. eg. \"Fetch users\", \"Create Booking\"\n */\nexport interface LoadingStates {\n [key: string]: LoadingState;\n}\n\n/**\n * Store state need to implement this to hold the loading states.\n * eg.\n *\n * export type SimpleState = WithLoadingStates & {\n * count: number;\n * profile: string;\n * ...\n * };\n *\n */\nexport interface WithLoadingStatesOnly {\n loadingStates: LoadingStates;\n}\n\n/**\n * Merging multiple loading states together.\n */\nexport type CombinedLoadingState = Pick<LoadingState, 'loading' | 'success' | 'error'>;\n\n// ----------------------------------------------------------------\n// Internal use\n// ----------------------------------------------------------------\nexport type OnState<State> = State extends infer S ? S : never;\nexport type ActionFactoryResult<T extends object> = ActionCreator<\n string,\n (props: T & NotAllowedCheck<T>) => T & TypedAction<string>\n>;\n","import { ActionCreatorProps, createAction, NotAllowedCheck, props } from '@ngrx/store';\nimport { Action } from '@ngrx/store/src/models';\nimport { lodash } from '../lodash';\nimport {\n ActionFactoryResult,\n CombinedLoadingState,\n FailureAction,\n FailureHandlerState,\n LoadAction,\n LoadingState\n} from './loading-state-types';\n\n/**\n * See if a new API fetch should be issued.\n *\n * @param currentState The current loading state.\n * @param action The the LoadAction dispatched by the user.\n * @returns True if a new API fetch should be issued.\n */\nexport function shouldIssueFetch(\n currentState: Readonly<LoadingState>,\n action: Readonly<LoadAction>\n): boolean {\n if (currentState == null) {\n return true;\n }\n\n const maxAge = action?.maxAge;\n\n // If action not given, then we err on the side of caution and always do a reload, even if\n // an API fetch is already happening.\n if (maxAge == null) {\n return true;\n } else {\n // Check if data not loaded or if too old.\n const reload =\n !currentState.successTimestamp || Date.now() - currentState.successTimestamp >= maxAge;\n\n // Do not issue duplicate loads if a fetch is already in progress\n return reload && !currentState.loading;\n }\n}\n\n/**\n * Return the type of error handler that should be used.\n *\n * @param currentState the current state\n * @param action the load action dispatched by the user\n * @param issueFetch whether the load action should issue a new API call.\n * @returns\n */\nexport function getFailureHandlerState(\n currentState: Readonly<LoadingState>,\n action: Readonly<LoadAction>,\n issueFetch: boolean\n): FailureHandlerState {\n if (currentState == null) {\n // If currentState does not exist, then errorHandlerState can be assumed to be initialised to ErrorHandlerState.INIT\n if (action.localError) {\n return FailureHandlerState.LOCAL;\n } else {\n return FailureHandlerState.GLOBAL;\n }\n }\n\n // If loading or issuing API fetch, then there is guaranteed to be a success/failure\n // action that the global error handler might need to handle.\n if (currentState.loading || issueFetch) {\n // If any load action sets the localError to true, then it disables the global error hander\n // until the success/failure action is handled.\n if (action.localError) {\n return FailureHandlerState.LOCAL;\n } else if (currentState.failureHandlerState == FailureHandlerState.INIT) {\n // If it's in the INIT state, then we use the default global handler because the\n // loading action has not requests for localError handler.\n return FailureHandlerState.GLOBAL;\n }\n // else just fall through and return the existing errorHandler unchanged.\n }\n\n return currentState.failureHandlerState;\n}\n\n/**\n * This function make it easier to define the type of prop<T>. Internal use only.\n *\n * T extends object meets the condition of props function\n *\n * ref: https://stackoverflow.com/questions/65888508/how-to-use-generic-type-in-ngrx-createaction-props\n *\n * @param type String type of the action\n * @returns An action creator function\n */\nexport function actionFactory<T extends object>(type: string): ActionFactoryResult<T> {\n // Restricting config type to match createAction requirements\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return createAction(type, props<any>() as ActionCreatorProps<T> & NotAllowedCheck<T>);\n}\n\n/**\n * Make a clone of any object that implements the LoadingState interface, but copy over only\n * those fields that are in the LoadingState interface.\n *\n * @param src Any object that implements the LoadingState interface\n * @returns A copy of src with only fields from LoadingState\n */\nexport function cloneLoadingState(src: LoadingState): LoadingState {\n // Since the \"src\" could contain extra fields, we want to make sure we exclude other fields in the\n // comparison. Using Required<> to make sure we don't miss any fields.\n // Would be better if we can iterate all the fields in the LoadingStateBase interface. But unless we change\n // LoadingStateBase to a class, it doesn't seem likely. Below is verbose, but at least it should capture all\n // the fields.\n\n if (src == null) {\n return src;\n } else {\n // Using Required<> to ensure we don't miss any fields from LoadingStateBase.\n const ret: Required<LoadingState> = lodash.pick(src as Required<LoadingState>, [\n 'isLoadingState',\n 'loading',\n 'success',\n 'issueFetch',\n 'failureHandlerState',\n 'successTimestamp',\n 'error'\n ]);\n\n return ret;\n }\n}\n\n/**\n * Combine the currentState and the user dispatched LoadAction into newState.\n *\n * @param action User dispatched LoadAction\n * @param currentState Current store state\n * @returns A new state if the state should change, null otherwise.\n */\nexport function getNewLoadState(\n action: LoadAction & Action,\n currentState: LoadingState\n): Readonly<LoadingState> | null {\n const issueFetch = shouldIssueFetch(currentState, action);\n\n const failureHandlerState = getFailureHandlerState(currentState, action, issueFetch);\n\n const newState: LoadingState = issueFetch\n ? {\n isLoadingState: true,\n loading: true,\n success: false,\n issueFetch,\n failureHandlerState,\n successTimestamp: currentState?.successTimestamp,\n error: undefined\n }\n : {\n isLoadingState: true,\n // Deliberately avoiding the use of the spread operator, i.e. no ...currentState\n // because we want to be 100% explicit about the states we are setting. Using ...currentState\n // makes it difficult to read. Being explicit means we need to specify all fields\n // from LoadingState. If we ever add more states to LoadingState the typing will catch any\n // missing states. There's also just the loading(), success(), failure() functions\n // so not too cumbersome to be explicit.\n loading: currentState.loading,\n success: currentState.success,\n issueFetch,\n failureHandlerState,\n successTimestamp: currentState.successTimestamp,\n error: currentState.error\n };\n\n // Note that even if issueFetch is false, the errorHandlerState could still change. So we should\n // compare every field from old to new state.\n return lodash.isEqual(currentState, newState) ? null : newState;\n}\n\n/**\n * @returns Always returns a new success state.\n */\nexport function getNewSuccessState(): Readonly<LoadingState> {\n const ret: LoadingState = {\n isLoadingState: true,\n loading: false,\n success: true,\n issueFetch: false,\n // Each load action will set this again, so here we just set it back to default.\n failureHandlerState: FailureHandlerState.INIT,\n // Since this time field changes, all SuccessAction will cause a state update.\n successTimestamp: Date.now(),\n error: undefined\n };\n\n return ret;\n}\n\n/**\n * Returns a new FailureState.\n *\n * @param action User dispatched FailureAction\n * @param currentState Current loading state\n * @returns A new loading state.\n */\nexport function getNewFailureState(\n action: FailureAction & Action,\n currentState: LoadingState\n): Readonly<LoadingState> {\n return {\n isLoadingState: true,\n loading: false,\n success: false,\n issueFetch: false,\n // Leading this as is for the global failure handler to check.\n failureHandlerState: currentState.failureHandlerState,\n successTimestamp: currentState.successTimestamp,\n error: action.error\n };\n}\n\nexport function combineLoadingStates(loadingStates: LoadingState[]): CombinedLoadingState {\n return {\n loading: loadingStates.some((state) => !!state?.loading),\n success: loadingStates.every((state) => !!state?.success),\n error: loadingStates.find((state) => !!state?.error)?.error\n };\n}\n","import { combineLatest, map, Observable } from 'rxjs';\nimport { combineLoadingStates } from './loading-state/loading-state-functions';\nimport { CombinedLoadingState, LoadingState } from './loading-state/loading-state-types';\nimport { WithLoadingStates } from './types';\n\nexport function getInitialState(): WithLoadingStates {\n return {\n loadingStates: {},\n idLoadingStates: {}\n };\n}\n\nexport function combineLatestLoadingStates(\n loadingStates$: Observable<LoadingState>[]\n): Observable<CombinedLoadingState> {\n return combineLatest(loadingStates$).pipe(\n map((loadingStates) => {\n return combineLoadingStates(loadingStates);\n })\n );\n}\n","import { ActionReducer, MetaReducer } from '@ngrx/store';\nimport {\n FailureAction,\n FailureHandlerState,\n LoadingState\n} from './loading-state/loading-state-types';\nimport { lodash } from './lodash';\n\nexport type FailureHandler = (failureAction: FailureAction, state: LoadingState) => void;\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction instanceOfLoadingState(state: any): state is LoadingState {\n return (state as LoadingState)?.isLoadingState;\n}\n\n/**\n * Recursively process all fields of the states. When it encounters a LoadingStateBase object, it\n * will check if there are failures and needs global failure handling. If global handling is needed,\n * it shows a snackbar message, and changes the failureHandler to FailureHandlerState.INIT as a way to\n * mark the failure as having been processed.\n *\n */\nfunction processState(options: {\n state: unknown;\n failureAction: FailureAction;\n failureHandler: FailureHandler;\n}): void {\n const { state, failureAction, failureHandler } = options;\n\n if (instanceOfLoadingState(state)) {\n if (state.error && state.failureHandlerState == FailureHandlerState.GLOBAL) {\n // Passing back new reference from this reducer is not unnecessary since for each reducer pass\n // all unhandled global failures are handled and failureHandlerState set to FailureHandlerState.INIT to\n // mark it as having been processed.\n // No effects can fire in between the change from GLOBAL to INIT. And since\n // the loadingState must have changed in response to a failure, it's always a new reference\n // compared to the old state.\n state.failureHandlerState = FailureHandlerState.INIT;\n\n failureHandler(failureAction, state);\n }\n } else {\n if (lodash.isArray(state)) {\n // Recursively handle all sub fields. This includes LoadingStateTypes.ID_LOADING_STATE\n // As per previous comment, we are not creating new state references so editing in-place.\n state.forEach((field) =>\n processState({\n ...options,\n state: field\n })\n );\n } else if (lodash.isPlainObject(state)) {\n Object.values(state as object).forEach((field) =>\n processState({\n ...options,\n state: field\n })\n );\n }\n }\n}\n\nexport function globalFailureReducerFactory(failureHandler: FailureHandler): MetaReducer {\n return (reducer: ActionReducer<any, any>): ActionReducer<any, any> => {\n return (state: any, action: any): any => {\n state = reducer(state, action);\n\n const failureAction = action as unknown as FailureAction;\n\n // We must ensure that only FailureAction has this field. But we can\n // easily change this field name to something more unique.\n if (failureAction.error) {\n processState({ state, failureHandler: failureHandler, failureAction });\n }\n\n return state;\n };\n };\n}\n","import { Actions, createEffect, ofType } from '@ngrx/effects';\nimport { Action, createSelector, DefaultProjectorFn, MemoizedSelector, on } from '@ngrx/store';\nimport { TypedAction } from '@ngrx/store/src/models';\nimport { catchError, groupBy, mergeMap, Observable, of, pipe, UnaryFunction } from 'rxjs';\nimport {\n cloneLoadingState,\n combineLoadingStates,\n getNewFailureState,\n getNewLoadState,\n getNewSuccessState\n} from '../loading-state/loading-state-functions';\nimport {\n ActionFactoryResult,\n LoadingActionsReducerTypes,\n LoadingState,\n OnState\n} from '../loading-state/loading-state-types';\nimport {\n Id,\n IdFailureAction,\n IdLoadAction,\n IdLoadingState,\n IdLoadingStateMap,\n IdLoadingStates,\n IdSuccessAction,\n WithIdLoadingStatesOnly\n} from './id-loading-state-types';\n\n/**\n * IdLoadingAction is similar to LoadingAction with the difference that it's parameterized on a user provided ID.\n *\n */\nexport class IdLoadingActions<\n LoadPayloadType extends object,\n SuccessPayloadType extends object,\n FailurePayloadType extends object\n> {\n readonly idLoad: ActionFactoryResult<IdLoadAction & LoadPayloadType>;\n readonly idSuccess: ActionFactoryResult<IdSuccessAction & SuccessPayloadType>;\n readonly idFailure: ActionFactoryResult<IdFailureAction & FailurePayloadType>;\n\n constructor(options: {\n idLoad: ActionFactoryResult<IdLoadAction & LoadPayloadType>;\n idSuccess: ActionFactoryResult<IdSuccessAction & SuccessPayloadType>;\n idFailure: ActionFactoryResult<IdFailureAction & FailurePayloadType>;\n }) {\n // Could have used createActionGroup() but the string literal typing of Source is giving me trouble. For now,\n // just separate types.\n this.idLoad = options.idLoad;\n this.idSuccess = options.idSuccess;\n this.idFailure = options.idFailure;\n }\n\n // ----------------------------------------------------------------------------\n // Typing\n // ----------------------------------------------------------------------------\n instanceOfIdLoad(\n action: Action\n ): action is ReturnType<ActionFactoryResult<IdLoadAction & LoadPayloadType>> {\n return action.type === this.idLoad.type;\n }\n\n instanceOfIdSuccess(\n action: Action\n ): action is ReturnType<ActionFactoryResult<IdSuccessAction & SuccessPayloadType>> {\n return action.type === this.idSuccess.type;\n }\n\n instanceOfIdFailure(\n action: Action\n ): action is ReturnType<ActionFactoryResult<IdFailureAction & FailurePayloadType>> {\n return action.type === this.idFailure.type;\n }\n\n // ------------------------------------------------------------------------------------------------\n // Reducer\n // ------------------------------------------------------------------------------------------------\n reducer<State extends WithIdLoadingStatesOnly>(options?: {\n onLoad?: (\n state: OnState<State>,\n action: IdLoadAction & LoadPayloadType & TypedAction<string>\n ) => State;\n onSuccess?: (\n state: OnState<State>,\n action: IdSuccessAction & SuccessPayloadType & TypedAction<string>\n ) => State;\n onFailure?: (\n state: OnState<State>,\n action: IdFailureAction & FailurePayloadType & TypedAction<string>\n ) => State;\n }): [\n LoadingActionsReducerTypes<State>,\n LoadingActionsReducerTypes<State>,\n LoadingActionsReducerTypes<State>\n ] {\n const { onLoad, onSuccess, onFailure } = options || {};\n return [\n on(this.idLoad, (state, action) => {\n // Reducer must always create a new copy of the state.\n const newState = {\n ...state,\n idLoadingStates: this.setState(getNewLoadState, action, state.idLoadingStates)\n };\n\n // The updated loadingStates is passed to the user code for maximum\n // flexibility in case the user wishes to change the loadingStates.\n return (onLoad ? onLoad(newState, action) : newState) as OnState<State>;\n }),\n on(this.idSuccess, (state, action) => {\n const newState = {\n ...state,\n idLoadingStates: this.setState(getNewSuccessState, action, state.idLoadingStates)\n };\n\n return (onSuccess ? onSuccess(newState, action) : newState) as OnState<State>;\n }),\n on(this.idFailure, (state, action) => {\n const newState = {\n ...state,\n idLoadingStates: this.setState(getNewFailureState, action, state.idLoadingStates)\n };\n\n return (onFailure ? onFailure(newState, action) : newState) as OnState<State>;\n })\n ];\n }\n\n catchError(id: Id): ReturnType<typeof catchError> {\n return catchError((error) => {\n return of(\n // AZ: Casting to \"any\" is less than ideal. But just can't figure out the complex typing here.\n this.idFailure({\n id,\n error\n } as any)\n );\n });\n }\n\n // ----------------------------------------------------------------------------\n // Selectors\n // ----------------------------------------------------------------------------\n /**\n * Returns a map of selectors for loading, success, error, and the entire state.\n * The advantage of doing it in a bundle is that we can share the result of createStateSelector(),\n * if we separated into individual functions, each function might need to call createStateSelector()\n * to create a new instance of the selector. We can't cache any created selectors because will cause\n * memory leak since the cached references are always help in this class and hence does not get released.\n * @param selectLoadingStates Selector that returns the loadingStats of the feature slice. You can use createLoadingStatesSelector()\n * to create it.\n * @returns A collection of selectors\n * state: the LoadingState\n * loading: True if loading\n * success: True if last load was successful\n * error: LrError2 object if previous loading failed.\n *\n */\n createIdSelectors(\n selectIdLoadingStates: MemoizedSelector<\n object,\n IdLoadingStates,\n DefaultProjectorFn<IdLoadingStates>\n >\n ): {\n state: (id: Id) => MemoizedSelector<object, IdLoadingState, DefaultProjectorFn<IdLoadingState>>;\n loading: (id: Id) => MemoizedSelector<object, boolean, DefaultProjectorFn<boolean>>;\n success: (id: Id) => MemoizedSelector<object, boolean, DefaultProjectorFn<boolean>>;\n error: (id: Id) => MemoizedSelector<object, any, DefaultProjectorFn<any>>;\n combinedState: MemoizedSelector<object, any, DefaultProjectorFn<any>>;\n } {\n const selectIdLoadingStateMap = createSelector(\n selectIdLoadingStates,\n (idLoadingStates) => idLoadingStates[this.key]\n );\n\n const state = (\n id: Id\n ): MemoizedSelector<object, IdLoadingState, DefaultProjectorFn<IdLoadingState>> => {\n return createSelector(selectIdLoadingStateMap, (idLoadingStateMap) =>\n this.getIdLoadingState(idLoadingStateMap, id)\n );\n };\n\n const loading = (id: Id) => {\n return createSelector(state(id), (idLoadingState) => idLoadingState.loading);\n };\n const success = (id: Id) => {\n return createSelector(state(id), (idLoadingState) => idLoadingState.success);\n };\n const error = (id: Id) => {\n return createSelector(state(id), (idLoadingState) => idLoadingState.error);\n };\n\n const combinedState = createSelector(selectIdLoadingStateMap, (idLoadingStateMap) => {\n return combineLoadingStates(idLoadingStateMap ? Object.values(idLoadingStateMap) : []);\n });\n\n return {\n state,\n loading,\n success,\n error,\n combinedState\n };\n }\n\n // ----------------------------------------------------------------------------\n // Effects\n // ----------------------------------------------------------------------------\n idLoadHandler(\n fetch: (\n idActions$: Observable<IdLoadAction & LoadPayloadType & TypedAction<string>>,\n id: Id\n ) => Observable<Action>\n ): UnaryFunction<Observable<Action>, Observable<Action>> {\n return pipe(ofType(this.idLoad), (source): Observable<Action> => {\n // Below is inspired by: https://github.com/nrwl/nx/blob/master/packages/angular/src/runtime/nx/data-persistence.ts#L75\n const groupedFetches = source.pipe(\n groupBy((action) => {\n return action.id; // This will be used as the \"group.key\"\n })\n );\n return groupedFetches.pipe(\n mergeMap((group) => {\n return fetch(group, group.key);\n })\n );\n });\n }\n\n createEffect(\n actions$: Actions,\n fetch: (\n idActions$: Observable<IdLoadAction & LoadPayloadType & TypedAction<string>>,\n id: Id\n ) => Observable<Action>\n ) {\n return createEffect(() => {\n return actions$.pipe(this.idLoadHandler(fetch));\n });\n }\n\n // ----------------------------------------------------------------------------\n // Helpers\n // ----------------------------------------------------------------------------\n private get key(): string {\n return this.idLoad.type;\n }\n\n private getIdLoadingState(\n idLoadingStateMap: Readonly<IdLoadingStateMap>,\n id: Id\n ): Readonly<IdLoadingState> {\n if (id == null || id === '') {\n throw new Error('id parameter is null or empty string, this is almost always a logic bug.');\n }\n // We should not be modifying the state without going via the reducer, hence\n // returning the immutable \"init\" object.\n return idLoadingStateMap?.[id];\n }\n\n private setState(\n getNewState: (\n action: Action & (IdLoadAction | IdSuccessAction | IdFailureAction),\n oldLoadingState: Readonly<LoadingState>\n ) => Readonly<LoadingState> | null,\n action: Action & (IdLoadAction | IdSuccessAction | IdFailureAction),\n idLoadingStates: Readonly<IdLoadingStates>\n ): Readonly<IdLoadingStates> {\n const currentState = cloneLoadingState(idLoadingStates[this.key]?.[action.id]);\n const newState = getNewState(action, currentState);\n\n if (newState) {\n // Return new reference only when the state has changed.\n return {\n ...idLoadingStates,\n [this.key]: {\n ...idLoadingStates[this.key],\n [action.id]: { ...newState, isIdLoadingState: true, id: action.id }\n }\n };\n } else {\n // No change in state, so no change in parent state.\n return idLoadingStates;\n }\n }\n}\n","// These classes are basically serving the same purpose as props<T> in createAction() where it\n// just holds the type and allows you to name the class to make it easier to read. The alternative\n// is to explicitly specify the type when calling createLoadingActions<...>(). But the template\n\nimport { createSelector, DefaultProjectorFn, MemoizedSelector } from '@ngrx/store';\nimport { actionFactory } from '../loading-state/loading-state-functions';\nimport { NoIntersection } from '../utils';\nimport { IdLoadingActions } from './id-loading-state-actions';\nimport {\n IdFailureAction,\n IdLoadAction,\n IdLoadingStates,\n IdSuccessAction,\n WithIdLoadingStatesOnly\n} from './id-loading-state-types';\n\nexport function idLoad<\n LoadPayloadType extends NotIdLoadingAction<LoadPayloadType>\n>(): IdLoad<LoadPayloadType> {\n return new IdLoad<LoadPayloadType>();\n}\n\nexport function idSuccess<\n SuccessPayloadType extends NotIdSuccessAction<SuccessPayloadType>\n>(): IdSuccess<SuccessPayloadType> {\n return new IdSuccess<SuccessPayloadType>();\n}\n\nexport function idFailure<\n FailurePayloadType extends NotIdFailureAction<FailurePayloadType>\n>(): IdFailure<FailurePayloadType> {\n return new IdFailure<FailurePayloadType>();\n}\n\n/**\n * Creates a set of IdLoadAction, IdSuccessAction, and IdFailureAction. Selectors and reducers are always bundled into\n * the same structure.\n *\n * The difference between IdLoadAction and LoadAction is that IdLoadAction is parameterized by an id field. So you can\n * display loading states for multiple items that are all loading in parallel. Typical use case is you have a list of items,\n * and you can issue load actions for each one, parameterized by an id of your own choosing, and observe the loading state\n * of each item, again parameterized by the id.\n *\n * @param type The \"type\" of the action.\n * @param _idLoad See usage example\n * @param _idSuccess See usage example\n * @param _idFailure See usage example\n * @returns An instance of LoadingActions class that bundles together actions, selectors and reducers.\n * @example\n * export const fetchItem = createIdLoadingActions(\n * 'Fetch Item',\n * // An id field is already included in each of LoadAction, SuccessAction, FailureAction\n * load<{}>(),\n * success<{ item: object }>(),\n * failure<{}>()\n * );\n *\n * // Dispatch load action\n * const id = \"123\";\n * this.store.dispatch(fetchItem.idLoad({ id }));\n *\n * // Using ngrx's createFeatureSelector to select the feature slice from global store.\n * const selectState = createFeatureSelector<SimpleState>(SIMPLE_FEATURE_KEY);\n * const selectLoadingStates = createLoadingStatesSelector(selectState);\n *\n * const fetchItemSelectors = fetchItem.createIdSelectors(selectLoadingStates);\n *\n * // Observe the loading state.\n * this.store.select(fetchItemSelectors.state(id));\n *\n */\nexport function createIdLoadingActions<\n LoadPayloadType extends object,\n SuccessPayloadType extends object,\n FailurePayloadType extends object\n>(\n actionTypePrefix: string,\n _idLoad: IdLoad<LoadPayloadType>,\n _idSuccess: IdSuccess<SuccessPayloadType>,\n _idFailure: IdFailure<FailurePayloadType>\n): IdLoadingActions<LoadPayloadType, SuccessPayloadType, FailurePayloadType> {\n return new IdLoadingActions({\n idLoad: actionFactory<IdLoadAction & LoadPayloadType>(`${actionTypePrefix}`),\n idSuccess: actionFactory<IdSuccessAction & SuccessPayloadType>(`${actionTypePrefix} Success`),\n idFailure: actionFactory<IdFailureAction & FailurePayloadType>(`${actionTypePrefix} Failure`)\n });\n}\n\n/**\n *\n * @param featureSelector Selector that selects the current feature slice of the store.\n * @returns Selector that selects the loadingStates field from the store\n * @example\n * // Using ngrx's createFeatureSelector to select the feature slice from global store.\n * const selectState = createFeatureSelector<SimpleState>(SIMPLE_FEATURE_KEY);\n * const selectLoadingStates = createLoadingStatesSelector(selectState);\n *\n * You can then use selectLoadingStates to compose other selectors. eg.\n *\n * export const fetchItem = createIdLoadingActions(\n * 'Fetch Item',\n * load<{}>(),\n * success<{ item: object }>(),\n * failure<{}>()\n * );\n *\n * export const fetchItemSelectors = fetchItem.createSelectors(selectLoadingStates);\n *\n */\nexport function createIdLoadingStatesSelector<State extends WithIdLoadingStatesOnly>(\n featureSelector: MemoizedSelector<object, State, DefaultProjectorFn<State>>\n): MemoizedSelector<object, IdLoadingStates, DefaultProjectorFn<IdLoadingStates>> {\n return createSelector(featureSelector, (state) => {\n return state.idLoadingStates;\n });\n}\n\n// ------------------------------------------------------------------------\n// Internal\n// ------------------------------------------------------------------------\n\n// types are positional only, so not easy to read.\nclass IdLoad<_LoadPayloadType> {\n // These variables with constant string typing prevents Load and Success instances from being\n // assignable to each other.\n type: 'ID_LOAD' = 'ID_LOAD';\n}\nclass IdSuccess<_SuccessPayloadType> {\n type: 'ID_SUCCESS' = 'ID_SUCCESS';\n}\nclass IdFailure<_FailurePayloadType> {\n type: 'ID_FAILURE' = 'ID_FAILURE';\n}\n\n// This ensures that we don't redefine the existing fields in the actions.\ntype NotIdLoadingAction<T> = NoIntersection<T, IdLoadAction>;\ntype NotIdSuccessAction<T> = NoIntersection<T, IdSuccessAction>;\ntype NotIdFailureAction<T> = NoIntersection<T, IdFailureAction>;\n","import { createSelector, DefaultProjectorFn, MemoizedSelector, on } from '@ngrx/store';\nimport { Action, TypedAction } from '@ngrx/store/src/models';\nimport { catchError, of } from 'rxjs';\nimport {\n cloneLoadingState,\n getNewFailureState,\n getNewLoadState,\n getNewSuccessState\n} from './loading-state-functions';\nimport {\n ActionFactoryResult,\n FailureAction,\n LoadAction,\n LoadingActionsReducerTypes,\n LoadingState,\n LoadingStates,\n OnState\n} from './loading-state-types';\n\n/**\n * This class bundles up a set of load, success, and failure actions. It contains helpers to create\n * reducers for these actions and helpers for creating selectors.\n *\n * Do not use this class directly, use the createLoadingActions() helper function.\n *\n */\nexport class LoadingActions<\n LoadPayloadType extends object,\n SuccessPayloadType extends object,\n FailurePayloadType extends object\n> {\n /** The actions the user can dispatch */\n readonly load: ActionFactoryResult<LoadAction & LoadPayloadType>;\n readonly success: ActionFactoryResult<SuccessPayloadType>;\n readonly failure: ActionFactoryResult<FailureAction & FailurePayloadType>;\n\n constructor(options: {\n load: ActionFactoryResult<LoadAction & LoadPayloadType>;\n success: ActionFactoryResult<SuccessPayloadType>;\n failure: ActionFactoryResult<FailureAction & FailurePayloadType>;\n }) {\n // Could have used createActionGroup() but the string literal typing of Source is giving me trouble. For now,\n // just separate types.\n this.load = options.load;\n this.success = options.success;\n this.failure = options.failure;\n }\n\n // ----------------------------------------------------------------------------\n // Typing\n // ----------------------------------------------------------------------------\n /**\n * Type guard to test if action is of type LoadingActions.load.\n *\n * @param action Any ngrx action\n * @returns True if action if of type LoadingActions.load\n */\n instanceOfLoad(\n action: Action\n ): action is ReturnType<ActionFactoryResult<LoadAction & LoadPayloadType>> {\n return action.type === this.load.type;\n }\n\n /**\n * Type guard to test if action is of type LoadingActions.success.\n *\n * @param action Any ngrx action\n * @returns True if action if of type LoadingActions.success\n */\n instanceOfSuccess(action: Action): action is ReturnType<ActionFactoryResult<SuccessPayloadType>> {\n return action.type === this.success.type;\n }\n\n /**\n * Type guard to test if action is of type LoadingActions.failure.\n *\n * @param action Any ngrx action\n * @returns True if action if of type LoadingActions.failure\n */\n instanceOfFailure(\n action: Action\n ): action is ReturnType<ActionFactoryResult<FailureAction & FailurePayloadType>> {\n return action.type === this.failure.type;\n }\n\n // ------------------------------------------------------------------------------------------------\n // Reducer\n // ------------------------------------------------------------------------------------------------\n /**\n * Creates reducers for the load, success, failure actions.\n *\n * @param options.onLoad Call back when action is LoadAction. Return new copy of state if state needs to change.\n * @param options.onSuccess Call back when action is SuccessAction. Return new copy of state if state needs to change.\n * @param options.onFailure Call back when action is FailureAction. Return new copy of state if state needs to change.\n * @returns A tuple of \"on()\" instances that handles load, success, failure actions in this order.\n * @example\n * export const reducer = createReducer(\n * initialState,\n * // Note: (1)\n * ...fetchItem.reducer<ItemState>({\n * onSuccess: (state, { item }): ItemState => {\n * return { ...state, item };\n * },\n * // You can also customise what happens for LoadAction and FailureAction through onLoad and onFailure.\n * // But most of the time, there's nothing to do for those. They are automatically handled by the fetchItem.reducer\n * }),\n * );\n *\n * (1) We need this explicit return type because \"return type widening\" means that the\n * type constraints on the onSuccess function is only narrowing, so if you provide more\n * fields in the return object that is not in VaultState, there is no error. Because\n * the return value can be narrowed to the onSuccess function's constraints. So we have\n * to explicitly specify the return type here. This is a well known issue that has been\n * raise since at least 2016. So does not look like it will be fixed. Also, it's a recommended\n * pattern in ngrx reducers to have this explicit typing.\n *\n * Ref https://github.com/microsoft/TypeScript/issues/241#issuecomment-327269994\n *\n * (2): Given that there are explicity return types on the onLoad, onSuccess, onFailure functions\n * it should be possible to infer the type of the state here. But can't figure out how.\n *\n */\n reducer<State extends { loadingStates: LoadingStates }>(options?: {\n onLoad?: (\n state: OnState<State>,\n action: LoadAction & LoadPayloadType & TypedAction<string>\n ) => State;\n onSuccess?: (state: OnState<State>, action: SuccessPayloadType & TypedAction<string>) => State;\n onFailure?: (\n state: OnState<State>,\n action: FailureAction & FailurePayloadType & TypedAction<string>\n ) => State;\n }): [\n LoadingActionsReducerTypes<State>,\n LoadingActionsReducerTypes<State>,\n LoadingActionsReducerTypes<State>\n ] {\n const { onLoad, onSuccess, onFailure } = options || {};\n return [\n on(this.load, (state, action) => {\n // Reducer must always create a new copy of the state.\n const newState = {\n ...state,\n loadingStates: this.setState(getNewLoadState, action, state.loadingStates)\n };\n\n // The updated loadingStates is passed to the user code for maximum\n // flexibility in case the user wishes to change the loadingStates.\n return (onLoad ? onLoad(newState, action) : newState) as OnState<State>;\n }),\n on(this.success, (state, action) => {\n const newState = {\n ...state,\n loadingStates: this.setState(getNewSuccessState, action, state.loadingStates)\n };\n\n return (onSuccess ? onSuccess(newState, action) : newState) as OnState<State>;\n }),\n on(this.failure, (state, action) => {\n const newState = {\n ...state,\n loadingStates: this.setState(getNewFailureState, action, state.loadingStates)\n };\n\n return (onFailure ? onFailure(newState, action) : newState) as OnState<State>;\n })\n ];\n }\n\n /**\n * Catches errors in effect.\n *\n * @returns rxjs operator that emits a FailureAction.\n * @example\n *\n * fetchCount$ = createEffect(() => {\n * return this.actions$.pipe(\n * ofType(fetchCount.load),\n * filterLoading(this.store.select(fetchCountSelectors.state)),\n * switchMap((action) => {\n * return apiCAll().pipe(\n * map(item => fetchCount.success(item)),\n * fetchCount.catchError()\n * );\n * })\n * );\n * });\n *\n */\n catchError(): ReturnType<typeof catchError> {\n return catchError((error) => {\n return of(\n // AZ: Casting to \"any\" is less than ideal. But just can't figure out the complex typing here.\n this.failure({\n error\n } as any)\n );\n });\n }\n\n // ----------------------------------------------------------------------------\n // Selectors\n // ----------------------------------------------------------------------------\n /**\n * Returns a map of selectors for loading, success, error, and the entire loading state. Similar to the ngrx entities adaptor.\n *\n * Design: The advantage of doing it in a bundle is that we can share the result of createStateSelector().\n * If we had separate individual functions, each function might need to call createStateSelector()\n * to create a new instance of the selector. We can't cache any created selectors because that will cause\n * a memory leak since the cached references are always held in this class and hence does not get released.\n *\n * @param selectLoadingStates Selector that returns the loadingStats of the feature slice. You can use createLoadingStatesSelector()\n * to create it.\n * @returns A collection of selectors\n * state: the LoadingState\n * loading: True if loading\n * success: True if last load was successful\n * error: any errors from the last API call.\n * @example\n * // The feature slice selector. Standard ngrx stuff.\n * const selectState = createFeatureSelector<SimpleState>(SIMPLE_FEATURE_KEY);\n *\n * // Selector that selects the loadingStates field from the global store. The createLoadingStatesSelector()\n * // is provided as a part of this lib as well.\n * const selectLoadingStates = createLoadingStatesSelector(selectState);\n *\n * // Create the selectors related to the fetchItem loading state.\n * export const fetchItemSelectors = fetchItem.createSelectors(selectLoadingStates);\n *\n * // You can then observe the loading states:\n * this.store.select(fetchItemSelectors.state); // The entire LoadingState\n * this.store.select(fetchItemSelectors.success); // The boolean success flag\n * this.store.select(fetchItemSelectors.loading); // The boolean loading flag\n *\n */\n createSelectors(\n selectLoadingStates: MemoizedSelector<object, LoadingStates, DefaultProjectorFn<LoadingStates>>\n ): {\n state: MemoizedSelector<object, LoadingState, DefaultProjectorFn<LoadingState>>;\n loading: MemoizedSelector<object, boolean, DefaultProjectorFn<boolean>>;\n success: MemoizedSelector<object, boolean, DefaultProjectorFn<boolean>>;\n error: MemoizedSelector<object, any, DefaultProjectorFn<any>>;\n } {\n const state = createSelector(selectLoadingStates, (loadingStates) => {\n return this.getLoadingState(loadingStates);\n });\n\n const loading = createSelector(state, (loadingState) => loadingState.loading);\n const success = createSelector(state, (loadingState) => loadingState.success);\n const error = createSelector(state, (loadingState) => loadingState.error);\n\n return {\n state,\n loading,\n success,\n error\n };\n }\n\n // ----------------------------------------------------------------------------\n // Helpers\n // ----------------------------------------------------------------------------\n private get key(): string {\n return this.load.type;\n }\n\n private getLoadingState(loadingStates: Readonly<LoadingStates>): Readonly<LoadingState> {\n // We should not be modifying the state without going via the reducer, hence\n // returning the immutable \"init\" object.\n return loadingStates[this.key];\n }\n\n private setState(\n getNewState: (\n action: Action & LoadAction,\n oldLoadingState: Readonly<LoadingState>\n ) => Readonly<LoadingState> | null,\n action: Action & LoadAction,\n loadingStates: Readonly<LoadingStates>\n ): Readonly<LoadingStates> {\n // We work with LoadingStateBase here to be generic. The idLoadingActions also\n // use these functions.\n const currentState = cloneLoadingState(this.getLoadingState(loadingStates));\n const newState = getNewState(action, currentState);\n\n if (newState) {\n // Return new reference only when the state has changed.\n return {\n ...loadingStates,\n [this.key]: {\n ...newState\n }\n };\n } else {\n // No change in state, so no change in parent state.\n return loadingStates;\n }\n }\n}\n","import { createSelector, DefaultProjectorFn, MemoizedSelector } from '@ngrx/store';\nimport { NoIntersection } from '../utils';\nimport { LoadingActions } from './loading-state-actions';\nimport { actionFactory } from './loading-state-functions';\nimport {\n FailureAction,\n LoadAction,\n LoadingStates,\n WithLoadingStatesOnly\n} from './loading-state-types';\n\nexport function load<\n LoadPayloadType extends NotLoadingAction<LoadPayloadType>\n>(): Load<LoadPayloadType> {\n return new Load<LoadPayloadType>();\n}\n\nexport function success<SuccessPayloadType>(): Success<SuccessPayloadType> {\n return new Success<SuccessPayloadType>();\n}\n\nexport function failure<\n FailurePayloadType extends NotFailureAction<FailurePayloadType>\n>(): Failure<FailurePayloadType> {\n return new Failure<FailurePayloadType>();\n}\n\n/**\n * Creates a set of load, success, failure actions. Selectors and reducers are always bundled into\n * the same structure.\n *\n * @param type The \"type\" of the action.\n * @param _load See usage example\n * @param _success See usage example\n * @param _failure See usage example\n * @returns An instance of LoadingActions class that bundles together actions, selectors and reducers.\n * @example\n * export const fetchItem = createLoadingActions(\n * 'Fetch Item',\n * load<{ itemId: number }>(), // Action type is: 'Fetch Item'\n * success<{ item: object }>(), // Action type is: 'Fetch Item Success'\n * failure<{}>() // Action type is: 'Fetch Item Failure'\n * );\n */\nexport function createLoadingActions<\n LoadPayloadType extends object,\n SuccessPayloadType extends object,\n FailurePayloadType extends object\n>(\n type: string,\n _load: Load<LoadPayloadType>,\n _success: Success<SuccessPayloadType>,\n _failure: Failure<FailurePayloadType>\n): LoadingActions<LoadPayloadType, SuccessPayloadType, FailurePayloadType> {\n return new LoadingActions({\n load: actionFactory<LoadAction & LoadPayloadType>(`${type}`),\n success: actionFactory<SuccessPayloadType>(`${type} Success`),\n failure: actionFactory<FailureAction & FailurePayloadType>(`${type} Failure`)\n });\n}\n\n/**\n *\n * @param featureSelector Selector that selects the current feature slice of the store.\n * @returns Selector that selects the loadingStates field from the store\n * @example\n * // Using ngrx's createFeatureSelector to select the feature slice from global store.\n * const selectState = createFeatureSelector<SimpleState>(SIMPLE_FEATURE_KEY);\n * const selectLoadingStates = createLoadingStatesSelector(selectState);\n *\n * You can then use selectLoadingStates to compose other selectors. eg.\n *\n * export const fetchItem = createLoadingActions(\n * 'Fetch Item',\n * load<{ itemId: number }>(),\n * success<{ item: object }>(),\n * failure<{}>()\n * );\n *\n * export const fetchItemSelectors = fetchItem.createSelectors(selectLoadingStates);\n *\n */\nexport function createLoadingStatesSelector<State extends WithLoadingStatesOnly>(\n featureSelector: MemoizedSelector<object, State, DefaultProjectorFn<State>>\n): MemoizedSelector<object, LoadingStates, DefaultProjectorFn<LoadingStates>> {\n return createSelector(featureSelector, (state) => {\n return state.loadingStates;\n });\n}\n\n// ------------------------------------------------------------------------\n// Internal\n// ------------------------------------------------------------------------\n\n// These classes are basically serving the same purpose as props<T> in createAction() where it\n// just holds the type and allows you to name the class to make it easier to read. The alternative\n// is to explicitly specify the type when calling createLoadingActions<...>(). But the template\n\n// types are positional only, so not easy to read.\nclass Load<_LoadPayloadType> {\n // These variables with constant string typing prevents Load and Success instances from being\n // assignable to each other.\n type: 'LOAD' = 'LOAD';\n}\nclass Success<_SuccessPayloadType> {\n type: 'SUCCESS' = 'SUCCESS';\n}\nclass Failure<_FailurePayloadType> {\n type: 'FAILURE' = 'FAILURE';\n}\n\n// This ensures that we don't redefine the existing fields in the actions.\ntype NotLoadingAction<T> = NoIntersection<T, LoadAction>;\ntype NotFailureAction<T> = NoIntersection<T, FailureAction>;\n","import { filter, map, Observable, pipe, UnaryFunction, withLatestFrom } from 'rxjs';\nimport { LoadingState } from './loading-state-types';\n\n/**\n * A ngrx pipeline operator that filters out any actions that does not require the\n * issuing of API calls.\n *\n * Design: Note that whether the action should issue a fetch is done in the reducer, where the\n * loadingState.issueFetch parameter is updated. We can NOT combine the current state and the\n * action in the effect to decide if we need to issue an API call, because that could lead to race\n * conditions. The only guaranteed point of synchronous execution is the reducer.\n *\n * @param loadingState$ Observable that emit the current loading state from the store.\n * @returns Stream that only emits a LoadAction if that that load action should result in an API call.\n * @ex