Skip to content
Open
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
43c5d72
Iterate over the added nodes in 'one pass' so that we don't need to b…
eoghanmurray Feb 10, 2025
ed757b3
Test changes, rearrangement of mutations
eoghanmurray Feb 10, 2025
1d8b37b
Add some ids as I'm interested in tracing these nodes through pushAdd…
eoghanmurray Feb 10, 2025
99da1e4
Do away with the second pass as we can handle shadow DOM in the first…
eoghanmurray Feb 10, 2025
490aea9
Performance oriented refactor focusing on scenario where a large numb…
eoghanmurray Feb 11, 2025
d9587d4
Satisfy typescript which could be smarter here ... we can guarantee t…
eoghanmurray Feb 11, 2025
2a0eedd
Utilize `lastChild` to avoid possibly crawling through hundreds of nodes
eoghanmurray Feb 11, 2025
96ea20d
We've already got `nextSibling` here so can skip a step and avoid the…
eoghanmurray Feb 11, 2025
2aa8597
Test rearrangements in the adds array due to new algorithm; should be…
eoghanmurray Feb 11, 2025
8d4e766
We were calling `inDom` in all cases, so don't do the other ancestor …
eoghanmurray Feb 11, 2025
3b33611
Don't think we're explicitly looking at the slimdom stuff in relation…
eoghanmurray Feb 11, 2025
f260c0d
Add changeset
eoghanmurray Feb 11, 2025
87b091a
Don't think `main` subfolder was ever used as an output target; this …
eoghanmurray Feb 11, 2025
1441ef3
Placate eslint (`while(true)` is a Pythonism rather than do..while) -…
eoghanmurray Feb 11, 2025
b5b04e2
Forgot to add the mutation.html file - also add doctype
eoghanmurray Feb 11, 2025
720e174
Simplify the parentId check, doesn't need to ever by null
eoghanmurray Feb 11, 2025
c166319
Move mutation tests into their own files to demonstrate an idea which…
eoghanmurray Feb 12, 2025
4a751ee
Apply formatting changes
eoghanmurray Feb 12, 2025
7734331
Some inconsequential tests to cover blocking scenarios
eoghanmurray Feb 12, 2025
4c2af79
Was trying to 'catch out' the mutation handling by having siblings pr…
eoghanmurray Feb 12, 2025
8b27440
fixup! Move mutation tests into their own files to demonstrate an ide…
eoghanmurray Feb 12, 2025
453beb0
I can't recreate a scenario for this case in testing, so add a warnin…
eoghanmurray Feb 12, 2025
03f2146
Put each snap file in it's own folder and shorten names
eoghanmurray Feb 12, 2025
9666d32
Satisfy eslint
eoghanmurray Feb 13, 2025
e90b18b
Repeat the mutation tests but with the blocking/ignored nodes already…
eoghanmurray Mar 7, 2025
9a7a47e
Indicate that replay no longer needs the queue, as added nodes should…
eoghanmurray Mar 7, 2025
f9f660c
Merge branch 'master' into pushAddOrder
Juice10 Sep 5, 2025
7d39bbc
Update packages/rrweb/src/replay/index.ts
eoghanmurray Sep 5, 2025
0b0d662
Update packages/rrweb/src/record/mutation.ts
eoghanmurray Sep 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Do away with the second pass as we can handle shadow DOM in the first…
… pass
  • Loading branch information
eoghanmurray committed Mar 7, 2025
commit 99da1e4fcd3603df1781c3cc31868ca164104c5e
179 changes: 12 additions & 167 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import type {
attributeCursor,
removedNodeMutation,
addedNodeMutation,
Optional,
} from '@rrweb/types';
import {
isBlocked,
Expand All @@ -34,104 +33,6 @@ import {
} from '../utils';
import dom from '@rrweb/utils';

type DoubleLinkedListNode = {
previous: DoubleLinkedListNode | null;
next: DoubleLinkedListNode | null;
value: NodeInLinkedList;
};
type NodeInLinkedList = Node & {
__ln: DoubleLinkedListNode;
};

function isNodeInLinkedList(n: Node | NodeInLinkedList): n is NodeInLinkedList {
return '__ln' in n;
}

class DoubleLinkedList {
public length = 0;
public head: DoubleLinkedListNode | null = null;
public tail: DoubleLinkedListNode | null = null;

public get(position: number) {
if (position >= this.length) {
throw new Error('Position outside of list range');
}

let current = this.head;
for (let index = 0; index < position; index++) {
current = current?.next || null;
}
return current;
}

public addNode(n: Node) {
const node: DoubleLinkedListNode = {
value: n as NodeInLinkedList,
previous: null,
next: null,
};
(n as NodeInLinkedList).__ln = node;
if (n.previousSibling && isNodeInLinkedList(n.previousSibling)) {
const current = n.previousSibling.__ln.next;
node.next = current;
node.previous = n.previousSibling.__ln;
n.previousSibling.__ln.next = node;
if (current) {
current.previous = node;
}
} else if (
n.nextSibling &&
isNodeInLinkedList(n.nextSibling) &&
n.nextSibling.__ln.previous
) {
const current = n.nextSibling.__ln.previous;
node.previous = current;
node.next = n.nextSibling.__ln;
n.nextSibling.__ln.previous = node;
if (current) {
current.next = node;
}
} else {
if (this.head) {
this.head.previous = node;
}
node.next = this.head;
this.head = node;
}
if (node.next === null) {
this.tail = node;
}
this.length++;
}

public removeNode(n: NodeInLinkedList) {
const current = n.__ln;
if (!this.head) {
return;
}

if (!current.previous) {
this.head = current.next;
if (this.head) {
this.head.previous = null;
} else {
this.tail = null;
}
} else {
current.previous.next = current.next;
if (current.next) {
current.next.previous = current.previous;
} else {
this.tail = current.previous;
}
}
if (n.__ln) {
delete (n as Optional<NodeInLinkedList, '__ln'>).__ln;
}
this.length--;
}
}

const moveKey = (id: number, parentId: number) => `${id}@${parentId}`;

/**
Expand Down Expand Up @@ -272,11 +173,6 @@ export default class MutationBuffer {
const adds: addedNodeMutation[] = [];
const addedIds = new Set<number>();

/**
* Sometimes child node may be pushed before its newly added
* parent, so we init a queue to store these nodes.
*/
const addList = new DoubleLinkedList();
const getNextId = (n: Node): number | null => {
let ns: Node | null = n;
let nextId: number | null = IGNORED_NODE; // slimDOM: ignored
Expand All @@ -288,14 +184,22 @@ export default class MutationBuffer {
};
const pushAdd = (n: Node) => {
const parent = dom.parentNode(n);
if (!parent || !inDom(n)) {
if (!parent) {
return;
}

const parentId = isShadowRoot(parent)
let parentId = isShadowRoot(parent)
? this.mirror.getId(getShadowHost(n))
: this.mirror.getId(parent);

// If the node is the direct child of a shadow root, we treat the shadow host as its parent node.
if (parentId === -1 && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
const shadowHost = dom.host(parent as ShadowRoot);
parentId = this.mirror.getId(shadowHost);
} else if (!inDom(n)) {
return;
}

let cssCaptured = false;
if (n.nodeType === Node.TEXT_NODE) {
const parentTag = (parent as Element).tagName;
Expand All @@ -311,7 +215,7 @@ export default class MutationBuffer {

const nextId = getNextId(n);
if (parentId === -1 || nextId === -1) {
return addList.addNode(n);
return;
}
const sn = serializeNodeWithId(n, {
doc: this.doc,
Expand Down Expand Up @@ -374,7 +278,7 @@ export default class MutationBuffer {
) {
continue;
}
pushAdd(n);
this.addedSet.add(n);
}

let n = null;
Expand Down Expand Up @@ -406,65 +310,6 @@ export default class MutationBuffer {
}
}

let candidate: DoubleLinkedListNode | null = null;
while (addList.length) {
let node: DoubleLinkedListNode | null = null;
if (candidate) {
const parentId = this.mirror.getId(dom.parentNode(candidate.value));
const nextId = getNextId(candidate.value);
if (parentId !== -1 && nextId !== -1) {
node = candidate;
}
}
if (!node) {
let tailNode = addList.tail;
while (tailNode) {
const _node = tailNode;
tailNode = tailNode.previous;
// ensure _node is defined before attempting to find value
if (_node) {
const parentId = this.mirror.getId(dom.parentNode(_node.value));
const nextId = getNextId(_node.value);

if (nextId === -1) continue;
// nextId !== -1 && parentId !== -1
else if (parentId !== -1) {
node = _node;
break;
}
// nextId !== -1 && parentId === -1 This branch can happen if the node is the child of shadow root
else {
const unhandledNode = _node.value;
const parent = dom.parentNode(unhandledNode);
// If the node is the direct child of a shadow root, we treat the shadow host as its parent node.
if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
const shadowHost = dom.host(parent as ShadowRoot);
const parentId = this.mirror.getId(shadowHost);
if (parentId !== -1) {
node = _node;
break;
}
}
}
}
}
}
if (!node) {
/**
* If all nodes in queue could not find a serialized parent,
* it may be a bug or corner case. We need to escape the
* dead while loop at once.
*/
while (addList.head) {
addList.removeNode(addList.head.value);
}
break;
}
candidate = node.previous;
addList.removeNode(node.value);
pushAdd(node.value);
}

const payload = {
texts: this.texts
.map((text) => {
Expand Down