feat(actions): add action context for safe nested dispatch with depth limit to prevent deadlocks
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as plugins from './smartstate.plugins.js';
|
||||
import { Observable, shareReplay, takeUntil } from 'rxjs';
|
||||
import { StateAction, type IActionDef } from './smartstate.classes.stateaction.js';
|
||||
import { StateAction, type IActionDef, type IActionContext } from './smartstate.classes.stateaction.js';
|
||||
import type { Smartstate } from './smartstate.classes.smartstate.js';
|
||||
|
||||
export type TMiddleware<TPayload> = (
|
||||
@@ -28,6 +28,8 @@ function fromAbortSignal(signal: AbortSignal): Observable<void> {
|
||||
}
|
||||
|
||||
export class StatePart<TStatePartName, TStatePayload> {
|
||||
private static readonly MAX_NESTED_DISPATCH_DEPTH = 10;
|
||||
|
||||
public name: TStatePartName;
|
||||
public state = new plugins.smartrx.rxjs.Subject<TStatePayload>();
|
||||
public stateStore: TStatePayload | undefined;
|
||||
@@ -249,6 +251,28 @@ export class StatePart<TStatePartName, TStatePayload> {
|
||||
return new StateAction(this, actionDef);
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a depth-tracked action context for safe nested dispatch.
|
||||
* Using context.dispatch() within an actionDef bypasses the mutation queue
|
||||
* and executes the sub-action inline, preventing deadlocks.
|
||||
*/
|
||||
private createActionContext(depth: number): IActionContext<TStatePayload> {
|
||||
const self = this;
|
||||
return {
|
||||
dispatch: async <U>(action: StateAction<TStatePayload, U>, payload: U): Promise<TStatePayload> => {
|
||||
if (depth >= StatePart.MAX_NESTED_DISPATCH_DEPTH) {
|
||||
throw new Error(
|
||||
`Maximum nested action dispatch depth (${StatePart.MAX_NESTED_DISPATCH_DEPTH}) exceeded. ` +
|
||||
`Check for circular action dispatches.`
|
||||
);
|
||||
}
|
||||
const innerContext = self.createActionContext(depth + 1);
|
||||
const newState = await action.actionDef(self, payload, innerContext);
|
||||
return self.applyState(newState);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* dispatches an action on the statepart level
|
||||
*/
|
||||
@@ -256,11 +280,13 @@ export class StatePart<TStatePartName, TStatePayload> {
|
||||
await this.cumulativeDeferred.promise;
|
||||
return this.mutationQueue = this.mutationQueue.then(
|
||||
async () => {
|
||||
const newState = await stateAction.actionDef(this, actionPayload);
|
||||
const context = this.createActionContext(0);
|
||||
const newState = await stateAction.actionDef(this, actionPayload, context);
|
||||
return this.applyState(newState);
|
||||
},
|
||||
async () => {
|
||||
const newState = await stateAction.actionDef(this, actionPayload);
|
||||
const context = this.createActionContext(0);
|
||||
const newState = await stateAction.actionDef(this, actionPayload, context);
|
||||
return this.applyState(newState);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user