-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Fixed an issue with targeted ancestor reentrancy #3424
Conversation
🦋 Changeset detectedLatest commit: 4a300b6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
👇 Click on the image for a new way to code review
Legend |
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 4a300b6:
|
@@ -956,8 +956,8 @@ class StateNode< | |||
const reentryNodes: StateNode<any, any, any, any, any>[] = []; | |||
|
|||
if (!isInternal) { | |||
allNextStateNodes.forEach((n1) => { | |||
reentryNodes.push(...this.getExternalReentryNodes(n1)); | |||
nextStateNodes.forEach((targetNode) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the change here from allNextStateNodes
to nextStateNodes
.
nextStateNodes
represent the target state nodes literally used in the transition definition whereas allNextStateNodes
represent "resolved" targets (initial states etc already resolved). To compute correctly what has to be reentered we need to consider the target states before resolving them.
let [marker, other]: [ | ||
StateNode<TContext, any, TEvent, any, any, any> | undefined, | ||
StateNode<TContext, any, TEvent, any, any, any> | ||
] = targetNode.order > this.order ? [targetNode, this] : [this, targetNode]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
somewhat neat trick with comparing those orders here:
targetNode.order > this.order
means thattargetNode
is either a descendant ofthis
OR it's somewhere outside of it, within the latter siblings of one of its ancestorstargetNode.order < this.order
means thattargetNode
is either the ancestor ofthis
OR it's somewhere outside of it, within the former siblings of one of its ancestors
Since this function returns []
when those two do not form an ancestor<->descendant relationship we can safely rely on this as we are not even concerned here with finding a common ancestor of the two, we only want to find out if one is the ancestor of the other one and collect all nodes in between if that's the case (including the detected ancestor node).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very clever! To prevent any edge-cases, we should ensure that order
is not configurable (I think it technically is right now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there is currently any risk here because order
is a local variable initialized to a static 0
and only assigned within the dfs
call here:
xstate/packages/core/src/StateNode.ts
Lines 374 to 385 in e35493f
// Document order | |
let order = 0; | |
function dfs( | |
stateNode: StateNode<TContext, any, TEvent, any, any, any> | |
): void { | |
stateNode.order = order++; | |
for (const child of getChildren(stateNode)) { | |
dfs(child); | |
} | |
} |
f22dcc0
to
4a300b6
Compare
// it's in a different part of the tree so no states will be reentered for such an external transition | ||
return []; | ||
} | ||
nodes.push(this); | ||
nodes.push(possibleAncestor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Observation - this sort of uses the parent of the found ancestor as the transition domain which means that we are using LCA algorithm here whereas SCXML is using LCCA. I'm not exactly sure why SCXML has switched to LCCA because I think that they have been using LCA at the beginning. It feels to me that LCA is an easier rule and LCCA is a little bit harder to explain so I wonder what made them switch those algorithms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this break any SCXML tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using LCA over LCCA? Sure. Using LCCA can lead to some weird behaviors when parallel states are involved though and I'm not sure why those weird behaviors would be desirable in the first place. We should dig into this more to understand the motivation for using LCCA in UML.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code + test makes sense. We just have to ensure this is working in v5, and maybe even copy the test over.
I've checked - it already works OK there.
That will just happen automatically~ when we get to merging main to next, so no specific additional actions are required here. |
fixes #3409