Skip to content

Commit

Permalink
Stop calling exit actions on stop (#4377)
Browse files Browse the repository at this point in the history
* Do not call exit actions when the actor gets stopped

* adjust test cases

* add changeset
  • Loading branch information
Andarist authored Oct 20, 2023
1 parent 078eaad commit 14cb2ed
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 269 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-apes-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': major
---

`exit` actions of all states are no longer called when the machine gets stopped externally. Note that they are still called when the machine reaches its final state.
22 changes: 1 addition & 21 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ export function macrostep(

// Handle stop event
if (event.type === XSTATE_STOP) {
nextState = stopStep(nextState, event, actorCtx);
nextState = stopChildren(nextState, event, actorCtx);
states.push(nextState);

return {
Expand Down Expand Up @@ -1649,26 +1649,6 @@ function stopChildren(
);
}

function stopStep(
nextState: AnyState,
event: AnyEventObject,
actorCtx: AnyActorContext
) {
const exitActions = nextState.configuration
.sort((a, b) => b.order - a.order)
.flatMap((stateNode) => stateNode.exit);

nextState = resolveActionsAndContext(
nextState,
event,
actorCtx,
exitActions,
[]
);

return stopChildren(nextState, event, actorCtx);
}

function selectTransitions(
event: AnyEventObject,
nextState: AnyState
Expand Down
264 changes: 21 additions & 243 deletions packages/core/test/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1385,239 +1385,25 @@ describe('entry/exit actions', () => {
});

describe('when stopped', () => {
it('exit actions should be called when stopping a machine', () => {
let exitCalled = false;
let childExitCalled = false;

const machine = createMachine({
exit: () => {
exitCalled = true;
},
initial: 'a',
states: {
a: {
exit: () => {
childExitCalled = true;
}
}
}
});

const service = createActor(machine).start();
service.stop();

expect(exitCalled).toBeTruthy();
expect(childExitCalled).toBeTruthy();
});

it('should call each exit handler only once when the service gets stopped', () => {
const machine = createMachine({
initial: 'a',
states: {
a: {
initial: 'a1',
states: {
a1: {}
}
}
}
});

const flushTracked = trackEntries(machine);

const service = createActor(machine).start();

flushTracked();
service.stop();

expect(flushTracked()).toEqual([
'exit: a.a1',
'exit: a',
'exit: __root__'
]);
});
it('exit actions should not be called when stopping a machine', () => {
const rootSpy = jest.fn();
const childSpy = jest.fn();

it('should call exit actions in reversed document order when the service gets stopped', () => {
const machine = createMachine({
exit: rootSpy,
initial: 'a',
states: {
a: {
on: {
EV: {
// just a noop action to ensure that a transition is selected when we send an event
actions: () => {}
}
}
}
}
});

const flushTracked = trackEntries(machine);

const service = createActor(machine).start();

// it's important to send an event here that results in a transition that computes new `state.configuration`
// and that could impact the order in which exit actions are called
service.send({ type: 'EV' });
flushTracked();
service.stop();

expect(flushTracked()).toEqual(['exit: a', 'exit: __root__']);
});

it('should call exit actions of parallel states in reversed document order when the service gets stopped after earlier region transition', () => {
const machine = createMachine({
type: 'parallel',
states: {
a: {
initial: 'child_a',
states: {
child_a: {
on: {
EV: {
// just a noop action to ensure that a transition is selected when we send an event
actions: () => {}
}
}
}
}
},
b: {
initial: 'child_b',
states: {
child_b: {}
}
}
}
});

const flushTracked = trackEntries(machine);

const service = createActor(machine).start();

// it's important to send an event here that results in a transition as that computes new `state.configuration`
// and that could impact the order in which exit actions are called
service.send({ type: 'EV' });
flushTracked();
service.stop();

expect(flushTracked()).toEqual([
'exit: b.child_b',
'exit: b',
'exit: a.child_a',
'exit: a',
'exit: __root__'
]);
});

it('should call exit actions of parallel states in reversed document order when the service gets stopped after later region transition', () => {
const machine = createMachine({
type: 'parallel',
states: {
a: {
initial: 'child_a',
states: {
child_a: {}
}
},
b: {
initial: 'child_b',
states: {
child_b: {
on: {
EV: {
// just a noop action to ensure that a transition is selected when we send an event
actions: () => {}
}
}
}
}
}
}
});

const flushTracked = trackEntries(machine);

const service = createActor(machine).start();

// it's important to send an event here that results in a transition as that computes new `state.configuration`
// and that could impact the order in which exit actions are called
service.send({ type: 'EV' });
flushTracked();
service.stop();

expect(flushTracked()).toEqual([
'exit: b.child_b',
'exit: b',
'exit: a.child_a',
'exit: a',
'exit: __root__'
]);
});

it('should call exit actions of parallel states in reversed document order when the service gets stopped after multiple regions transition', () => {
const machine = createMachine({
type: 'parallel',
states: {
a: {
initial: 'child_a',
states: {
child_a: {
on: {
EV: {
// just a noop action to ensure that a transition is selected when we send an event
actions: () => {}
}
}
}
}
},
b: {
initial: 'child_b',
states: {
child_b: {
on: {
EV: {
// just a noop action to ensure that a transition is selected when we send an event
actions: () => {}
}
}
}
}
exit: childSpy
}
}
});

const flushTracked = trackEntries(machine);

const service = createActor(machine).start();
// it's important to send an event here that results in a transition as that computes new `state.configuration`
// and that could impact the order in which exit actions are called
service.send({ type: 'EV' });
flushTracked();
service.stop();

expect(flushTracked()).toEqual([
'exit: b.child_b',
'exit: b',
'exit: a.child_a',
'exit: a',
'exit: __root__'
]);
});

it('an exit action executed when an interpreter gets stopped should receive `xstate.stop` event', () => {
let receivedEvent;
const machine = createMachine({
exit: ({ event }) => {
receivedEvent = event;
}
});

const service = createActor(machine).start();
service.stop();

expect(receivedEvent).toEqual({ type: 'xstate.stop' });
expect(rootSpy).not.toHaveBeenCalled();
expect(childSpy).not.toHaveBeenCalled();
});

it('an exit action executed when an interpreter reaches its final state should be called with the last received event', () => {
Expand Down Expand Up @@ -1757,16 +1543,14 @@ describe('entry/exit actions', () => {
expect(eventReceived).toBe(true);
});

it('sent events from exit handlers of a stopped child should be received by its children', () => {
let eventReceived = false;
it('sent events from exit handlers of a stopped child should not be received by its children', () => {
const spy = jest.fn();

const grandchild = createMachine({
id: 'grandchild',
on: {
STOPPED: {
actions: () => {
eventReceived = true;
}
actions: spy
}
}
});
Expand Down Expand Up @@ -1799,10 +1583,10 @@ describe('entry/exit actions', () => {
const interpreter = createActor(parent).start();
interpreter.send({ type: 'NEXT' });

expect(eventReceived).toBe(true);
expect(spy).not.toHaveBeenCalled();
});

it('sent events from exit handlers of a done child should be received by its children ', () => {
it('sent events from exit handlers of a done child should be received by its children', () => {
const spy = jest.fn();

const grandchild = createMachine({
Expand Down Expand Up @@ -1873,8 +1657,8 @@ describe('entry/exit actions', () => {
interpreter.stop();
});

it('should execute referenced custom actions correctly when stopping an interpreter', () => {
let called = false;
it('should note execute referenced custom actions correctly when stopping an interpreter', () => {
const spy = jest.fn();
const parent = createMachine(
{
id: 'parent',
Expand All @@ -1883,20 +1667,18 @@ describe('entry/exit actions', () => {
},
{
actions: {
referencedAction: () => {
called = true;
}
referencedAction: spy
}
}
);

const interpreter = createActor(parent).start();
interpreter.stop();

expect(called).toBe(true);
expect(spy).not.toHaveBeenCalled();
});

it('should execute builtin actions correctly when stopping an interpreter', () => {
it('should not execute builtin actions when stopping an interpreter', () => {
const machine = createMachine(
{
context: {
Expand Down Expand Up @@ -1927,10 +1709,7 @@ describe('entry/exit actions', () => {
const interpreter = createActor(machine).start();
interpreter.stop();

expect(interpreter.getSnapshot().context.executedAssigns).toEqual([
'referenced',
'inline'
]);
expect(interpreter.getSnapshot().context.executedAssigns).toEqual([]);
});

it('should clear all scheduled events when the interpreter gets stopped', () => {
Expand Down Expand Up @@ -1992,10 +1771,10 @@ describe('entry/exit actions', () => {

service.send({ type: 'INITIALIZE_SYNC_SEQUENCE' });

expect(exitActions).toEqual(['foo action', 'bar action']);
expect(exitActions).toEqual(['foo action']);
});

it('should execute exit actions of the settled state of the last initiated microstep after executing all actions from that microstep', () => {
it('should not execute exit actions of the settled state of the last initiated microstep after executing all actions from that microstep', () => {
const executedActions: string[] = [];
const machine = createMachine({
initial: 'foo',
Expand Down Expand Up @@ -2033,8 +1812,7 @@ describe('entry/exit actions', () => {

expect(executedActions).toEqual([
'foo exit action',
'foo transition action',
'bar exit action'
'foo transition action'
]);
});
});
Expand Down
Loading

0 comments on commit 14cb2ed

Please sign in to comment.