Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(store): prevent initializing state factory at feature levels #2262

Merged
merged 1 commit into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ $ npm install @ngxs/store@dev
- Fix(websocket-plugin): Do not dispatch action when root injector is destroyed [#2257](https://github.com/ngxs/store/pull/2257)
- Refactor(store): Replace `exhaustMap` [#2254](https://github.com/ngxs/store/pull/2254)
- Refactor(store): Tree-shake development options token [#2260](https://github.com/ngxs/store/pull/2260)
- Performance(store): Prevent initializing state factory at feature levels [#2261](https://github.com/ngxs/store/pull/2261)

### 18.1.5 2024-11-12

Expand Down
91 changes: 34 additions & 57 deletions packages/store/src/internal/state-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,20 +69,17 @@ function cloneDefaults(defaults: any): any {
* The `StateFactory` class adds root and feature states to the graph.
* This extracts state names from state classes, checks if they already
* exist in the global graph, throws errors if their names are invalid, etc.
* See its constructor, state factories inject state factories that are
* parent-level providers. This is required to get feature states from the
* injector on the same level.
*
* The `NgxsModule.forFeature(...)` returns `providers: [StateFactory, ...states]`.
* The `StateFactory` is initialized on the feature level and goes through `...states`
* to get them from the injector through `injector.get(state)`.
* Root and feature initializers call `addAndReturnDefaults()` to add those states
* to the global graph. Since `addAndReturnDefaults` runs within the injection
* context (which might be the root injector or a feature injector), we can
* retrieve an instance of the state class using `inject(StateClass)`.
* @ignore
*/
@Injectable()
@Injectable({ providedIn: 'root' })
export class StateFactory implements OnDestroy {
private readonly _injector = inject(Injector);
private readonly _config = inject(NgxsConfig);
private readonly _parentFactory = inject(StateFactory, { optional: true, skipSelf: true });
private readonly _stateContextFactory = inject(StateContextFactory);
private readonly _actions = inject(InternalActions);
private readonly _actionResults = inject(InternalDispatchedActionResults);
Expand All @@ -95,56 +92,43 @@ export class StateFactory implements OnDestroy {
private _ngxsUnhandledErrorHandler: NgxsUnhandledErrorHandler = null!;

private _states: MappedStore[] = [];
get states(): MappedStore[] {
return this._parentFactory ? this._parentFactory.states : this._states;
}

private _statesByName: StatesByName = {};
get statesByName(): StatesByName {
return this._parentFactory ? this._parentFactory.statesByName : this._statesByName;
}

private _statePaths: ɵPlainObjectOf<string> = {};
private get statePaths(): ɵPlainObjectOf<string> {
return this._parentFactory ? this._parentFactory.statePaths : this._statePaths;
}

getRuntimeSelectorContext = ɵmemoize(() => {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const stateFactory = this;
const propGetter = stateFactory._propGetter;

function resolveGetter(key: string) {
const path = stateFactory.statePaths[key];
const path = stateFactory._statePaths[key];
return path ? propGetter(path.split('.')) : null;
}

const context: ɵRuntimeSelectorContext = this._parentFactory
? this._parentFactory.getRuntimeSelectorContext()
: {
getStateGetter(key: string) {
// Use `@__INLINE__` annotation to forcely inline `resolveGetter`.
// This is a Terser annotation, which will function only in the production mode.
let getter = /*@__INLINE__*/ resolveGetter(key);
if (getter) {
return getter;
}
return (...args) => {
// Late loaded getter
if (!getter) {
getter = /*@__INLINE__*/ resolveGetter(key);
}
return getter ? getter(...args) : undefined;
};
},
getSelectorOptions(localOptions?: ɵSharedSelectorOptions) {
const globalSelectorOptions = stateFactory._config.selectorOptions;
return {
...globalSelectorOptions,
...(localOptions || {})
};
const context: ɵRuntimeSelectorContext = {
getStateGetter(key: string) {
// Use `@__INLINE__` annotation to forcely inline `resolveGetter`.
// This is a Terser annotation, which will function only in the production mode.
let getter = /*@__INLINE__*/ resolveGetter(key);
if (getter) {
return getter;
}
return (...args) => {
// Late loaded getter
if (!getter) {
getter = /*@__INLINE__*/ resolveGetter(key);
}
return getter ? getter(...args) : undefined;
};
},
getSelectorOptions(localOptions?: ɵSharedSelectorOptions) {
const globalSelectorOptions = stateFactory._config.selectorOptions;
return {
...globalSelectorOptions,
...(localOptions || {})
};
}
};
return context;
});

Expand All @@ -155,7 +139,7 @@ export class StateFactory implements OnDestroy {
/**
* Add a new state to the global defs.
*/
add(stateClasses: ɵStateClassInternal[]): MappedStore[] {
private add(stateClasses: ɵStateClassInternal[]): MappedStore[] {
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
ensureStatesAreDecorated(stateClasses);
}
Expand Down Expand Up @@ -189,7 +173,7 @@ export class StateFactory implements OnDestroy {
path,
isInitialised: false,
actions: meta.actions,
instance: this._injector.get(stateClass),
instance: inject(stateClass),
defaults: cloneDefaults(meta.defaults)
};

Expand All @@ -200,7 +184,7 @@ export class StateFactory implements OnDestroy {
bootstrappedStores.push(stateMap);
}

this.states.push(stateMap);
this._states.push(stateMap);
this.hydrateActionMetasMap(stateMap);
}

Expand All @@ -223,13 +207,6 @@ export class StateFactory implements OnDestroy {
}

connectActionHandlers(): void {
// Note: We have to connect actions only once when the `StateFactory`
// is being created for the first time. This checks if we're in
// a child state factory and the parent state factory already exists.
if (this._parentFactory || this._actionsSubscription !== null) {
return;
}

this._actionsSubscription = this._actions
.pipe(
filter((ctx: ActionContext) => ctx.status === ActionStatus.Dispatched),
Expand Down Expand Up @@ -306,7 +283,7 @@ export class StateFactory implements OnDestroy {
newStates: ɵStateClassInternal[];
} {
const newStates: ɵStateClassInternal[] = [];
const statesMap: StatesByName = this.statesByName;
const statesMap: StatesByName = this._statesByName;

for (const stateClass of stateClasses) {
const stateName = ɵgetStoreMetadata(stateClass).name!;
Expand All @@ -324,7 +301,7 @@ export class StateFactory implements OnDestroy {
}

private addRuntimeInfoToMeta(meta: ɵMetaDataModel, path: string): void {
this.statePaths[meta.name!] = path;
this._statePaths[meta.name!] = path;
// TODO: versions after v3 - we plan to get rid of the `path` property because it is non-deterministic
// we can do this when we get rid of the incorrectly exposed getStoreMetadata
// We will need to come up with an alternative to what was exposed in v3 because this is used by many plugins
Expand All @@ -336,7 +313,7 @@ export class StateFactory implements OnDestroy {
getValue(this._initialState, path) !== undefined;
// This checks whether a state has been already added to the global graph and
// its lifecycle is in 'bootstrapped' state.
return this.statesByName[name] && valueIsBootstrappedInInitialState;
return this._statesByName[name] && valueIsBootstrappedInInitialState;
}

private hydrateActionMetasMap({ path, actions, instance }: MappedStore): void {
Expand Down
2 changes: 0 additions & 2 deletions packages/store/src/standalone-features/feature-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import { ɵStateClass } from '@ngxs/store/internals';

import { FEATURE_STATE_TOKEN } from '../symbols';
import { PluginManager } from '../plugin-manager';
import { StateFactory } from '../internal/state-factory';

/**
* This function provides the required providers when calling `NgxsModule.forFeature`
* or `provideStates`. It is shared between the NgModule and standalone APIs.
*/
export function getFeatureProviders(states: ɵStateClass[]): Provider[] {
return [
StateFactory,
PluginManager,
...states,
{
Expand Down
2 changes: 0 additions & 2 deletions packages/store/src/standalone-features/root-providers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { APP_BOOTSTRAP_LISTENER, Provider, inject } from '@angular/core';
import { ɵStateClass, ɵNgxsAppBootstrappedState } from '@ngxs/store/internals';

import { StateFactory } from '../internal/state-factory';
import { CUSTOM_NGXS_EXECUTION_STRATEGY } from '../execution/symbols';
import { NgxsModuleOptions, ROOT_STATE_TOKEN, NGXS_OPTIONS } from '../symbols';

Expand All @@ -14,7 +13,6 @@ export function getRootProviders(
options: NgxsModuleOptions
): Provider[] {
return [
StateFactory,
...states,
{
provide: ROOT_STATE_TOKEN,
Expand Down
Loading