Skip to content

Fix edge case where oldChild can become detach causing the recursive …#48

Merged
pendo-ops merged 1 commit intopendo-mainfrom
ai-fix-detached-node-in-diff-children-122273
Jul 22, 2025
Merged

Fix edge case where oldChild can become detach causing the recursive …#48
pendo-ops merged 1 commit intopendo-mainfrom
ai-fix-detached-node-in-diff-children-122273

Conversation

@arivyiii
Copy link
Copy Markdown

@arivyiii arivyiii commented May 27, 2025

…function to end early


This change is Reviewable

@kode-toad
Copy link
Copy Markdown

kode-toad bot commented May 27, 2025

Failed pendo.io/jira-ref check. Please correct Jira references in the following commits:

8436691

Copy link
Copy Markdown

@megboehlert megboehlert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 1 files at r1, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @guntherjh and @juliecheng)

Copy link
Copy Markdown

@guntherjh guntherjh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix seems fine. Any chance you can add test coverage?

Reviewed 1 of 1 files at r1, all commit messages.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @juliecheng)

Copy link
Copy Markdown
Author

@arivyiii arivyiii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah i can add them in.

Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on @juliecheng)

@arivyiii arivyiii force-pushed the ai-fix-detached-node-in-diff-children-122273 branch from 8436691 to e467a7e Compare May 30, 2025 21:08
@kode-toad
Copy link
Copy Markdown

kode-toad bot commented May 30, 2025

Failed pendo.io/jira-ref check. Please correct Jira references in the following commits:

e467a7e

Copy link
Copy Markdown

@juliecheng juliecheng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 3 of 3 files at r2, all commit messages.
Reviewable status: all files reviewed, 1 unresolved discussion (waiting on @arivyiii)


packages/rrdom/test/diff.test.ts line 1188 at r2 (raw file):

    });

    it.only('should maintain correct order when duplicate node is found causing nextSibling traversal desync', () => {

nit this needs to be removed

Copy link
Copy Markdown

@juliecheng juliecheng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: all files reviewed, 1 unresolved discussion (waiting on @arivyiii)


packages/rrdom/test/diff.test.ts line 1188 at r2 (raw file):

Previously, juliecheng wrote…

nit this needs to be removed

by this i mean only :oops:

@arivyiii arivyiii force-pushed the ai-fix-detached-node-in-diff-children-122273 branch from e467a7e to 466187e Compare June 2, 2025 14:06
@kode-toad
Copy link
Copy Markdown

kode-toad bot commented Jun 2, 2025

Failed pendo.io/jira-ref check. Please correct Jira references in the following commits:

466187e

Copy link
Copy Markdown

@guntherjh guntherjh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1 of 3 files at r2.
Reviewable status: 2 of 3 files reviewed, 2 unresolved discussions (waiting on @arivyiii and @juliecheng)


packages/rrdom/src/diff.ts line 537 at r3 (raw file):

    
      // Insert the new node before the current oldChild to preserve correct order.
      oldTree.insertBefore(newNode, oldChild);

The rest of this function uses handleInsertBefore wrapped in a try/catch. handleInsertBefore calls insertBefore under the hood, but I'm thinking we should follow the existing pattern and use handleInsertBefore wrapped in a try/catch as well.

On that note though, I'm still trying to wrap my head around all that diffChildren and the rest of the diff* functions actually do. I'm starting to think we want this type of check higher up and not part of the recursive while loop 🤔

@arivyiii
Copy link
Copy Markdown
Author

arivyiii commented Jun 2, 2025

packages/rrdom/src/diff.ts line 537 at r3 (raw file):

Previously, guntherjh (John Henry Gunther) wrote…

The rest of this function uses handleInsertBefore wrapped in a try/catch. handleInsertBefore calls insertBefore under the hood, but I'm thinking we should follow the existing pattern and use handleInsertBefore wrapped in a try/catch as well.

On that note though, I'm still trying to wrap my head around all that diffChildren and the rest of the diff* functions actually do. I'm starting to think we want this type of check higher up and not part of the recursive while loop 🤔

I can try to do my best as least with how the diff and diffChildren is suppose to work.

Diff at a high level is responsible for comparing the nodes using the newTree as the source of truth which represents the virtual DOM. If oldTree and newTree are out of sync the goal is for oldTree to resemble newTree by the end of the diff call. This includes making sure properties and attributes are properly applied to the real DOM.

It can be broken down into three levels of responsibility but I'll focus on the main two since they're related to this bug.

diffBeforeUpdatingChildren is responsible for making sure the current nodes line up. If they do not it creates a new node from newTree and replaces oldTree with it in the DOM.

This is where the issue is occuring for this bug. After replacing the node the original reference to oldTree which was passed by reference becomes outdated. The parent that called diff still holds onto the original oldTree reference which has now been replaced and is no longer part of the DOM. This causes problems because the node becomes detached and no longer has a parentNode or nextSibling. That could potentially break traversal in diffChildren if diff is called from within that function.

diffChildren this is the big one but it's main job is to recursively diff all the children and ensure that all of their nodes line up. the first half of the function does a pretty good job at handling this by comparing the first and last and iterating inwards until it's done comparing, along the process when it finds a diff it handles it by removing the old node and insert a node where the oldNode used to be, which looks to be a much safer operation than the replaceChild one that gets used in the diffBeforeUpdatingChildren.

After it finishes aligning the nodes it will recursively iterate through each of the old nodes and new nodes. This is the part where properties and attributes get attached to the child nodes.

Traversal stops here as soon as either oldChild or newChild becomes null. So even if there is more data left in newChild it assumes at this point that the nodes are aligned and it is safe to bail out of the loop

Copy link
Copy Markdown

@guntherjh guntherjh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 2 of 3 files reviewed, 3 unresolved discussions (waiting on @arivyiii and @juliecheng)


packages/rrdom/src/diff.ts line 537 at r3 (raw file):

Previously, arivyiii (Allen Ivy) wrote…

I can try to do my best as least with how the diff and diffChildren is suppose to work.

Diff at a high level is responsible for comparing the nodes using the newTree as the source of truth which represents the virtual DOM. If oldTree and newTree are out of sync the goal is for oldTree to resemble newTree by the end of the diff call. This includes making sure properties and attributes are properly applied to the real DOM.

It can be broken down into three levels of responsibility but I'll focus on the main two since they're related to this bug.

diffBeforeUpdatingChildren is responsible for making sure the current nodes line up. If they do not it creates a new node from newTree and replaces oldTree with it in the DOM.

This is where the issue is occuring for this bug. After replacing the node the original reference to oldTree which was passed by reference becomes outdated. The parent that called diff still holds onto the original oldTree reference which has now been replaced and is no longer part of the DOM. This causes problems because the node becomes detached and no longer has a parentNode or nextSibling. That could potentially break traversal in diffChildren if diff is called from within that function.

diffChildren this is the big one but it's main job is to recursively diff all the children and ensure that all of their nodes line up. the first half of the function does a pretty good job at handling this by comparing the first and last and iterating inwards until it's done comparing, along the process when it finds a diff it handles it by removing the old node and insert a node where the oldNode used to be, which looks to be a much safer operation than the replaceChild one that gets used in the diffBeforeUpdatingChildren.

After it finishes aligning the nodes it will recursively iterate through each of the old nodes and new nodes. This is the part where properties and attributes get attached to the child nodes.

Traversal stops here as soon as either oldChild or newChild becomes null. So even if there is more data left in newChild it assumes at this point that the nodes are aligned and it is safe to bail out of the loop

My lack of understanding here is definitely a "me" problem, but I really appreciate the explanation (it helps a ton!). My biggest worry was around how these changes would impact time complexity, but looking a bit closer, I don't think it would have much (if any) impact since the mismatch case continues.

I'm going to keep poking around in here for my own understanding. I'm also seeing some new test failures in diff.test.ts that aren't present in pendo-main. Note I am seeing can build from a html containing nested shadow doms also fail in upstream so I wouldn't worry about that one.


packages/rrdom/test/diff.test.ts line 1250 at r3 (raw file):

        expect(finalIds).toEqual(oldList.map(n => String(n.id)));
      });
      

I'd remove these extra spaces

@arivyiii arivyiii force-pushed the ai-fix-detached-node-in-diff-children-122273 branch from 466187e to 8c4a9e6 Compare June 4, 2025 16:41
@kode-toad
Copy link
Copy Markdown

kode-toad bot commented Jun 4, 2025

Failed pendo.io/jira-ref check. Please correct Jira references in the following commits:

8c4a9e6

Copy link
Copy Markdown

@guntherjh guntherjh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 2 of 2 files at r4, all commit messages.
Reviewable status: all files reviewed, 2 unresolved discussions (waiting on @arivyiii)

@arivyiii arivyiii force-pushed the ai-fix-detached-node-in-diff-children-122273 branch from 8c4a9e6 to 77cf879 Compare June 10, 2025 18:19
@kode-toad
Copy link
Copy Markdown

kode-toad bot commented Jun 10, 2025

Failed pendo.io/jira-ref check. Please correct Jira references in the following commits:

77cf879

@guntherjh
Copy link
Copy Markdown

[merge]

@pendo-ops pendo-ops merged commit e79e621 into pendo-main Jul 22, 2025
5 of 12 checks passed
@kode-toad
Copy link
Copy Markdown

kode-toad bot commented Jul 22, 2025

Branch ai-fix-detached-node-in-diff-children-122273 deleted

@kode-toad kode-toad bot deleted the ai-fix-detached-node-in-diff-children-122273 branch July 22, 2025 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants