diff --git a/packages/hmr-plugin/src/internal/hmr-state-context-factory.ts b/packages/hmr-plugin/src/internal/hmr-state-context-factory.ts index 608df15a5..9ed9daefe 100644 --- a/packages/hmr-plugin/src/internal/hmr-state-context-factory.ts +++ b/packages/hmr-plugin/src/internal/hmr-state-context-factory.ts @@ -21,6 +21,7 @@ export class HmrStateContextFactory { */ public createStateContext(): StateContext { return { + abortSignal: new AbortController().signal, dispatch: actions => this.store!.dispatch(actions), getState: () => this.store!.snapshot(), setState: val => { diff --git a/packages/store/src/internal/lifecycle-state-manager.ts b/packages/store/src/internal/lifecycle-state-manager.ts index e1a27b303..99774bf1f 100644 --- a/packages/store/src/internal/lifecycle-state-manager.ts +++ b/packages/store/src/internal/lifecycle-state-manager.ts @@ -99,6 +99,7 @@ export class LifecycleStateManager implements OnDestroy { } private _getStateContext(mappedStore: MappedStore): StateContext { + // Question: abort controller is not gonna be available for lifecycle hooks. return this._stateContextFactory.createStateContext(mappedStore.path); } } diff --git a/packages/store/src/internal/state-context-factory.ts b/packages/store/src/internal/state-context-factory.ts index 38ec2bac6..3cc3f7a2a 100644 --- a/packages/store/src/internal/state-context-factory.ts +++ b/packages/store/src/internal/state-context-factory.ts @@ -1,12 +1,12 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { getValue, setValue } from '@ngxs/store/plugins'; import { ExistingState, StateOperator, isStateOperator } from '@ngxs/store/operators'; -import { Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; -import { StateContext } from '../symbols'; -import { StateOperations } from '../internal/internals'; -import { InternalStateOperations } from '../internal/state-operations'; +import type { StateContext } from '../symbols'; import { simplePatch } from './state-operators'; +import type { StateOperations } from '../internal/internals'; +import { InternalStateOperations } from '../internal/state-operations'; /** * State Context factory class @@ -14,15 +14,16 @@ import { simplePatch } from './state-operators'; */ @Injectable({ providedIn: 'root' }) export class StateContextFactory { - constructor(private _internalStateOperations: InternalStateOperations) {} + private _internalStateOperations = inject(InternalStateOperations); /** * Create the state context */ - createStateContext(path: string): StateContext { + createStateContext(path: string, abortSignal?: AbortSignal): StateContext { const root = this._internalStateOperations.getRootStateOperations(); return { + abortSignal: abortSignal!, getState(): T { const currentAppState = root.getState(); return getState(currentAppState, path); diff --git a/packages/store/src/internal/state-factory.ts b/packages/store/src/internal/state-factory.ts index 3d51e54bb..6bc77bdaf 100644 --- a/packages/store/src/internal/state-factory.ts +++ b/packages/store/src/internal/state-factory.ts @@ -25,7 +25,8 @@ import { mergeMap, takeUntil, finalize, - Observable + Observable, + fromEvent } from 'rxjs'; import { NgxsConfig } from '../symbols'; @@ -345,10 +346,12 @@ export class StateFactory implements OnDestroy { const { dispatched$ } = this._actions; for (const actionType of Object.keys(actions)) { const actionHandlers = actions[actionType].map(actionMeta => { - const cancelable = !!actionMeta.options.cancelUncompleted; + const abortController = new AbortController(); + const abortSignal = abortController.signal; + const cancellable = !!actionMeta.options.cancelUncompleted; return (action: any) => { - const stateContext = this._stateContextFactory.createStateContext(path); + const stateContext = this._stateContextFactory.createStateContext(path, abortSignal); let result = instance[actionMeta.fn](stateContext, action); @@ -384,12 +387,16 @@ export class StateFactory implements OnDestroy { defaultIfEmpty(undefined) ); - if (cancelable) { - const notifier$ = dispatched$.pipe(ofActionDispatched(action)); - result = result.pipe(takeUntil(notifier$)); + if (cancellable) { + const cancelled = dispatched$.pipe(ofActionDispatched(action)); + result = result.pipe(takeUntil(cancelled)); } + const aborted = fromEvent(abortSignal, 'abort'); + result = result.pipe( + takeUntil(aborted), + // Note that we use the `finalize` operator only when the action handler // returns an observable. If the action handler is synchronous, we do not // need to set the state context functions to `noop`, as the absence of a diff --git a/packages/store/src/symbols.ts b/packages/store/src/symbols.ts index 58fbe1166..61d461083 100644 --- a/packages/store/src/symbols.ts +++ b/packages/store/src/symbols.ts @@ -99,6 +99,8 @@ export { StateOperator }; * State context provided to the actions in the state. */ export interface StateContext { + abortSignal: AbortSignal; + /** * Get the current state. */