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

feat(store): expose abort controller on state context #2244

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export class HmrStateContextFactory<T, S> {
*/
public createStateContext(): StateContext<S> {
return {
abortSignal: new AbortController().signal,
dispatch: actions => this.store!.dispatch(actions),
getState: () => <S>this.store!.snapshot(),
setState: val => {
Expand Down
1 change: 1 addition & 0 deletions packages/store/src/internal/lifecycle-state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export class LifecycleStateManager implements OnDestroy {
}

private _getStateContext(mappedStore: MappedStore): StateContext<any> {
// Question: abort controller is not gonna be available for lifecycle hooks.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the abortSignal could be available for lifecycle hooks.
For example, when we have a local state, the abort signal for the lifecycle hooks would be triggered when the state should be unloaded.

return this._stateContextFactory.createStateContext(mappedStore.path);
}
}
15 changes: 8 additions & 7 deletions packages/store/src/internal/state-context-factory.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
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
* @ignore
*/
@Injectable({ providedIn: 'root' })
export class StateContextFactory {
constructor(private _internalStateOperations: InternalStateOperations) {}
private _internalStateOperations = inject(InternalStateOperations);

/**
* Create the state context
*/
createStateContext<T>(path: string): StateContext<T> {
createStateContext<T>(path: string, abortSignal?: AbortSignal): StateContext<T> {
const root = this._internalStateOperations.getRootStateOperations();

return {
abortSignal: abortSignal!,
getState(): T {
const currentAppState = root.getState();
return getState(currentAppState, path);
Expand Down
19 changes: 13 additions & 6 deletions packages/store/src/internal/state-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
mergeMap,
takeUntil,
finalize,
Observable
Observable,
fromEvent
} from 'rxjs';

import { NgxsConfig } from '../symbols';
Expand Down Expand Up @@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If cancellation is enabled we would retain a reference to the previous abortController for the handler and when the next action hits this handler we would call abortController.abort() on the previous abortController to trigger the cancellation of the previous handler and of any related work that has used the abort signal from the context.
When the abortSignal fires for some work in progress in the handler (like fetch), the handler would throw an error, we should catch this error and not let it propagate to the action error handlers.

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);

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/store/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export { StateOperator };
* State context provided to the actions in the state.
*/
export interface StateContext<T> {
abortSignal: AbortSignal;

/**
* Get the current state.
*/
Expand Down
Loading