diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ec8af6a5..72a6946a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ $ npm install @ngxs/store@dev ### To become next patch version - Fix(store): Add root store initializer guard [#2278](https://github.com/ngxs/store/pull/2278) +- Fix(store): Complete action results on destroy [#2282](https://github.com/ngxs/store/pull/2282) ### 19.0.0 2024-12-3 diff --git a/packages/store/internals/src/state-stream.ts b/packages/store/internals/src/state-stream.ts index 39da83a1f..fa482ec64 100644 --- a/packages/store/internals/src/state-stream.ts +++ b/packages/store/internals/src/state-stream.ts @@ -21,9 +21,12 @@ export class ɵStateStream extends ɵOrderedBehaviorSubject<ɵPlainObject> imple } ngOnDestroy(): void { - // The StateStream should never emit values once the root view is removed, - // such as when the `NgModuleRef.destroy()` method is called. This is crucial - // for preventing memory leaks in server-side rendered apps, where a new StateStream + // 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. + // The `StateStream` should never emit values once the root view is removed, + // such as when the `ApplicationRef.destroy()` method is called. This is crucial + // for preventing memory leaks in server-side rendered apps, where a new `StateStream` // is created for each HTTP request. If users forget to unsubscribe from `store.select` // or `store.subscribe`, it can result in significant memory leaks in SSR apps. this.complete(); diff --git a/packages/store/src/actions-stream.ts b/packages/store/src/actions-stream.ts index b33cbace5..a49139d38 100644 --- a/packages/store/src/actions-stream.ts +++ b/packages/store/src/actions-stream.ts @@ -39,6 +39,9 @@ export class InternalActions extends ɵOrderedSubject implements } ngOnDestroy(): void { + // 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(); } } diff --git a/packages/store/src/internal/action-results.ts b/packages/store/src/internal/action-results.ts new file mode 100644 index 000000000..94ed97d4d --- /dev/null +++ b/packages/store/src/internal/action-results.ts @@ -0,0 +1,23 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +import type { ActionContext } from '../actions-stream'; + +/** + * 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. + */ +@Injectable({ providedIn: 'root' }) +export class InternalDispatchedActionResults + extends Subject + implements OnDestroy +{ + ngOnDestroy(): void { + // 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(); + } +} diff --git a/packages/store/src/internal/dispatcher.ts b/packages/store/src/internal/dispatcher.ts index 7ea1edfbf..15c578a24 100644 --- a/packages/store/src/internal/dispatcher.ts +++ b/packages/store/src/internal/dispatcher.ts @@ -1,24 +1,16 @@ import { inject, Injectable, Injector, NgZone, runInInjectionContext } from '@angular/core'; -import { forkJoin, Observable, of, Subject, throwError } from 'rxjs'; +import { forkJoin, Observable, of, throwError } from 'rxjs'; import { filter, map, mergeMap, shareReplay, take } from 'rxjs/operators'; import { getActionTypeFromInstance } from '@ngxs/store/plugins'; import { ɵPlainObject, ɵStateStream } from '@ngxs/store/internals'; -import { ActionContext, ActionStatus, InternalActions } from '../actions-stream'; import { PluginManager } from '../plugin-manager'; -import { InternalNgxsExecutionStrategy } from '../execution/internal-ngxs-execution-strategy'; import { leaveNgxs } from '../operators/leave-ngxs'; import { fallbackSubscriber } from './fallback-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. - */ -@Injectable({ providedIn: 'root' }) -export class InternalDispatchedActionResults extends Subject {} +import { InternalDispatchedActionResults } from './action-results'; +import { ActionContext, ActionStatus, InternalActions } from '../actions-stream'; +import { InternalNgxsExecutionStrategy } from '../execution/internal-ngxs-execution-strategy'; @Injectable({ providedIn: 'root' }) export class InternalDispatcher { diff --git a/packages/store/src/internal/state-factory.ts b/packages/store/src/internal/state-factory.ts index 7da192221..1629c61af 100644 --- a/packages/store/src/internal/state-factory.ts +++ b/packages/store/src/internal/state-factory.ts @@ -42,7 +42,7 @@ import { } from './internals'; import { NgxsActionRegistry } from '../actions/action-registry'; import { ActionContext, ActionStatus, InternalActions } from '../actions-stream'; -import { InternalDispatchedActionResults } from '../internal/dispatcher'; +import { InternalDispatchedActionResults } from '../internal/action-results'; import { ensureStateNameIsUnique, ensureStatesAreDecorated } from '../utils/store-validators'; import { ensureStateClassIsInjectable } from '../ivy/ivy-enabled-in-dev-mode'; import { NgxsUnhandledActionsLogger } from '../dev-features/ngxs-unhandled-actions-logger';