diff --git a/.changeset/eight-terms-hunt.md b/.changeset/eight-terms-hunt.md new file mode 100644 index 0000000000..f5dc1d0eb9 --- /dev/null +++ b/.changeset/eight-terms-hunt.md @@ -0,0 +1,5 @@ +--- +'rrweb': patch +--- + +Fix: Switch from virtual dom to real dom before rebuilding fullsnapshot diff --git a/.changeset/real-masks-explode.md b/.changeset/real-masks-explode.md new file mode 100644 index 0000000000..6703435454 --- /dev/null +++ b/.changeset/real-masks-explode.md @@ -0,0 +1,5 @@ +--- +'rrdom': patch +--- + +Fix: If RRNode appends a single child twice, children of the node will become an infinite link list. diff --git a/packages/rrdom/src/diff.ts b/packages/rrdom/src/diff.ts index e4e06e8db5..78628988e0 100644 --- a/packages/rrdom/src/diff.ts +++ b/packages/rrdom/src/diff.ts @@ -491,7 +491,7 @@ function diffChildren( } else if (newStartIndex > newEndIndex) { for (; oldStartIndex <= oldEndIndex; oldStartIndex++) { const node = oldChildren[oldStartIndex]; - if (!node || !parentNode.contains(node)) continue; + if (!node || node.parentNode !== parentNode) continue; try { parentNode.removeChild(node); replayer.mirror.removeNodeFromMap(node); diff --git a/packages/rrdom/src/document.ts b/packages/rrdom/src/document.ts index 98ee1696e4..a053009a76 100644 --- a/packages/rrdom/src/document.ts +++ b/packages/rrdom/src/document.ts @@ -713,6 +713,8 @@ export type CSSStyleDeclaration = Record & { }; function appendChild(parent: IRRNode, newChild: IRRNode) { + if (newChild.parentNode) newChild.parentNode.removeChild(newChild); + if (parent.lastChild) { parent.lastChild.nextSibling = newChild; newChild.previousSibling = parent.lastChild; @@ -740,6 +742,9 @@ function insertBefore( "Failed to execute 'insertBefore' on 'RRNode': The RRNode before which the new node is to be inserted is not a child of this RRNode.", ); + if (newChild === refChild) return newChild; + if (newChild.parentNode) newChild.parentNode.removeChild(newChild); + newChild.previousSibling = refChild.previousSibling; refChild.previousSibling = newChild; newChild.nextSibling = refChild; diff --git a/packages/rrdom/test/document.test.ts b/packages/rrdom/test/document.test.ts index 421dc036e2..38a35e505f 100644 --- a/packages/rrdom/test/document.test.ts +++ b/packages/rrdom/test/document.test.ts @@ -766,6 +766,22 @@ describe('Basic RRDocument implementation', () => { expect(node.contains(child2)).toBeTruthy(); }); + it('can append a child with parent node', () => { + const node = document.createElement('div'); + const child = document.createElement('span'); + expect(node.appendChild(child)).toBe(child); + expect(node.childNodes).toEqual([child]); + expect(node.appendChild(child)).toBe(child); + expect(node.childNodes).toEqual([child]); + expect(child.parentNode).toBe(node); + + const node1 = document.createElement('div'); + expect(node1.appendChild(child)).toBe(child); + expect(node1.childNodes).toEqual([child]); + expect(child.parentNode).toBe(node1); + expect(node.childNodes).toEqual([]); + }); + it('can insert new child before an existing child', () => { const node = document.createElement('div'); const child1 = document.createElement('h1'); @@ -820,6 +836,25 @@ describe('Basic RRDocument implementation', () => { expect(node.contains(child1)).toBeTruthy(); }); + it('can insert a child with parent node', () => { + const node = document.createElement('div'); + const child1 = document.createElement('h1'); + expect(node.insertBefore(child1, null)).toBe(child1); + expect(node.childNodes).toEqual([child1]); + expect(node.insertBefore(child1, child1)).toBe(child1); + expect(node.childNodes).toEqual([child1]); + expect(child1.parentNode).toEqual(node); + + const node2 = document.createElement('div'); + const child2 = document.createElement('h2'); + expect(node2.insertBefore(child2, null)).toBe(child2); + expect(node2.childNodes).toEqual([child2]); + expect(node2.insertBefore(child1, child2)).toBe(child1); + expect(node2.childNodes).toEqual([child1, child2]); + expect(child1.parentNode).toEqual(node2); + expect(node.childNodes).toEqual([]); + }); + it('can remove an existing child', () => { const node = document.createElement('div'); const child1 = document.createElement('h1'); diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index a6591142c0..52762102ff 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -777,6 +777,16 @@ export class Replayer { } }; + /** + * Normally rebuilding full snapshot should not be under virtual dom environment. + * But if the order of data events has some issues, it might be possible. + * Adding this check to avoid any potential issues. + */ + if (this.usingVirtualDom) { + this.virtualDom.destroyTree(); + this.usingVirtualDom = false; + } + this.mirror.reset(); rebuild(event.data.node, { doc: this.iframe.contentDocument,