Skip to content

Commit

Permalink
Generate xstate.done.state.* events recursively for nested parallel…
Browse files Browse the repository at this point in the history
… states (#4358)

* Generate `xstate.done.state.*` events recursively for nested parallel states

* clean up the test case slightly

* add more test cases
  • Loading branch information
Andarist committed Oct 17, 2023
1 parent 84c46c1 commit 03ac5c0
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-suns-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'xstate': minor
---

`xstate.done.state.*` events will now be generated recursively for all parallel states on the ancestors path.
4 changes: 3 additions & 1 deletion packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ export class StateMachine<
...(state as any),
value: resolveStateValue(this.root, state.value),
configuration,
status: isInFinalState(configuration) ? 'done' : state.status
status: isInFinalState(configurationSet, this.root)
? 'done'
: state.status
});
}

Expand Down
29 changes: 14 additions & 15 deletions packages/core/src/stateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,12 @@ export function getStateValue(
}

export function isInFinalState(
configuration: Array<AnyStateNode>,
stateNode: AnyStateNode = configuration[0].machine.root
configuration: Set<AnyStateNode>,
stateNode: AnyStateNode
): boolean {
if (stateNode.type === 'compound') {
return getChildren(stateNode).some(
(s) => s.type === 'final' && configuration.includes(s)
(s) => s.type === 'final' && configuration.has(s)
);
}
if (stateNode.type === 'parallel') {
Expand Down Expand Up @@ -1114,7 +1114,7 @@ function microstepProcedure(

const nextConfiguration = [...mutConfiguration];

const done = isInFinalState(nextConfiguration);
const done = isInFinalState(mutConfiguration, currentState.machine.root);

if (done) {
nextState = resolveActionsAndContext(
Expand Down Expand Up @@ -1222,18 +1222,17 @@ function enterStates(
)
);

if (parent.parent) {
const grandparent = parent.parent;

if (grandparent.type === 'parallel') {
if (
getChildren(grandparent).every((parentNode) =>
isInFinalState([...mutConfiguration], parentNode)
)
) {
internalQueue.push(createDoneStateEvent(grandparent.id));
}
let ancestorMarker: typeof parent | undefined = parent.parent;
while (ancestorMarker) {
if (
ancestorMarker.type === 'parallel' &&
isInFinalState(mutConfiguration, ancestorMarker)
) {
internalQueue.push(createDoneStateEvent(ancestorMarker.id));
ancestorMarker = ancestorMarker.parent;
continue;
}
break;
}
}
}
Expand Down
252 changes: 252 additions & 0 deletions packages/core/test/final.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,4 +231,256 @@ describe('final states', () => {

expect(spy).toHaveBeenCalledWith(1);
});

it('should emit a done state event for a parallel state when its parallel children reach their final states', () => {
const machine = createMachine({
initial: 'first',
states: {
first: {
type: 'parallel',
states: {
alpha: {
type: 'parallel',
states: {
one: {
initial: 'start',
states: {
start: {
on: {
finish_one_alpha: 'finish'
}
},
finish: {
type: 'final'
}
}
},
two: {
initial: 'start',
states: {
start: {
on: {
finish_two_alpha: 'finish'
}
},
finish: {
type: 'final'
}
}
}
}
},
beta: {
type: 'parallel',
states: {
third: {
initial: 'start',
states: {
start: {
on: {
finish_three_beta: 'finish'
}
},
finish: {
type: 'final'
}
}
},
fourth: {
initial: 'start',
states: {
start: {
on: {
finish_four_beta: 'finish'
}
},
finish: {
type: 'final'
}
}
}
}
}
},
onDone: 'done'
},
done: {
type: 'final'
}
}
});

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

actorRef.send({
type: 'finish_one_alpha'
});
actorRef.send({
type: 'finish_two_alpha'
});
actorRef.send({
type: 'finish_three_beta'
});
actorRef.send({
type: 'finish_four_beta'
});

expect(actorRef.getSnapshot().status).toBe('done');
});

it('should emit a done state event for a parallel state when its compound child reaches its final state when the other parallel child region is already in its final state', () => {
const machine = createMachine({
initial: 'first',
states: {
first: {
type: 'parallel',
states: {
alpha: {
type: 'parallel',
states: {
one: {
initial: 'start',
states: {
start: {
on: {
finish_one_alpha: 'finish'
}
},
finish: {
type: 'final'
}
}
},
two: {
initial: 'start',
states: {
start: {
on: {
finish_two_alpha: 'finish'
}
},
finish: {
type: 'final'
}
}
}
}
},
beta: {
initial: 'three',
states: {
three: {
on: {
finish_beta: 'finish'
}
},
finish: {
type: 'final'
}
}
}
},
onDone: 'done'
},
done: {
type: 'final'
}
}
});

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

// reach final state of a parallel state
actorRef.send({
type: 'finish_one_alpha'
});
actorRef.send({
type: 'finish_two_alpha'
});

// reach final state of a compound state
actorRef.send({
type: 'finish_beta'
});

expect(actorRef.getSnapshot().status).toBe('done');
});

it('should emit a done state event for a parallel state when its parallel child reaches its final state when the other compound child region is already in its final state', () => {
const machine = createMachine({
initial: 'first',
states: {
first: {
type: 'parallel',
states: {
alpha: {
type: 'parallel',
states: {
one: {
initial: 'start',
states: {
start: {
on: {
finish_one_alpha: 'finish'
}
},
finish: {
type: 'final'
}
}
},
two: {
initial: 'start',
states: {
start: {
on: {
finish_two_alpha: 'finish'
}
},
finish: {
type: 'final'
}
}
}
}
},
beta: {
initial: 'three',
states: {
three: {
on: {
finish_beta: 'finish'
}
},
finish: {
type: 'final'
}
}
}
},
onDone: 'done'
},
done: {
type: 'final'
}
}
});

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

// reach final state of a compound state
actorRef.send({
type: 'finish_beta'
});

// reach final state of a parallel state
actorRef.send({
type: 'finish_one_alpha'
});
actorRef.send({
type: 'finish_two_alpha'
});

expect(actorRef.getSnapshot().status).toBe('done');
});
});

0 comments on commit 03ac5c0

Please sign in to comment.