UNPKG

@ngxs/store

Version:
1,174 lines (1,151 loc) 105 kB
import * as i0 from '@angular/core'; import { inject, Injectable, DestroyRef, NgZone, Injector, runInInjectionContext, InjectionToken, ErrorHandler, ɵisPromise as _isPromise, computed, makeEnvironmentProviders, provideEnvironmentInitializer, NgModule, APP_BOOTSTRAP_LISTENER, ApplicationRef, PendingTasks, assertInInjectionContext, EnvironmentInjector, createEnvironmentInjector } from '@angular/core'; import { config, Observable, Subject, of, forkJoin, map, shareReplay, filter, take, mergeMap, EMPTY, from, isObservable, defaultIfEmpty, takeUntil, finalize, catchError, distinctUntilChanged, startWith, skip, buffer, debounceTime } from 'rxjs'; import { ɵwrapObserverCalls as _wrapObserverCalls, ɵOrderedSubject as _OrderedSubject, ɵStateStream as _StateStream, ɵhasOwnProperty as _hasOwnProperty, ɵmemoize as _memoize, ɵgetStoreMetadata as _getStoreMetadata, ɵgetSelectorMetadata as _getSelectorMetadata, ɵMETA_KEY as _META_KEY, ɵINITIAL_STATE_TOKEN as _INITIAL_STATE_TOKEN, ɵNgxsActionRegistry as _NgxsActionRegistry, ɵNgxsAppBootstrappedState as _NgxsAppBootstrappedState, ɵensureStoreMetadata as _ensureStoreMetadata, ɵMETA_OPTIONS_KEY as _META_OPTIONS_KEY, ɵensureSelectorMetadata as _ensureSelectorMetadata, ɵNGXS_STATE_CONTEXT_FACTORY as _NGXS_STATE_CONTEXT_FACTORY, ɵNGXS_STATE_FACTORY as _NGXS_STATE_FACTORY } from '@ngxs/store/internals'; export { StateToken } from '@ngxs/store/internals'; import { NGXS_PLUGINS, getActionTypeFromInstance, InitState, UpdateState, setValue, getValue, ɵisPluginClass as _isPluginClass } from '@ngxs/store/plugins'; export { InitState, NGXS_PLUGINS, UpdateState, actionMatcher, getActionTypeFromInstance, getValue, setValue } from '@ngxs/store/plugins'; import { isStateOperator } from '@ngxs/store/operators'; class PluginManager { plugins = []; _parentManager = inject(PluginManager, { optional: true, skipSelf: true }); _pluginHandlers = inject(NGXS_PLUGINS, { optional: true }); constructor() { this.registerHandlers(); } get _rootPlugins() { return this._parentManager?.plugins || this.plugins; } registerHandlers() { const pluginHandlers = this.getPluginHandlers(); this._rootPlugins.push(...pluginHandlers); } getPluginHandlers() { const handlers = this._pluginHandlers || []; return handlers.map((plugin) => (plugin.handle ? plugin.handle.bind(plugin) : plugin)); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: PluginManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: PluginManager, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: PluginManager, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * Returns operator that will run * `subscribe` outside of the ngxs execution context */ function leaveNgxs(ngxsExecutionStrategy) { return _wrapObserverCalls(fn => ngxsExecutionStrategy.leave(fn)); } const ɵɵunhandledRxjsErrorCallbacks = new WeakMap(); let installed = false; function installOnUnhandhedErrorHandler() { if (installed) { return; } const existingHandler = config.onUnhandledError; config.onUnhandledError = function (error) { const unhandledErrorCallback = ɵɵunhandledRxjsErrorCallbacks.get(error); if (unhandledErrorCallback) { unhandledErrorCallback(); } else if (existingHandler) { existingHandler.call(this, error); } else { throw error; } }; installed = true; } function executeUnhandledCallback(error) { const unhandledErrorCallback = ɵɵunhandledRxjsErrorCallbacks.get(error); if (unhandledErrorCallback) { unhandledErrorCallback(); return true; } return false; } function assignUnhandledCallback(error, callback) { // Since the error can be essentially anything, we must ensure that we only // handle objects, as weak maps do not allow any other key type besides objects. // The error can also be a string if thrown in the following manner: `throwError('My Error')`. if (error && typeof error === 'object') { let hasBeenCalled = false; ɵɵunhandledRxjsErrorCallbacks.set(error, () => { if (!hasBeenCalled) { hasBeenCalled = true; callback(); } }); } return error; } function fallbackSubscriber(ngZone) { return (source) => { let subscription = source.subscribe({ error: error => { ngZone.runOutsideAngular(() => { // This is necessary to schedule a microtask to ensure that synchronous // errors are not reported before the real subscriber arrives. If an error // is thrown synchronously in any action, it will be reported to the error // handler regardless. Since RxJS reports unhandled errors asynchronously, // implementing a microtask ensures that we are also safe in this scenario. queueMicrotask(() => { if (subscription) { executeUnhandledCallback(error); } }); }); } }); return new Observable(subscriber => { // Now that there is a real subscriber, we can unsubscribe our pro-active subscription subscription?.unsubscribe(); subscription = null; return source.subscribe(subscriber); }); }; } /** * Internal Action result stream that is emitted when an action is completed. * This is used as a method of returning the action result to the dispatcher * for the observable returned by the dispatch(...) call. * The dispatcher then asynchronously pushes the result from this stream onto the main action stream as a result. */ class InternalDispatchedActionResults extends Subject { constructor() { super(); // Complete the subject once the root injector is destroyed to ensure // there are no active subscribers that would receive events or perform // any actions after the application is destroyed. inject(DestroyRef).onDestroy(() => this.complete()); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalDispatchedActionResults, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalDispatchedActionResults, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalDispatchedActionResults, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class InternalNgxsExecutionStrategy { _ngZone = inject(NgZone); enter(func) { if (typeof ngServerMode !== 'undefined' && ngServerMode) { return this._runInsideAngular(func); } return this._runOutsideAngular(func); } leave(func) { return this._runInsideAngular(func); } _runInsideAngular(func) { if (NgZone.isInAngularZone()) { return func(); } return this._ngZone.run(func); } _runOutsideAngular(func) { if (NgZone.isInAngularZone()) { return this._ngZone.runOutsideAngular(func); } return func(); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalNgxsExecutionStrategy, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalNgxsExecutionStrategy, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalNgxsExecutionStrategy, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Status of a dispatched action */ var ActionStatus; (function (ActionStatus) { ActionStatus["Dispatched"] = "DISPATCHED"; ActionStatus["Successful"] = "SUCCESSFUL"; ActionStatus["Canceled"] = "CANCELED"; ActionStatus["Errored"] = "ERRORED"; })(ActionStatus || (ActionStatus = {})); /** * Internal Action stream that is emitted anytime an action is dispatched. */ class InternalActions extends _OrderedSubject { // This subject will be the first to know about the dispatched action, its purpose is for // any logic that must be executed before action handlers are invoked (i.e., cancelation). dispatched$ = new Subject(); constructor() { super(); this.subscribe(ctx => { if (ctx.status === ActionStatus.Dispatched) { this.dispatched$.next(ctx); } }); const destroyRef = inject(DestroyRef); destroyRef.onDestroy(() => { // Complete the subject once the root injector is destroyed to ensure // there are no active subscribers that would receive events or perform // any actions after the application is destroyed. this.complete(); this.dispatched$.complete(); }); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalActions, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalActions, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalActions, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); /** * Action stream that is emitted anytime an action is dispatched. * * You can listen to this in services to react without stores. */ class Actions extends Observable { constructor() { const internalActions$ = inject(InternalActions); const internalExecutionStrategy = inject(InternalNgxsExecutionStrategy); // The `InternalActions` subject emits outside of the Angular zone. // We have to re-enter the Angular zone for any incoming consumer. // The shared `Subject` reduces the number of change detections. // This would call leave only once for any stream emission across all active subscribers. const sharedInternalActions$ = new Subject(); internalActions$ .pipe(leaveNgxs(internalExecutionStrategy)) .subscribe(sharedInternalActions$); super(observer => { const childSubscription = sharedInternalActions$.subscribe({ next: ctx => observer.next(ctx), error: error => observer.error(error), complete: () => observer.complete() }); observer.add(childSubscription); }); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: Actions, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: Actions, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: Actions, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [] }); class InternalDispatcher { _ngZone = inject(NgZone); _actions = inject(InternalActions); _actionResults = inject(InternalDispatchedActionResults); _pluginManager = inject(PluginManager); _stateStream = inject(_StateStream); _ngxsExecutionStrategy = inject(InternalNgxsExecutionStrategy); _injector = inject(Injector); /** * Dispatches event(s). */ dispatch(actionOrActions) { const result = this._ngxsExecutionStrategy.enter(() => this.dispatchByEvents(actionOrActions)); return result.pipe(fallbackSubscriber(this._ngZone), leaveNgxs(this._ngxsExecutionStrategy)); } dispatchByEvents(actionOrActions) { if (Array.isArray(actionOrActions)) { if (actionOrActions.length === 0) return of(undefined); return forkJoin(actionOrActions.map(action => this.dispatchSingle(action))).pipe(map(() => undefined)); } else { return this.dispatchSingle(actionOrActions); } } dispatchSingle(action) { if (typeof ngDevMode !== 'undefined' && ngDevMode) { const type = getActionTypeFromInstance(action); if (!type) { const error = new Error(`This action doesn't have a type property: ${action.constructor.name}`); return new Observable(subscriber => subscriber.error(error)); } } const prevState = this._stateStream.getValue(); const plugins = this._pluginManager.plugins; return compose(this._injector, [ ...plugins, (nextState, nextAction) => { if (nextState !== prevState) { this._stateStream.next(nextState); } const actionResult$ = this.getActionResultStream(nextAction); actionResult$.subscribe(ctx => this._actions.next(ctx)); this._actions.next({ action: nextAction, status: ActionStatus.Dispatched }); return this.createDispatchObservable(actionResult$); } ])(prevState, action).pipe(shareReplay()); } getActionResultStream(action) { return this._actionResults.pipe(filter((ctx) => ctx.action === action && ctx.status !== ActionStatus.Dispatched), take(1), shareReplay()); } createDispatchObservable(actionResult$) { return actionResult$.pipe(mergeMap((ctx) => { switch (ctx.status) { case ActionStatus.Successful: // The `createDispatchObservable` function should return the // state, as its result is used by plugins. return of(this._stateStream.getValue()); case ActionStatus.Errored: throw ctx.error; default: // Once dispatched or canceled, we complete it immediately because // `dispatch()` should emit (or error, or complete) as soon as it succeeds or fails. return EMPTY; } }), shareReplay()); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalDispatcher, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalDispatcher, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * Composes a array of functions from left to right. Example: * * compose([fn, final])(state, action); * * then the funcs have a signature like: * * function fn (state, action, next) { * console.log('here', state, action, next); * return next(state, action); * } * * function final (state, action) { * console.log('here', state, action); * return state; * } * * the last function should not call `next`. */ const compose = (injector, funcs) => (...args) => { const curr = funcs.shift(); return runInInjectionContext(injector, () => curr(...args, (...nextArgs) => compose(injector, funcs)(...nextArgs))); }; // The injection token is used to resolve a list of states provided at // the root level through either `NgxsModule.forRoot` or `provideStore`. const ROOT_STATE_TOKEN = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'ROOT_STATE_TOKEN' : ''); // The injection token is used to resolve a list of states provided at // the feature level through either `NgxsModule.forFeature` or `provideStates`. // The Array<Array> is used to overload the resolved value of the token because // it is a multi-provider token. const FEATURE_STATE_TOKEN = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'FEATURE_STATE_TOKEN' : ''); // The injection token is used to resolve to options provided at the root // level through either `NgxsModule.forRoot` or `provideStore`. const NGXS_OPTIONS = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_OPTIONS' : ''); /** * The NGXS config settings. */ class NgxsConfig { /** * Run in development mode. This will add additional debugging features: * - Object.freeze on the state and actions to guarantee immutability * (default: false) * * Note: this property will be accounted only in development mode. * It makes sense to use it only during development to ensure there're no state mutations. * When building for production, the `Object.freeze` will be tree-shaken away. */ developmentMode; compatibility = { strictContentSecurityPolicy: false }; /** * Defining shared selector options */ selectorOptions = { injectContainerState: false, suppressErrors: false }; /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsConfig, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsConfig, providedIn: 'root', useFactory: () => { const defaultConfig = new NgxsConfig(); const config = inject(NGXS_OPTIONS); return { ...defaultConfig, ...config, selectorOptions: { ...defaultConfig.selectorOptions, ...config.selectorOptions } }; } }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsConfig, decorators: [{ type: Injectable, args: [{ providedIn: 'root', useFactory: () => { const defaultConfig = new NgxsConfig(); const config = inject(NGXS_OPTIONS); return { ...defaultConfig, ...config, selectorOptions: { ...defaultConfig.selectorOptions, ...config.selectorOptions } }; } }] }] }); /** * Represents a basic change from a previous to a new value for a single state instance. * Passed as a value in a NgxsSimpleChanges object to the ngxsOnChanges hook. */ class NgxsSimpleChange { previousValue; currentValue; firstChange; constructor(previousValue, currentValue, firstChange) { this.previousValue = previousValue; this.currentValue = currentValue; this.firstChange = firstChange; } } /** * Object freeze code * https://github.com/jsdf/deep-freeze */ const deepFreeze = (o) => { Object.freeze(o); const oIsFunction = typeof o === 'function'; Object.getOwnPropertyNames(o).forEach(function (prop) { if (_hasOwnProperty(o, prop) && (oIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true) && o[prop] !== null && (typeof o[prop] === 'object' || typeof o[prop] === 'function') && !Object.isFrozen(o[prop])) { deepFreeze(o[prop]); } }); return o; }; /** * @ignore */ class InternalStateOperations { _stateStream = inject(_StateStream); _dispatcher = inject(InternalDispatcher); _config = inject(NgxsConfig); /** * Returns the root state operators. */ getRootStateOperations() { const rootStateOperations = { getState: () => this._stateStream.getValue(), setState: (newState) => this._stateStream.next(newState), dispatch: (actionOrActions) => this._dispatcher.dispatch(actionOrActions) }; if (typeof ngDevMode !== 'undefined' && ngDevMode) { return this._config.developmentMode ? ensureStateAndActionsAreImmutable(rootStateOperations) : rootStateOperations; } else { return rootStateOperations; } } setStateToTheCurrentWithNew(results) { const stateOperations = this.getRootStateOperations(); // Get our current stream const currentState = stateOperations.getState(); // Set the state to the current + new stateOperations.setState({ ...currentState, ...results.defaults }); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalStateOperations, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalStateOperations, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalStateOperations, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); function ensureStateAndActionsAreImmutable(root) { return { getState: () => root.getState(), setState: value => { const frozenValue = deepFreeze(value); return root.setState(frozenValue); }, dispatch: actions => { return root.dispatch(actions); } }; } function createRootSelectorFactory(selectorMetaData, selectors, memoizedSelectorFn) { return (context) => { const { argumentSelectorFunctions, selectorOptions } = getRuntimeSelectorInfo(context, selectorMetaData, selectors); const { suppressErrors } = selectorOptions; return function selectFromRoot(rootState) { // Determine arguments from the app state using the selectors const results = argumentSelectorFunctions.map(argFn => argFn(rootState)); // If the lambda attempts to access something in the state that doesn't exist, // it will throw a `TypeError`. Since this behavior is common, we simply return // `undefined` in such cases. try { return memoizedSelectorFn(...results); } catch (ex) { if (suppressErrors && ex instanceof TypeError) { return undefined; } // We're logging an error in this function because it may be used by `select`, // `selectSignal`, and `selectSnapshot`. Therefore, there's no need to catch // exceptions there to log errors. if (typeof ngDevMode !== 'undefined' && ngDevMode) { const message = 'The selector below has thrown an error upon invocation. ' + 'Please check for any unsafe property access that may result in null ' + 'or undefined values.'; // Avoid concatenating the message with the original function, as this will // invoke `toString()` on the function. Instead, log it as the second argument. // This way, developers will be able to navigate to the actual code in the browser. console.error(message, selectorMetaData.originalFn); } throw ex; } }; }; } function createMemoizedSelectorFn(originalFn, creationMetadata) { const containerClass = creationMetadata?.containerClass; const wrappedFn = function wrappedSelectorFn() { // eslint-disable-next-line prefer-rest-params const returnValue = originalFn.apply(containerClass, arguments); if (typeof returnValue === 'function') { const innerMemoizedFn = _memoize.apply(null, [returnValue]); return innerMemoizedFn; } return returnValue; }; const memoizedFn = _memoize(wrappedFn); Object.setPrototypeOf(memoizedFn, originalFn); return memoizedFn; } function getRuntimeSelectorInfo(context, selectorMetaData, selectors = []) { const localSelectorOptions = selectorMetaData.getSelectorOptions(); const selectorOptions = context.getSelectorOptions(localSelectorOptions); const selectorsToApply = getSelectorsToApply(selectors, selectorOptions, selectorMetaData.containerClass); const argumentSelectorFunctions = selectorsToApply.map(selector => { const factory = getRootSelectorFactory(selector); return factory(context); }); return { selectorOptions, argumentSelectorFunctions }; } function getSelectorsToApply(selectors = [], selectorOptions, containerClass) { const selectorsToApply = []; // The container state refers to the state class that includes the // definition of the selector function, for example: // @State() // class AnimalsState { // @Selector() // static getAnimals(state: AnimalsStateModel) {} // } // The `AnimalsState` serves as the container state. Additionally, the // selector may reside within a namespace or another class lacking the // `@State` decorator, thus not being treated as the container state. const canInjectContainerState = selectorOptions.injectContainerState || selectors.length === 0; if (containerClass && canInjectContainerState) { // If we are on a state class, add it as the first selector parameter const metadata = _getStoreMetadata(containerClass); if (metadata) { selectorsToApply.push(containerClass); } } selectorsToApply.push(...selectors); return selectorsToApply; } /** * This function gets the factory function to create the selector to get the selected slice from the app state * @ignore */ function getRootSelectorFactory(selector) { const metadata = _getSelectorMetadata(selector) || _getStoreMetadata(selector); return metadata?.makeRootSelector || (() => selector); } /** * Get a deeply nested value. Example: * * getValue({ foo: bar: [] }, 'foo.bar') //=> [] * * Note: This is not as fast as the `fastPropGetter` but is strict Content Security Policy compliant. * See perf hit: https://jsperf.com/fast-value-getter-given-path/1 * * @ignore */ function compliantPropGetter(paths) { return obj => { for (let i = 0; i < paths.length; i++) { if (!obj) return undefined; obj = obj[paths[i]]; } return obj; }; } /** * The generated function is faster than: * - pluck (Observable operator) * - memoize * * @ignore */ function fastPropGetter(paths) { const segments = paths; let seg = 'store.' + segments[0]; let i = 0; const l = segments.length; let expr = seg; while (++i < l) { expr = expr + ' && ' + (seg = seg + '.' + segments[i]); } const fn = new Function('store', 'return ' + expr + ';'); return fn; } const ɵPROP_GETTER = new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'PROP_GETTER' : '', { providedIn: 'root', factory: () => inject(NgxsConfig).compatibility?.strictContentSecurityPolicy ? compliantPropGetter : fastPropGetter }); /** * Given an array of states, it will return a object graph. Example: * const states = [ * Cart, * CartSaved, * CartSavedItems * ] * * would return: * * const graph = { * cart: ['saved'], * saved: ['items'], * items: [] * }; * * @ignore */ function buildGraph(stateClasses) { // Resolve a state's name from the class reference. const findName = (stateClass) => { const meta = stateClasses.find(s => s === stateClass); if (typeof ngDevMode !== 'undefined' && ngDevMode && !meta) { throw new Error(`Child state not found: ${stateClass}. \r\nYou may have forgotten to add states to module`); } return meta[_META_KEY].name; }; // Build the dependency graph. return stateClasses.reduce((graph, stateClass) => { const meta = stateClass[_META_KEY]; graph[meta.name] = (meta.children || []).map(findName); return graph; }, {}); } /** * Given a states array, returns object graph * returning the name and state metadata. Example: * * const graph = { * cart: { metadata } * }; * * @ignore */ function nameToState(states) { return states.reduce((result, stateClass) => { const meta = stateClass[_META_KEY]; result[meta.name] = stateClass; return result; }, {}); } /** * Given a object relationship graph will return the full path * for the child items. Example: * * const graph = { * cart: ['saved'], * saved: ['items'], * items: [] * }; * * would return: * * const r = { * cart: 'cart', * saved: 'cart.saved', * items: 'cart.saved.items' * }; * * @ignore */ function findFullParentPath(obj, out = {}) { // Recursively find the full dotted parent path for a given key. const find = (graph, target) => { for (const key in graph) { if (graph[key]?.includes(target)) { const parent = find(graph, key); return parent ? `${parent}.${key}` : key; } } return null; }; // Build full path for each key for (const key in obj) { const parent = find(obj, key); out[key] = parent ? `${parent}.${key}` : key; } return out; } /** * Given a object graph, it will return the items topologically sorted Example: * * const graph = { * cart: ['saved'], * saved: ['items'], * items: [] * }; * * would return: * * const results = [ * 'items', * 'saved', * 'cart' * ]; * * @ignore */ function topologicalSort(graph) { const sorted = []; const visited = {}; // DFS (Depth-First Search) to visit each node and its dependencies. const visit = (name, ancestors = []) => { visited[name] = true; ancestors.push(name); for (const dep of graph[name]) { if (typeof ngDevMode !== 'undefined' && ngDevMode && ancestors.includes(dep)) { throw new Error(`Circular dependency '${dep}' is required by '${name}': ${ancestors.join(' -> ')}`); } if (!visited[dep]) visit(dep, ancestors.slice()); } // Add to sorted list if not already included. if (!sorted.includes(name)) sorted.push(name); }; // Start DFS from each key for (const key in graph) visit(key); return sorted.reverse(); } function throwStateNameError(name) { throw new Error(`${name} is not a valid state name. It needs to be a valid object property name.`); } function throwStateNamePropertyError() { throw new Error(`States must register a 'name' property.`); } function throwStateUniqueError(current, newName, oldName) { throw new Error(`State name '${current}' from ${newName} already exists in ${oldName}.`); } function throwStateDecoratorError(name) { throw new Error(`States must be decorated with @State() decorator, but "${name}" isn't.`); } function throwActionDecoratorError() { throw new Error('@Action() decorator cannot be used with static methods.'); } function throwSelectorDecoratorError() { throw new Error('Selectors only work on methods.'); } function getUndecoratedStateWithInjectableWarningMessage(name) { return `'${name}' class should be decorated with @Injectable() right after the @State() decorator`; } function getInvalidInitializationOrderMessage(addedStates) { let message = 'You have an invalid state initialization order. This typically occurs when `NgxsModule.forFeature`\n' + 'or `provideStates` is called before `NgxsModule.forRoot` or `provideStore`.\n' + 'One example is when `NgxsRouterPluginModule.forRoot` is called before `NgxsModule.forRoot`.'; if (addedStates) { const stateNames = Object.keys(addedStates).map(stateName => `"${stateName}"`); message += '\nFeature states added before the store initialization is complete: ' + `${stateNames.join(', ')}.`; } return message; } function throwPatchingArrayError() { throw new Error('Patching arrays is not supported.'); } function throwPatchingPrimitiveError() { throw new Error('Patching primitives is not supported.'); } const stateNameRegex = /* @__PURE__ */ new RegExp('^[a-zA-Z0-9_]+$'); function ensureStateNameIsValid(name) { if (!name) { throwStateNamePropertyError(); } else if (!stateNameRegex.test(name)) { throwStateNameError(name); } } function ensureStateNameIsUnique(stateName, state, statesByName) { const existingState = statesByName[stateName]; if (existingState && existingState !== state) { throwStateUniqueError(stateName, state.name, existingState.name); } } function ensureStatesAreDecorated(stateClasses) { stateClasses.forEach((stateClass) => { if (!_getStoreMetadata(stateClass)) { throwStateDecoratorError(stateClass.name); } }); } /** * All provided or injected tokens must have `@Injectable` decorator * (previously, injected tokens without `@Injectable` were allowed * if another decorator was used, e.g. pipes). */ function ensureStateClassIsInjectable(stateClass) { if (jit_hasInjectableAnnotation(stateClass) || aot_hasNgInjectableDef(stateClass)) { return; } console.warn(getUndecoratedStateWithInjectableWarningMessage(stateClass.name)); } function aot_hasNgInjectableDef(stateClass) { // `ɵprov` is a static property added by the NGCC compiler. It always exists in // AOT mode because this property is added before runtime. If an application is running in // JIT mode then this property can be added by the `@Injectable()` decorator. The `@Injectable()` // decorator has to go after the `@State()` decorator, thus we prevent users from unwanted DI errors. return !!stateClass.ɵprov; } function jit_hasInjectableAnnotation(stateClass) { // `ɵprov` doesn't exist in JIT mode (for instance when running unit tests with Jest). const annotations = stateClass.__annotations__ || []; return annotations.some((annotation) => annotation?.ngMetadataName === 'Injectable'); } const NGXS_DEVELOPMENT_OPTIONS = /* @__PURE__ */ new InjectionToken(typeof ngDevMode !== 'undefined' && ngDevMode ? 'NGXS_DEVELOPMENT_OPTIONS' : '', { providedIn: 'root', factory: () => ({ warnOnUnhandledActions: true }) }); class NgxsUnhandledActionsLogger { /** * These actions should be ignored by default; the user can increase this * list in the future via the `ignoreActions` method. */ _ignoredActions = new Set([InitState.type, UpdateState.type]); constructor() { const options = inject(NGXS_DEVELOPMENT_OPTIONS); if (typeof options.warnOnUnhandledActions === 'object') { this.ignoreActions(...options.warnOnUnhandledActions.ignore); } } /** * Adds actions to the internal list of actions that should be ignored. */ ignoreActions(...actions) { for (const action of actions) { this._ignoredActions.add(action.type); } } /** @internal */ warn(action) { const actionShouldBeIgnored = Array.from(this._ignoredActions).some(type => type === getActionTypeFromInstance(action)); if (actionShouldBeIgnored) { return; } action = action.constructor && action.constructor.name !== 'Object' ? action.constructor.name : action.type; console.warn(`The ${action} action has been dispatched but hasn't been handled. This may happen if the state with an action handler for this action is not registered.`); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsUnhandledActionsLogger, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsUnhandledActionsLogger }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsUnhandledActionsLogger, decorators: [{ type: Injectable }], ctorParameters: () => [] }); class NgxsUnhandledErrorHandler { _ngZone = inject(NgZone); _errorHandler = inject(ErrorHandler); /** * The `_unhandledErrorContext` is left unused internally since we do not * require it for internal operations. However, developers who wish to provide * their own custom error handler may utilize this context information. */ handleError(error, _unhandledErrorContext) { // In order to avoid duplicate error handling, it is necessary to leave // the Angular zone to ensure that errors are not caught twice. The `handleError` // method may contain a `throw error` statement, which is used to re-throw the error. // If the error is re-thrown within the Angular zone, it will be caught again by the // Angular zone. By default, `@angular/core` leaves the Angular zone when invoking // `handleError` (see `_callAndReportToErrorHandler`). this._ngZone.runOutsideAngular(() => this._errorHandler.handleError(error)); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsUnhandledErrorHandler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsUnhandledErrorHandler, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxsUnhandledErrorHandler, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * RxJS operator for selecting out specific actions. * * This will grab actions that have just been dispatched as well as actions that have completed */ function ofAction(...allowedTypes) { return ofActionOperator(allowedTypes); } /** * RxJS operator for selecting out specific actions. * * This will ONLY grab actions that have just been dispatched */ function ofActionDispatched(...allowedTypes) { return ofActionOperator(allowedTypes, [ActionStatus.Dispatched]); } /** * RxJS operator for selecting out specific actions. * * This will ONLY grab actions that have just been successfully completed */ function ofActionSuccessful(...allowedTypes) { return ofActionOperator(allowedTypes, [ActionStatus.Successful]); } /** * RxJS operator for selecting out specific actions. * * This will ONLY grab actions that have just been canceled */ function ofActionCanceled(...allowedTypes) { return ofActionOperator(allowedTypes, [ActionStatus.Canceled]); } /** * RxJS operator for selecting out specific actions. * * This will ONLY grab actions that have just been completed */ function ofActionCompleted(...allowedTypes) { const allowedStatuses = [ ActionStatus.Successful, ActionStatus.Canceled, ActionStatus.Errored ]; return ofActionOperator(allowedTypes, allowedStatuses, mapActionResult); } /** * RxJS operator for selecting out specific actions. * * This will ONLY grab actions that have just thrown an error */ function ofActionErrored(...allowedTypes) { return ofActionOperator(allowedTypes, [ActionStatus.Errored], mapActionResult); } function ofActionOperator(allowedTypes, statuses, // This could have been written as // `OperatorFunction<ActionContext, ActionCompletion | any>`, as it maps // either to `ctx.action` or to `ActionCompletion`. However, // `ActionCompletion | any` defaults to `any`, rendering the union // type meaningless. mapOperator = mapAction) { const allowedMap = createAllowedActionTypesMap(allowedTypes); const allowedStatusMap = statuses && createAllowedStatusesMap(statuses); return function (o) { return o.pipe(filterStatus(allowedMap, allowedStatusMap), mapOperator()); }; } function filterStatus(allowedTypes, allowedStatuses) { return filter((ctx) => { const actionType = getActionTypeFromInstance(ctx.action); const typeMatch = allowedTypes[actionType]; const statusMatch = allowedStatuses ? allowedStatuses[ctx.status] : true; return typeMatch && statusMatch; }); } function mapActionResult() { return map(({ action, status, error }) => { return { action, result: { successful: ActionStatus.Successful === status, canceled: ActionStatus.Canceled === status, error } }; }); } function mapAction() { return map((ctx) => ctx.action); } function createAllowedActionTypesMap(types) { return types.reduce((filterMap, klass) => { filterMap[getActionTypeFromInstance(klass)] = true; return filterMap; }, {}); } function createAllowedStatusesMap(statuses) { return statuses.reduce((filterMap, status) => { filterMap[status] = true; return filterMap; }, {}); } function simplePatch(value) { return (existingState) => { if (typeof ngDevMode !== 'undefined' && ngDevMode) { if (Array.isArray(value)) { throwPatchingArrayError(); } else if (typeof value !== 'object') { throwPatchingPrimitiveError(); } } const newState = { ...existingState }; for (const key in value) { // deep clone for patch compatibility newState[key] = value[key]; } return newState; }; } /** * State Context factory class * @ignore */ class StateContextFactory { _internalStateOperations = inject(InternalStateOperations); /** * Create the state context */ createStateContext(path) { const root = this._internalStateOperations.getRootStateOperations(); return { getState() { const currentAppState = root.getState(); return getState(currentAppState, path); }, patchState(val) { const currentAppState = root.getState(); const patchOperator = simplePatch(val); setStateFromOperator(root, currentAppState, patchOperator, path); }, setState(val) { const currentAppState = root.getState(); if (isStateOperator(val)) { setStateFromOperator(root, currentAppState, val, path); } else { setStateValue(root, currentAppState, val, path); } }, dispatch(actions) { return root.dispatch(actions); } }; } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: StateContextFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: StateContextFactory, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: StateContextFactory, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); function setStateValue(root, currentAppState, newValue, path) { const newAppState = setValue(currentAppState, path, newValue); root.setState(newAppState); return newAppState; // In doing this refactoring I noticed that there is a 'bug' where the // application state is returned instead of this state slice. // This has worked this way since the beginning see: // https://github.com/ngxs/store/blame/324c667b4b7debd8eb979006c67ca0ae347d88cd/src/state-factory.ts // This needs to be fixed, but is a 'breaking' change. // I will do this fix in a subsequent PR and we can decide how to handle it. } function setStateFromOperator(root, currentAppState, stateOperator, path) { const local = getState(currentAppState, path); const newValue = stateOperator(local); return setStateValue(root, currentAppState, newValue, path); } function getState(currentAppState, path) { return getValue(currentAppState, path); } class InternalActionHandlerFactory { _actions = inject(InternalActions); _stateContextFactory = inject(StateContextFactory); createActionHandler(path, handlerFn, options) { const { dispatched$ } = this._actions; return (action) => { const stateContext = this._stateContextFactory.createStateContext(path); let result = handlerFn(stateContext, action); // We need to use `isPromise` instead of checking whether // `result instanceof Promise`. In zone.js patched environments, `global.Promise` // is the `ZoneAwarePromise`. Some APIs, which are likely not patched by zone.js // for certain reasons, might not work with `instanceof`. For instance, the dynamic // import returns a native promise (not a `ZoneAwarePromise`), causing this check to // be falsy. if (_isPromise(result)) { result = from(result); } if (isObservable(result)) { result = result.pipe(mergeMap(value => (_isPromise(value) || isObservable(value) ? value : of(value))), // If this observable has completed without emitting any values, // we wouldn't want to complete the entire chain of actions. // If any observable completes, then the action will be canceled. // For instance, if any action handler had a statement like // `handler(ctx) { return EMPTY; }`, then the action would be canceled. // See https://github.com/ngxs/store/issues/1568 // Note that we actually don't care about the return type; we only care // about emission, and thus `undefined` is applicable by the framework. defaultIfEmpty(undefined)); if (options.cancelUncompleted) { const canceled = dispatched$.pipe(ofActionDispatched(action)); result = result.pipe(takeUntil(canceled)); } result = result.pipe( // Note that we use the `finalize` operator only when the action handler // explicitly returns an observable (or a promise) to wait for. This means // the action handler is written in a "fire & wait" style. If the handler’s // result is unsubscribed (either because the observable has completed or // it was unsubscribed by `takeUntil` due to a new action being dispatched), // we prevent writing to the state context. finalize(() => { if (typeof ngDevMode !== 'undefined' && ngDevMode) { function noopAndWarn() { console.warn(`"${action}" attempted to change the state, but the change was ignored because state updates are not allowed after the action handler has completed.`); } stateContext.setState = noopAndWarn; stateContext.patchState = noopAndWarn; } else { stateContext.setState = noop; stateContext.patchState = noop; } })); } else { // If the action handler is synchronous and returns nothing (`void`), we // still have to convert the result to a synchronous observable. result = of(undefined); } return result; }; } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalActionHandlerFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: InternalActionHandlerFactory, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: In