@ngxs/store
Version:
1,174 lines (1,151 loc) • 105 kB
JavaScript
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