Skip to content

Commit

Permalink
Fixed an issue that prevented events sent from the exit actions of th…
Browse files Browse the repository at this point in the history
…e invoking state to be delivered to the invoked actor (#3713)

* Add failing test

* Fixed an issue that prevented events sent from the exit actions of the invoking state to be delivered to the invoked actor

Co-authored-by: David Khourshid <davidkpiano@gmail.com>
  • Loading branch information
Andarist and davidkpiano committed Dec 19, 2022
1 parent 3ee04b9 commit 9605297
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/silly-buses-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': patch
---

Fixed an issue that prevented events sent from the exit actions of the invoking state to be delivered to the invoked actor (when leaving that state).
48 changes: 31 additions & 17 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,10 @@ class StateNode<
_event: SCXML.Event<TEvent>,
prevState?: State<TContext, any, any, any, any>,
predictableExec?: PredictableActionArgumentsExec
): Array<Array<ActionObject<TContext, TEvent>>> {
): Array<{
type: string;
actions: Array<ActionObject<TContext, TEvent>>;
}> {
const prevConfig = prevState
? getConfiguration([], this.getStateNodes(prevState.value))
: [];
Expand Down Expand Up @@ -1085,29 +1088,40 @@ class StateNode<
const invokeActions = stateNode.activities.map((activity) =>
start(activity)
);
return toActionObjects(
predictableExec
? [...entryActions, ...invokeActions]
: [...invokeActions, ...entryActions],
this.machine.options.actions as any
);
return {
type: 'entry',
actions: toActionObjects(
predictableExec
? [...entryActions, ...invokeActions]
: [...invokeActions, ...entryActions],
this.machine.options.actions as any
)
};
})
.concat([doneEvents.map(raise) as Array<ActionObject<TContext, TEvent>>]);

const exitActions = Array.from(exitStates).map((stateNode) =>
toActionObjects(
.concat({
type: 'state_done',
actions: doneEvents.map(raise) as Array<ActionObject<TContext, TEvent>>
});

const exitActions = Array.from(exitStates).map((stateNode) => ({
type: 'exit',
actions: toActionObjects(
[
...stateNode.onExit,
...stateNode.activities.map((activity) => stop(activity))
],
this.machine.options.actions as any
)
);
}));

const actions = exitActions
.concat([
toActionObjects(transition.actions, this.machine.options.actions as any)
])
.concat({
type: 'transition',
actions: toActionObjects(
transition.actions,
this.machine.options.actions as any
)
})
.concat(entryActions);

if (isDone) {
Expand All @@ -1124,7 +1138,7 @@ class StateNode<
(action.type !== actionTypes.send ||
(!!action.to && action.to !== SpecialTargets.Internal))
);
return actions.concat([stopActions]);
return actions.concat({ type: 'stop', actions: stopActions });
}

return actions;
Expand Down Expand Up @@ -1288,7 +1302,7 @@ class StateNode<
);
const activities = currentState ? { ...currentState.activities } : {};
for (const block of actionBlocks) {
for (const action of block) {
for (const action of block.actions) {
if (action.type === actionTypes.start) {
activities[
action.activity!.id || action.activity!.type
Expand Down
60 changes: 41 additions & 19 deletions packages/core/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,16 +627,19 @@ export function choose<TContext, TEvent extends EventObject>(
}

const pluckAssigns = <TContext, TEvent extends EventObject>(
actionBlocks: Array<Array<ActionObject<TContext, TEvent>>>
actionBlocks: Array<{
type: string;
actions: Array<ActionObject<TContext, TEvent>>;
}>
): AssignAction<TContext, TEvent>[] => {
const assignActions: AssignAction<TContext, TEvent>[] = [];

for (const block of actionBlocks) {
let i = 0;
while (i < block.length) {
if (block[i].type === actionTypes.assign) {
assignActions.push(block[i] as AssignAction<TContext, TEvent>);
block.splice(i, 1);
while (i < block.actions.length) {
if (block.actions[i].type === actionTypes.assign) {
assignActions.push(block.actions[i] as AssignAction<TContext, TEvent>);
block.actions.splice(i, 1);
continue;
}
i++;
Expand All @@ -651,7 +654,10 @@ export function resolveActions<TContext, TEvent extends EventObject>(
currentState: State<TContext, TEvent, any, any, any> | undefined,
currentContext: TContext,
_event: SCXML.Event<TEvent>,
actionBlocks: Array<Array<ActionObject<TContext, TEvent>>>,
actionBlocks: Array<{
type: string;
actions: Array<ActionObject<TContext, TEvent>>;
}>,
predictableExec?: PredictableActionArgumentsExec,
preserveActionOrder: boolean = false
): [Array<ActionObject<TContext, TEvent>>, TContext] {
Expand All @@ -667,7 +673,10 @@ export function resolveActions<TContext, TEvent extends EventObject>(

const deferredToBlockEnd: Array<ActionObject<TContext, TEvent>> = [];

function handleAction(actionObject: ActionObject<TContext, TEvent>) {
function handleAction(
blockType: string,
actionObject: ActionObject<TContext, TEvent>
) {
switch (actionObject.type) {
case actionTypes.raise: {
return resolveRaise(actionObject as RaiseAction<TEvent>);
Expand All @@ -691,7 +700,11 @@ export function resolveActions<TContext, TEvent extends EventObject>(
}

if (predictableExec && sendAction.to !== SpecialTargets.Internal) {
deferredToBlockEnd.push(sendAction);
if (blockType === 'entry') {
deferredToBlockEnd.push(sendAction);
} else {
predictableExec?.(sendAction, updatedContext, _event);
}
}

return sendAction;
Expand Down Expand Up @@ -733,10 +746,13 @@ export function resolveActions<TContext, TEvent extends EventObject>(
updatedContext,
_event,
[
toActionObjects(
toArray(matchedActions),
machine.options.actions as any
)
{
type: blockType,
actions: toActionObjects(
toArray(matchedActions),
machine.options.actions as any
)
}
],
predictableExec,
preserveActionOrder
Expand All @@ -759,10 +775,13 @@ export function resolveActions<TContext, TEvent extends EventObject>(
updatedContext,
_event,
[
toActionObjects(
toArray(matchedActions),
machine.options.actions as any
)
{
type: blockType,
actions: toActionObjects(
toArray(matchedActions),
machine.options.actions as any
)
}
],
predictableExec,
preserveActionOrder
Expand Down Expand Up @@ -812,11 +831,14 @@ export function resolveActions<TContext, TEvent extends EventObject>(
}
}

function processBlock(block: ActionObject<TContext, TEvent>[]) {
function processBlock(block: {
type: string;
actions: ActionObject<TContext, TEvent>[];
}) {
let resolvedActions: Array<ActionObject<TContext, TEvent>> = [];

for (const action of block) {
const resolved = handleAction(action);
for (const action of block.actions) {
const resolved = handleAction(block.type, action);
if (resolved) {
resolvedActions = resolvedActions.concat(resolved);
}
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,12 @@ export class Interpreter<
this.state,
this.state.context,
_event,
[exitActions],
[
{
type: 'exit',
actions: exitActions
}
],
this.machine.config.predictableActionArguments
? this._exec
: undefined,
Expand Down
34 changes: 33 additions & 1 deletion packages/core/test/predictableExec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
interpret,
assign,
spawn,
AnyInterpreter
AnyInterpreter,
sendTo
} from '../src';
import { raise, stop, send, sendParent } from '../src/actions';

Expand Down Expand Up @@ -775,4 +776,35 @@ describe('predictableExec', () => {

expect(gotEvent).toBe(true);
});

// https://github.com/statelyai/xstate/issues/3617
it('should deliver events sent from the exit actions to a service invoked in the same state', (done) => {
const machine = createMachine({
initial: 'active',
predictableActionArguments: true,
states: {
active: {
invoke: {
id: 'my-service',
src: (_, __) => (_, onReceive) => {
onReceive((event) => {
if (event.type === 'MY_EVENT') {
done();
}
});
}
},
exit: sendTo('my-service', { type: 'MY_EVENT' }),
on: {
TOGGLE: 'inactive'
}
},
inactive: {}
}
});

const actor = interpret(machine).start();

actor.send({ type: 'TOGGLE' });
});
});

0 comments on commit 9605297

Please sign in to comment.