UNPKG

ngrx-loading-state

Version:

NgRx Loading State consistently manages loading actions such as API fetches.

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