UNPKG

@ngrx/store

Version:

RxJS powered Redux for Angular apps

1,424 lines (1,403 loc) 58.3 kB
import * as i0 from '@angular/core'; import { Injectable, InjectionToken, Inject, computed, effect, untracked, inject, Injector, isDevMode, makeEnvironmentProviders, provideEnvironmentInitializer, NgModule, Optional } from '@angular/core'; import { BehaviorSubject, Observable, Subject, queueScheduler } from 'rxjs'; import { observeOn, withLatestFrom, scan, pluck, map, distinctUntilChanged } from 'rxjs/operators'; import { toSignal } from '@angular/core/rxjs-interop'; const REGISTERED_ACTION_TYPES = {}; function resetRegisteredActionTypes() { for (const key of Object.keys(REGISTERED_ACTION_TYPES)) { delete REGISTERED_ACTION_TYPES[key]; } } /** * @description * Creates a configured `Creator` function that, when called, returns an object in the shape of the `Action` interface. * * Action creators reduce the explicitness of class-based action creators. * * @param type Describes the action that will be dispatched * @param config Additional metadata needed for the handling of the action. See {@link createAction#usage-notes Usage Notes}. * * @usageNotes * * **Declaring an action creator** * * Without additional metadata: * ```ts * export const increment = createAction('[Counter] Increment'); * ``` * With additional metadata: * ```ts * export const loginSuccess = createAction( * '[Auth/API] Login Success', * props<{ user: User }>() * ); * ``` * With a function: * ```ts * export const loginSuccess = createAction( * '[Auth/API] Login Success', * (response: Response) => response.user * ); * ``` * * **Dispatching an action** * * Without additional metadata: * ```ts * store.dispatch(increment()); * ``` * With additional metadata: * ```ts * store.dispatch(loginSuccess({ user: newUser })); * ``` * * **Referencing an action in a reducer** * * Using a switch statement: * ```ts * switch (action.type) { * // ... * case AuthApiActions.loginSuccess.type: { * return { * ...state, * user: action.user * }; * } * } * ``` * Using a reducer creator: * ```ts * on(AuthApiActions.loginSuccess, (state, { user }) => ({ ...state, user })) * ``` * * **Referencing an action in an effect** * ```ts * effectName$ = createEffect( * () => this.actions$.pipe( * ofType(AuthApiActions.loginSuccess), * // ... * ) * ); * ``` */ function createAction(type, config) { REGISTERED_ACTION_TYPES[type] = (REGISTERED_ACTION_TYPES[type] || 0) + 1; if (typeof config === 'function') { return defineType(type, (...args) => ({ ...config(...args), type, })); } const as = config ? config._as : 'empty'; switch (as) { case 'empty': return defineType(type, () => ({ type })); case 'props': return defineType(type, (props) => ({ ...props, type, })); default: throw new Error('Unexpected config.'); } } function props() { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return { _as: 'props', _p: undefined }; } function union(creators) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return undefined; } function defineType(type, creator) { return Object.defineProperty(creator, 'type', { value: type, writable: false, }); } function capitalize(text) { return (text.charAt(0).toUpperCase() + text.substring(1)); } function uncapitalize(text) { return (text.charAt(0).toLowerCase() + text.substring(1)); } function assertDefined(value, name) { if (value === null || value === undefined) { throw new Error(`${name} must be defined.`); } } /** * @description * A function that creates a group of action creators with the same source. * * @param config An object that contains a source and dictionary of events. * An event is a key-value pair of an event name and event props. * @returns A dictionary of action creators. * The name of each action creator is created by camel casing the event name. * The type of each action is created using the "[Source] Event Name" pattern. * * @usageNotes * * ```ts * const authApiActions = createActionGroup({ * source: 'Auth API', * events: { * // defining events with payload using the `props` function * 'Login Success': props<{ userId: number; token: string }>(), * 'Login Failure': props<{ error: string }>(), * * // defining an event without payload using the `emptyProps` function * 'Logout Success': emptyProps(), * * // defining an event with payload using the props factory * 'Logout Failure': (error: Error) => ({ error }), * }, * }); * * // action type: "[Auth API] Login Success" * authApiActions.loginSuccess({ userId: 10, token: 'ngrx' }); * * // action type: "[Auth API] Login Failure" * authApiActions.loginFailure({ error: 'Login Failure!' }); * * // action type: "[Auth API] Logout Success" * authApiActions.logoutSuccess(); * * // action type: "[Auth API] Logout Failure"; * authApiActions.logoutFailure(new Error('Logout Failure!')); * ``` */ function createActionGroup(config) { const { source, events } = config; return Object.keys(events).reduce((actionGroup, eventName) => ({ ...actionGroup, [toActionName(eventName)]: createAction(toActionType(source, eventName), events[eventName]), }), {}); } function emptyProps() { return props(); } function toActionName(eventName) { return eventName .trim() .split(' ') .map((word, i) => (i === 0 ? uncapitalize(word) : capitalize(word))) .join(''); } function toActionType(source, eventName) { return `[${source}] ${eventName}`; } const INIT = '@ngrx/store/init'; class ActionsSubject extends BehaviorSubject { constructor() { super({ type: INIT }); } next(action) { if (typeof action === 'function') { throw new TypeError(` Dispatch expected an object, instead it received a function. If you're using the createAction function, make sure to invoke the function before dispatching the action. For example, someAction should be someAction().`); } else if (typeof action === 'undefined') { throw new TypeError(`Actions must be objects`); } else if (typeof action.type === 'undefined') { throw new TypeError(`Actions must have a type property`); } super.next(action); } complete() { /* noop */ } ngOnDestroy() { super.complete(); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ActionsSubject, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ActionsSubject }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ActionsSubject, decorators: [{ type: Injectable }], ctorParameters: () => [] }); const ACTIONS_SUBJECT_PROVIDERS = [ActionsSubject]; const _ROOT_STORE_GUARD = new InjectionToken('@ngrx/store Internal Root Guard'); const _INITIAL_STATE = new InjectionToken('@ngrx/store Internal Initial State'); const INITIAL_STATE = new InjectionToken('@ngrx/store Initial State'); const REDUCER_FACTORY = new InjectionToken('@ngrx/store Reducer Factory'); const _REDUCER_FACTORY = new InjectionToken('@ngrx/store Internal Reducer Factory Provider'); const INITIAL_REDUCERS = new InjectionToken('@ngrx/store Initial Reducers'); const _INITIAL_REDUCERS = new InjectionToken('@ngrx/store Internal Initial Reducers'); const STORE_FEATURES = new InjectionToken('@ngrx/store Store Features'); const _STORE_REDUCERS = new InjectionToken('@ngrx/store Internal Store Reducers'); const _FEATURE_REDUCERS = new InjectionToken('@ngrx/store Internal Feature Reducers'); const _FEATURE_CONFIGS = new InjectionToken('@ngrx/store Internal Feature Configs'); const _STORE_FEATURES = new InjectionToken('@ngrx/store Internal Store Features'); const _FEATURE_REDUCERS_TOKEN = new InjectionToken('@ngrx/store Internal Feature Reducers Token'); const FEATURE_REDUCERS = new InjectionToken('@ngrx/store Feature Reducers'); /** * User-defined meta reducers from StoreModule.forRoot() */ const USER_PROVIDED_META_REDUCERS = new InjectionToken('@ngrx/store User Provided Meta Reducers'); /** * Meta reducers defined either internally by @ngrx/store or by library authors */ const META_REDUCERS = new InjectionToken('@ngrx/store Meta Reducers'); /** * Concats the user provided meta reducers and the meta reducers provided on the multi * injection token */ const _RESOLVED_META_REDUCERS = new InjectionToken('@ngrx/store Internal Resolved Meta Reducers'); /** * Runtime checks defined by the user via an InjectionToken * Defaults to `_USER_RUNTIME_CHECKS` */ const USER_RUNTIME_CHECKS = new InjectionToken('@ngrx/store User Runtime Checks Config'); /** * Runtime checks defined by the user via forRoot() */ const _USER_RUNTIME_CHECKS = new InjectionToken('@ngrx/store Internal User Runtime Checks Config'); /** * Runtime checks currently in use */ const ACTIVE_RUNTIME_CHECKS = new InjectionToken('@ngrx/store Internal Runtime Checks'); const _ACTION_TYPE_UNIQUENESS_CHECK = new InjectionToken('@ngrx/store Check if Action types are unique'); /** * InjectionToken that registers the global Store. * Mainly used to provide a hook that can be injected * to ensure the root state is loaded before something * that depends on it. */ const ROOT_STORE_PROVIDER = new InjectionToken('@ngrx/store Root Store Provider'); /** * InjectionToken that registers feature states. * Mainly used to provide a hook that can be injected * to ensure feature state is loaded before something * that depends on it. */ const FEATURE_STATE_PROVIDER = new InjectionToken('@ngrx/store Feature State Provider'); /** * @description * Combines reducers for individual features into a single reducer. * * You can use this function to delegate handling of state transitions to multiple reducers, each acting on their * own sub-state within the root state. * * @param reducers An object mapping keys of the root state to their corresponding feature reducer. * @param initialState Provides a state value if the current state is `undefined`, as it is initially. * @returns A reducer function. * * @usageNotes * * **Example combining two feature reducers into one "root" reducer** * * ```ts * export const reducer = combineReducers({ * featureA: featureAReducer, * featureB: featureBReducer * }); * ``` * * You can also override the initial states of the sub-features: * ```ts * export const reducer = combineReducers({ * featureA: featureAReducer, * featureB: featureBReducer * }, { * featureA: { counterA: 13 }, * featureB: { counterB: 37 } * }); * ``` */ function combineReducers(reducers, initialState = {}) { const reducerKeys = Object.keys(reducers); const finalReducers = {}; for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i]; if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key]; } } const finalReducerKeys = Object.keys(finalReducers); return function combination(state, action) { state = state === undefined ? initialState : state; let hasChanged = false; const nextState = {}; for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i]; const reducer = finalReducers[key]; const previousStateForKey = state[key]; const nextStateForKey = reducer(previousStateForKey, action); nextState[key] = nextStateForKey; hasChanged = hasChanged || nextStateForKey !== previousStateForKey; } return hasChanged ? nextState : state; }; } function omit(object, keyToRemove) { return Object.keys(object) .filter((key) => key !== keyToRemove) .reduce((result, key) => Object.assign(result, { [key]: object[key] }), {}); } function compose(...functions) { return function (arg) { if (functions.length === 0) { return arg; } const last = functions[functions.length - 1]; const rest = functions.slice(0, -1); return rest.reduceRight((composed, fn) => fn(composed), last(arg)); }; } function createReducerFactory(reducerFactory, metaReducers) { if (Array.isArray(metaReducers) && metaReducers.length > 0) { reducerFactory = compose.apply(null, [ ...metaReducers, reducerFactory, ]); } return (reducers, initialState) => { const reducer = reducerFactory(reducers); return (state, action) => { state = state === undefined ? initialState : state; return reducer(state, action); }; }; } function createFeatureReducerFactory(metaReducers) { const reducerFactory = Array.isArray(metaReducers) && metaReducers.length > 0 ? compose(...metaReducers) : (r) => r; return (reducer, initialState) => { reducer = reducerFactory(reducer); return (state, action) => { state = state === undefined ? initialState : state; return reducer(state, action); }; }; } class ReducerObservable extends Observable { } class ReducerManagerDispatcher extends ActionsSubject { } const UPDATE = '@ngrx/store/update-reducers'; class ReducerManager extends BehaviorSubject { get currentReducers() { return this.reducers; } constructor(dispatcher, initialState, reducers, reducerFactory) { super(reducerFactory(reducers, initialState)); this.dispatcher = dispatcher; this.initialState = initialState; this.reducers = reducers; this.reducerFactory = reducerFactory; } addFeature(feature) { this.addFeatures([feature]); } addFeatures(features) { const reducers = features.reduce((reducerDict, { reducers, reducerFactory, metaReducers, initialState, key }) => { const reducer = typeof reducers === 'function' ? createFeatureReducerFactory(metaReducers)(reducers, initialState) : createReducerFactory(reducerFactory, metaReducers)(reducers, initialState); reducerDict[key] = reducer; return reducerDict; }, {}); this.addReducers(reducers); } removeFeature(feature) { this.removeFeatures([feature]); } removeFeatures(features) { this.removeReducers(features.map((p) => p.key)); } addReducer(key, reducer) { this.addReducers({ [key]: reducer }); } addReducers(reducers) { this.reducers = { ...this.reducers, ...reducers }; this.updateReducers(Object.keys(reducers)); } removeReducer(featureKey) { this.removeReducers([featureKey]); } removeReducers(featureKeys) { featureKeys.forEach((key) => { this.reducers = omit(this.reducers, key) /*TODO(#823)*/; }); this.updateReducers(featureKeys); } updateReducers(featureKeys) { this.next(this.reducerFactory(this.reducers, this.initialState)); this.dispatcher.next({ type: UPDATE, features: featureKeys, }); } ngOnDestroy() { this.complete(); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ReducerManager, deps: [{ token: ReducerManagerDispatcher }, { token: INITIAL_STATE }, { token: INITIAL_REDUCERS }, { token: REDUCER_FACTORY }], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ReducerManager }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ReducerManager, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: ReducerManagerDispatcher }, { type: undefined, decorators: [{ type: Inject, args: [INITIAL_STATE] }] }, { type: undefined, decorators: [{ type: Inject, args: [INITIAL_REDUCERS] }] }, { type: undefined, decorators: [{ type: Inject, args: [REDUCER_FACTORY] }] }] }); const REDUCER_MANAGER_PROVIDERS = [ ReducerManager, { provide: ReducerObservable, useExisting: ReducerManager }, { provide: ReducerManagerDispatcher, useExisting: ActionsSubject }, ]; class ScannedActionsSubject extends Subject { ngOnDestroy() { this.complete(); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ScannedActionsSubject, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ScannedActionsSubject }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: ScannedActionsSubject, decorators: [{ type: Injectable }] }); const SCANNED_ACTIONS_SUBJECT_PROVIDERS = [ ScannedActionsSubject, ]; class StateObservable extends Observable { } class State extends BehaviorSubject { static { this.INIT = INIT; } constructor(actions$, reducer$, scannedActions, initialState) { super(initialState); const actionsOnQueue$ = actions$.pipe(observeOn(queueScheduler)); const withLatestReducer$ = actionsOnQueue$.pipe(withLatestFrom(reducer$)); const seed = { state: initialState }; const stateAndAction$ = withLatestReducer$.pipe(scan(reduceState, seed)); this.stateSubscription = stateAndAction$.subscribe(({ state, action }) => { this.next(state); scannedActions.next(action); }); this.state = toSignal(this, { manualCleanup: true, requireSync: true }); } ngOnDestroy() { this.stateSubscription.unsubscribe(); this.complete(); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: State, deps: [{ token: ActionsSubject }, { token: ReducerObservable }, { token: ScannedActionsSubject }, { token: INITIAL_STATE }], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: State }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: State, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: ActionsSubject }, { type: ReducerObservable }, { type: ScannedActionsSubject }, { type: undefined, decorators: [{ type: Inject, args: [INITIAL_STATE] }] }] }); function reduceState(stateActionPair = { state: undefined }, [action, reducer]) { const { state } = stateActionPair; return { state: reducer(state, action), action }; } const STATE_PROVIDERS = [ State, { provide: StateObservable, useExisting: State }, ]; // disabled because we have lowercase generics for `select` class Store extends Observable { constructor(state$, actionsObserver, reducerManager, injector) { super(); this.actionsObserver = actionsObserver; this.reducerManager = reducerManager; this.injector = injector; this.source = state$; this.state = state$.state; } select(pathOrMapFn, ...paths) { return select.call(null, pathOrMapFn, ...paths)(this); } /** * Returns a signal of the provided selector. * * @param selector selector function * @param options select signal options */ selectSignal(selector, options) { return computed(() => selector(this.state()), options); } lift(operator) { const store = new Store(this, this.actionsObserver, this.reducerManager); store.operator = operator; return store; } dispatch(actionOrDispatchFn, config) { if (typeof actionOrDispatchFn === 'function') { return this.processDispatchFn(actionOrDispatchFn, config); } this.actionsObserver.next(actionOrDispatchFn); } next(action) { this.actionsObserver.next(action); } error(err) { this.actionsObserver.error(err); } complete() { this.actionsObserver.complete(); } addReducer(key, reducer) { this.reducerManager.addReducer(key, reducer); } removeReducer(key) { this.reducerManager.removeReducer(key); } processDispatchFn(dispatchFn, config) { assertDefined(this.injector, 'Store Injector'); const effectInjector = config?.injector ?? getCallerInjector() ?? this.injector; return effect(() => { const action = dispatchFn(); untracked(() => this.dispatch(action)); }, { injector: effectInjector }); } /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: Store, deps: [{ token: StateObservable }, { token: ActionsSubject }, { token: ReducerManager }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable }); } /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: Store }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.3", ngImport: i0, type: Store, decorators: [{ type: Injectable }], ctorParameters: () => [{ type: StateObservable }, { type: ActionsSubject }, { type: ReducerManager }, { type: i0.Injector }] }); const STORE_PROVIDERS = [Store]; function select(pathOrMapFn, propsOrPath, ...paths) { return function selectOperator(source$) { let mapped$; if (typeof pathOrMapFn === 'string') { const pathSlices = [propsOrPath, ...paths].filter(Boolean); mapped$ = source$.pipe(pluck(pathOrMapFn, ...pathSlices)); } else if (typeof pathOrMapFn === 'function') { mapped$ = source$.pipe(map((source) => pathOrMapFn(source, propsOrPath))); } else { throw new TypeError(`Unexpected type '${typeof pathOrMapFn}' in select operator,` + ` expected 'string' or 'function'`); } return mapped$.pipe(distinctUntilChanged()); }; } function getCallerInjector() { try { return inject(Injector); } catch (_) { return undefined; } } const RUNTIME_CHECK_URL = 'https://ngrx.io/guide/store/configuration/runtime-checks'; function isUndefined(target) { return target === undefined; } function isNull(target) { return target === null; } function isArray(target) { return Array.isArray(target); } function isString(target) { return typeof target === 'string'; } function isBoolean(target) { return typeof target === 'boolean'; } function isNumber(target) { return typeof target === 'number'; } function isObjectLike(target) { return typeof target === 'object' && target !== null; } function isObject(target) { return isObjectLike(target) && !isArray(target); } function isPlainObject(target) { if (!isObject(target)) { return false; } const targetPrototype = Object.getPrototypeOf(target); return targetPrototype === Object.prototype || targetPrototype === null; } function isFunction(target) { return typeof target === 'function'; } function isComponent(target) { return isFunction(target) && target.hasOwnProperty('ɵcmp'); } function hasOwnProperty(target, propertyName) { return Object.prototype.hasOwnProperty.call(target, propertyName); } let _ngrxMockEnvironment = false; function setNgrxMockEnvironment(value) { _ngrxMockEnvironment = value; } function isNgrxMockEnvironment() { return _ngrxMockEnvironment; } function isEqualCheck(a, b) { return a === b; } function isArgumentsChanged(args, lastArguments, comparator) { for (let i = 0; i < args.length; i++) { if (!comparator(args[i], lastArguments[i])) { return true; } } return false; } function resultMemoize(projectionFn, isResultEqual) { return defaultMemoize(projectionFn, isEqualCheck, isResultEqual); } function defaultMemoize(projectionFn, isArgumentsEqual = isEqualCheck, isResultEqual = isEqualCheck) { let lastArguments = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any, , , , , let lastResult = null; let overrideResult; function reset() { lastArguments = null; lastResult = null; } function setResult(result = undefined) { overrideResult = { result }; } function clearResult() { overrideResult = undefined; } /* eslint-disable prefer-rest-params, prefer-spread */ // disabled because of the use of `arguments` function memoized() { if (overrideResult !== undefined) { return overrideResult.result; } if (!lastArguments) { lastResult = projectionFn.apply(null, arguments); lastArguments = arguments; return lastResult; } if (!isArgumentsChanged(arguments, lastArguments, isArgumentsEqual)) { return lastResult; } const newResult = projectionFn.apply(null, arguments); lastArguments = arguments; if (isResultEqual(lastResult, newResult)) { return lastResult; } lastResult = newResult; return newResult; } return { memoized, reset, setResult, clearResult }; } function createSelector(...input) { return createSelectorFactory(defaultMemoize)(...input); } function defaultStateFn(state, selectors, props, memoizedProjector) { if (props === undefined) { const args = selectors.map((fn) => fn(state)); return memoizedProjector.memoized.apply(null, args); } const args = selectors.map((fn) => fn(state, props)); return memoizedProjector.memoized.apply(null, [...args, props]); } /** * * @param memoize The function used to memoize selectors * @param options Config Object that may include a `stateFn` function defining how to return the selector's value, given the entire `Store`'s state, parent `Selector`s, `Props`, and a `MemoizedProjection` * * @usageNotes * * **Creating a Selector Factory Where Array Order Does Not Matter** * * ```ts * function removeMatch(arr: string[], target: string): string[] { * const matchIndex = arr.indexOf(target); * return [...arr.slice(0, matchIndex), ...arr.slice(matchIndex + 1)]; * } * * function orderDoesNotMatterComparer(a: any, b: any): boolean { * if (!Array.isArray(a) || !Array.isArray(b)) { * return a === b; * } * if (a.length !== b.length) { * return false; * } * let tempB = [...b]; * function reduceToDetermineIfArraysContainSameContents( * previousCallResult: boolean, * arrayMember: any * ): boolean { * if (previousCallResult === false) { * return false; * } * if (tempB.includes(arrayMember)) { * tempB = removeMatch(tempB, arrayMember); * return true; * } * return false; * } * return a.reduce(reduceToDetermineIfArraysContainSameContents, true); * } * * export const createOrderDoesNotMatterSelector = createSelectorFactory( * (projectionFun) => defaultMemoize( * projectionFun, * orderDoesNotMatterComparer, * orderDoesNotMatterComparer * ) * ); * ``` * * **Creating an Alternative Memoization Strategy** * * ```ts * function serialize(x: any): string { * return JSON.stringify(x); * } * * export const createFullHistorySelector = createSelectorFactory( * (projectionFunction) => { * const cache = {}; * * function memoized() { * const serializedArguments = serialize(...arguments); * if (cache[serializedArguments] != null) { * cache[serializedArguments] = projectionFunction.apply(null, arguments); * } * return cache[serializedArguments]; * } * return { * memoized, * reset: () => {}, * setResult: () => {}, * clearResult: () => {}, * }; * } * ); * ``` */ function createSelectorFactory(memoize, options = { stateFn: defaultStateFn, }) { return function (...input) { let args = input; if (Array.isArray(args[0])) { const [head, ...tail] = args; args = [...head, ...tail]; } else if (args.length === 1 && isSelectorsDictionary(args[0])) { args = extractArgsFromSelectorsDictionary(args[0]); } const selectors = args.slice(0, args.length - 1); const projector = args[args.length - 1]; const memoizedSelectors = selectors.filter((selector) => selector.release && typeof selector.release === 'function'); const memoizedProjector = memoize(function (...selectors) { return projector.apply(null, selectors); }); const memoizedState = defaultMemoize(function (state, props) { return options.stateFn.apply(null, [ state, selectors, props, memoizedProjector, ]); }); function release() { memoizedState.reset(); memoizedProjector.reset(); memoizedSelectors.forEach((selector) => selector.release()); } return Object.assign(memoizedState.memoized, { release, projector: memoizedProjector.memoized, setResult: memoizedState.setResult, clearResult: memoizedState.clearResult, }); }; } function createFeatureSelector(featureName) { return createSelector((state) => { const featureState = state[featureName]; if (!isNgrxMockEnvironment() && isDevMode() && !(featureName in state)) { console.warn(`@ngrx/store: The feature name "${featureName}" does ` + 'not exist in the state, therefore createFeatureSelector ' + 'cannot access it. Be sure it is imported in a loaded module ' + `using StoreModule.forRoot('${featureName}', ...) or ` + `StoreModule.forFeature('${featureName}', ...). If the default ` + 'state is intended to be undefined, as is the case with router ' + 'state, this development-only warning message can be ignored.'); } return featureState; }, (featureState) => featureState); } function isSelectorsDictionary(selectors) { return (!!selectors && typeof selectors === 'object' && Object.values(selectors).every((selector) => typeof selector === 'function')); } function extractArgsFromSelectorsDictionary(selectorsDictionary) { const selectors = Object.values(selectorsDictionary); const resultKeys = Object.keys(selectorsDictionary); const projector = (...selectorResults) => resultKeys.reduce((result, key, index) => ({ ...result, [key]: selectorResults[index], }), {}); return [...selectors, projector]; } /** * @description * A function that accepts a feature name and a feature reducer, and creates * a feature selector and a selector for each feature state property. * This function also provides the ability to add extra selectors to * the feature object. * * @param featureConfig An object that contains a feature name and a feature * reducer as required, and extra selectors factory as an optional argument. * @returns An object that contains a feature name, a feature reducer, * a feature selector, a selector for each feature state property, and extra * selectors. * * @usageNotes * * ```ts * interface ProductsState { * products: Product[]; * selectedId: string | null; * } * * const initialState: ProductsState = { * products: [], * selectedId: null, * }; * * const productsFeature = createFeature({ * name: 'products', * reducer: createReducer( * initialState, * on(ProductsApiActions.loadSuccess(state, { products }) => ({ * ...state, * products, * }), * ), * }); * * const { * name, * reducer, * // feature selector * selectProductsState, // type: MemoizedSelector<Record<string, any>, ProductsState> * // feature state properties selectors * selectProducts, // type: MemoizedSelector<Record<string, any>, Product[]> * selectSelectedId, // type: MemoizedSelector<Record<string, any>, string | null> * } = productsFeature; * ``` * * **Creating Feature with Extra Selectors** * * ```ts * type CallState = 'init' | 'loading' | 'loaded' | { error: string }; * * interface State extends EntityState<Product> { * callState: CallState; * } * * const adapter = createEntityAdapter<Product>(); * const initialState: State = adapter.getInitialState({ * callState: 'init', * }); * * export const productsFeature = createFeature({ * name: 'products', * reducer: createReducer(initialState), * extraSelectors: ({ selectProductsState, selectCallState }) => ({ * ...adapter.getSelectors(selectProductsState), * ...getCallStateSelectors(selectCallState) * }), * }); * * const { * name, * reducer, * // feature selector * selectProductsState, * // feature state properties selectors * selectIds, * selectEntities, * selectCallState, * // selectors returned by `adapter.getSelectors` * selectAll, * selectTotal, * // selectors returned by `getCallStateSelectors` * selectIsLoading, * selectIsLoaded, * selectError, * } = productsFeature; * ``` */ function createFeature(featureConfig) { const { name, reducer, extraSelectors: extraSelectorsFactory, } = featureConfig; const featureSelector = createFeatureSelector(name); const nestedSelectors = createNestedSelectors(featureSelector, reducer); const baseSelectors = { [`select${capitalize(name)}State`]: featureSelector, ...nestedSelectors, }; const extraSelectors = extraSelectorsFactory ? extraSelectorsFactory(baseSelectors) : {}; return { name, reducer, ...baseSelectors, ...extraSelectors, }; } function createNestedSelectors(featureSelector, reducer) { const initialState = getInitialState(reducer); const nestedKeys = (isPlainObject(initialState) ? Object.keys(initialState) : []); return nestedKeys.reduce((nestedSelectors, nestedKey) => ({ ...nestedSelectors, [`select${capitalize(nestedKey)}`]: createSelector(featureSelector, (parentState) => parentState?.[nestedKey]), }), {}); } function getInitialState(reducer) { return reducer(undefined, { type: '@ngrx/feature/init' }); } function _createStoreReducers(reducers) { return reducers instanceof InjectionToken ? inject(reducers) : reducers; } function _createFeatureStore(configs, featureStores) { return featureStores.map((feat, index) => { if (configs[index] instanceof InjectionToken) { const conf = inject(configs[index]); return { key: feat.key, reducerFactory: conf.reducerFactory ? conf.reducerFactory : combineReducers, metaReducers: conf.metaReducers ? conf.metaReducers : [], initialState: conf.initialState, }; } return feat; }); } function _createFeatureReducers(reducerCollection) { return reducerCollection.map((reducer) => { return reducer instanceof InjectionToken ? inject(reducer) : reducer; }); } function _initialStateFactory(initialState) { if (typeof initialState === 'function') { return initialState(); } return initialState; } function _concatMetaReducers(metaReducers, userProvidedMetaReducers) { return metaReducers.concat(userProvidedMetaReducers); } function _provideForRootGuard() { const store = inject(Store, { optional: true, skipSelf: true }); if (store) { throw new TypeError(`The root Store has been provided more than once. Feature modules should provide feature states instead.`); } return 'guarded'; } function immutabilityCheckMetaReducer(reducer, checks) { return function (state, action) { const act = checks.action(action) ? freeze(action) : action; const nextState = reducer(state, act); return checks.state() ? freeze(nextState) : nextState; }; } function freeze(target) { Object.freeze(target); const targetIsFunction = isFunction(target); Object.getOwnPropertyNames(target).forEach((prop) => { // Ignore Ivy properties, ref: https://github.com/ngrx/platform/issues/2109#issuecomment-582689060 if (prop.startsWith('ɵ')) { return; } if (hasOwnProperty(target, prop) && (targetIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true)) { const propValue = target[prop]; if ((isObjectLike(propValue) || isFunction(propValue)) && !Object.isFrozen(propValue)) { freeze(propValue); } } }); return target; } function serializationCheckMetaReducer(reducer, checks) { return function (state, action) { if (checks.action(action)) { const unserializableAction = getUnserializable(action); throwIfUnserializable(unserializableAction, 'action'); } const nextState = reducer(state, action); if (checks.state()) { const unserializableState = getUnserializable(nextState); throwIfUnserializable(unserializableState, 'state'); } return nextState; }; } function getUnserializable(target, path = []) { // Guard against undefined and null, e.g. a reducer that returns undefined if ((isUndefined(target) || isNull(target)) && path.length === 0) { return { path: ['root'], value: target, }; } const keys = Object.keys(target); return keys.reduce((result, key) => { if (result) { return result; } const value = target[key]; // Ignore Ivy components if (isComponent(value)) { return result; } if (isUndefined(value) || isNull(value) || isNumber(value) || isBoolean(value) || isString(value) || isArray(value)) { return false; } if (isPlainObject(value)) { return getUnserializable(value, [...path, key]); } return { path: [...path, key], value, }; }, false); } function throwIfUnserializable(unserializable, context) { if (unserializable === false) { return; } const unserializablePath = unserializable.path.join('.'); const error = new Error(`Detected unserializable ${context} at "${unserializablePath}". ${RUNTIME_CHECK_URL}#strict${context}serializability`); error.value = unserializable.value; error.unserializablePath = unserializablePath; throw error; } function inNgZoneAssertMetaReducer(reducer, checks) { return function (state, action) { if (checks.action(action) && !i0.NgZone.isInAngularZone()) { throw new Error(`Action '${action.type}' running outside NgZone. ${RUNTIME_CHECK_URL}#strictactionwithinngzone`); } return reducer(state, action); }; } function createActiveRuntimeChecks(runtimeChecks) { if (isDevMode()) { return { strictStateSerializability: false, strictActionSerializability: false, strictStateImmutability: true, strictActionImmutability: true, strictActionWithinNgZone: false, strictActionTypeUniqueness: false, ...runtimeChecks, }; } return { strictStateSerializability: false, strictActionSerializability: false, strictStateImmutability: false, strictActionImmutability: false, strictActionWithinNgZone: false, strictActionTypeUniqueness: false, }; } function createSerializationCheckMetaReducer({ strictActionSerializability, strictStateSerializability, }) { return (reducer) => strictActionSerializability || strictStateSerializability ? serializationCheckMetaReducer(reducer, { action: (action) => strictActionSerializability && !ignoreNgrxAction(action), state: () => strictStateSerializability, }) : reducer; } function createImmutabilityCheckMetaReducer({ strictActionImmutability, strictStateImmutability, }) { return (reducer) => strictActionImmutability || strictStateImmutability ? immutabilityCheckMetaReducer(reducer, { action: (action) => strictActionImmutability && !ignoreNgrxAction(action), state: () => strictStateImmutability, }) : reducer; } function ignoreNgrxAction(action) { return action.type.startsWith('@ngrx'); } function createInNgZoneCheckMetaReducer({ strictActionWithinNgZone, }) { return (reducer) => strictActionWithinNgZone ? inNgZoneAssertMetaReducer(reducer, { action: (action) => strictActionWithinNgZone && !ignoreNgrxAction(action), }) : reducer; } function provideRuntimeChecks(runtimeChecks) { return [ { provide: _USER_RUNTIME_CHECKS, useValue: runtimeChecks, }, { provide: USER_RUNTIME_CHECKS, useFactory: _runtimeChecksFactory, deps: [_USER_RUNTIME_CHECKS], }, { provide: ACTIVE_RUNTIME_CHECKS, deps: [USER_RUNTIME_CHECKS], useFactory: createActiveRuntimeChecks, }, { provide: META_REDUCERS, multi: true, deps: [ACTIVE_RUNTIME_CHECKS], useFactory: createImmutabilityCheckMetaReducer, }, { provide: META_REDUCERS, multi: true, deps: [ACTIVE_RUNTIME_CHECKS], useFactory: createSerializationCheckMetaReducer, }, { provide: META_REDUCERS, multi: true, deps: [ACTIVE_RUNTIME_CHECKS], useFactory: createInNgZoneCheckMetaReducer, }, ]; } function checkForActionTypeUniqueness() { return [ { provide: _ACTION_TYPE_UNIQUENESS_CHECK, multi: true, deps: [ACTIVE_RUNTIME_CHECKS], useFactory: _actionTypeUniquenessCheck, }, ]; } function _runtimeChecksFactory(runtimeChecks) { return runtimeChecks; } function _actionTypeUniquenessCheck(config) { if (!config.strictActionTypeUniqueness) { return; } const duplicates = Object.entries(REGISTERED_ACTION_TYPES) .filter(([, registrations]) => registrations > 1) .map(([type]) => type); if (duplicates.length) { throw new Error(`Action types are registered more than once, ${duplicates .map((type) => `"${type}"`) .join(', ')}. ${RUNTIME_CHECK_URL}#strictactiontypeuniqueness`); } } /** * Provides additional slices of state in the Store. * These providers cannot be used at the component level. * * @usageNotes * * ### Providing Store Features * * ```ts * const booksRoutes: Route[] = [ * { * path: '', * providers: [provideState('books', booksReducer)], * children: [ * { path: '', component: BookListComponent }, * { path: ':id', component: BookDetailsComponent }, * ], * }, * ]; * ``` */ function provideState(featureNameOrSlice, reducers, config = {}) { return makeEnvironmentProviders([ ..._provideState(featureNameOrSlice, reducers, config), ENVIRONMENT_STATE_PROVIDER, ]); } function _provideStore(reducers = {}, config = {}) { return [ { provide: _ROOT_STORE_GUARD, useFactory: _provideForRootGuard, }, { provide: _INITIAL_STATE, useValue: config.initialState }, { provide: INITIAL_STATE, useFactory: _initialStateFactory, deps: [_INITIAL_STATE], }, { provide: _INITIAL_REDUCERS, useValue: reducers }, { provide: _STORE_REDUCERS, useExisting: reducers instanceof InjectionToken ? reducers : _INITIAL_REDUCERS, }, { provide: INITIAL_REDUCERS, deps: [_INITIAL_REDUCERS, [new Inject(_STORE_REDUCERS)]], useFactory: _createStoreReducers, }, { provide: USER_PROVIDED_META_REDUCERS, useValue: config.metaReducers ? config.metaReducers : [], }, { provide: _RESOLVED_META_REDUCERS, deps: [META_REDUCERS, USER_PROVIDED_META_REDUCERS], useFactory: _concatMetaReducers, }, { provide: _REDUCER_FACTORY, useValue: config.reducerFactory ? config.reducerFactory : combineReducers, }, { provide: REDUCER_FACTORY, deps: [_REDUCER_FACTORY, _RESOLVED_META_REDUCERS], useFactory: createReducerFactory, }, ACTIONS_SUBJECT_PROVIDERS, REDUCER_MANAGER_PROVIDERS, SCANNED_ACTIONS_SUBJECT_PROVIDERS, STATE_PROVIDERS, STORE_PROVIDERS, provideRuntimeChecks(config.runtimeChecks), checkForActionTypeUniqueness(), ]; } function rootStoreProviderFactory() { inject(ActionsSubject); inject(ReducerObservable); inject(ScannedActionsSubject); inject(Store); inject(_ROOT_STORE_GUARD, { optional: true }); inject(_ACTION_TYPE_UNIQUENESS_CHECK, { optional: true }); } /** * Environment Initializer used in the root * providers to initialize the Store */ const ENVIRONMENT_STORE_PROVIDER = [ { provide: ROOT_STORE_PROVIDER, useFactory: rootStoreProviderFactory }, provideEnvironmentInitializer(() => inject(ROOT_STORE_PROVIDER)), ]; /** * Provides the global Store providers and initializes * the Store. * These providers cannot be used at the component level. * * @usageNotes * * ### Providing the Global Store * * ```ts * bootstrapApplication(AppComponent, { * providers: [provideStore()], * }); * ``` */ function provideStore(reducers, config) { return makeEnvironmentProviders([ ..._provideStore(reducers, config), ENVIRONMENT_STORE_PROVIDER, ]); } function featureStateProviderFactory() { inject(ROOT_STORE_PROVIDER); const features = inject(_STORE_FEATURES); const featureReducers = inject(FEATURE_REDUCERS); const reducerManager = inject(ReducerManager); inject(_ACTION_TYPE_UNIQUENESS_CHECK, { optional: true }); const feats = features.map((feature, index) => { const featureReducerCollection = featureReducers.shift(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const reducers = featureReducerCollection /*TODO(#823)*/[index]; return { ...feature, reducers, initialState: _initialStateFactory(feature.initialState), }; }); reducerManager.addFeatures(feats); } /** * Environment Initializer used in the feature * providers to register state features */ const ENVIRONMENT_STATE_PROVIDER = [ { provide: FEATURE_STATE_PROVIDER, useFactory: featureStateProviderFactory, }, provideEnvironmentInitializer(() => inject(FEATURE_STATE_PROVIDER)), ]; function _provideState(featureNameOrSlice, reducers, config = {}) { return [ { provide: _FEATURE_CONFIGS, multi: true, useValue: featureNameOrSlice instanceof Object ? {} : config, }, { provide: STORE_FEATURES, multi: true, useValue: { key: featureNameOrSlice instanceof Object ? featureNameOrSlice.name : featureNameOrSlice, reducerFactory: !(config instanceof InjectionToken) && config.reducerFactory ? config.reducerFactory : combineReducers, metaReducers: !(config instanceof InjectionToken) && config.metaReducers ? config.metaReducers : [], initialState: !(config instanceof InjectionToken) && config.initialState ? config.initialState : undefined, }, }, { provide: _STORE_FEATURES, deps: [_FEATURE_CONFIGS, STORE_FEATURES], useFactory: _createFeatureStore, }, { provide: _FEATURE_REDUCERS, multi: true, useValue: featureNameOrSlice instanceof Object ? featureNameOrSlice.reducer : reducers, }, { provide: _FEATURE_REDUCERS_TOKEN, multi: true, useExisting: reducers instanceof InjectionToken ? reducers : _FEATURE_REDUCERS, }, { provide: FEATURE_REDUCERS, mul