Fix add node logic with missingNextNodeMap

This patch include a breaking change to the recorder's event data.

We used to consider mirror.getId will always return the id of the
target node because we keep serialize every node. But if we call
mirror.getId before serialization then bug happened. This could
happen when we get nextId of newly added nodes if its next sibling
was also newly added.
So we have to return -1 as the id of node which was not serialized
and when we building added nodes in the replayer we should handle
this.

For example, nodes el1, el2 were added together and el1's nextId
will be -1 since el2 was not serialized at that moment. Now we
call el1 as a 'missing next node' and not append it into the DOM
tree after building, instead we store it in a missingNextNodeMap.
After a added node in the same mutation was successfully appened
we will check whether it has a previous id and the id was pointed
to some nodes in the map, if so, we will insert that node before
it and delete the node from map.
This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 4c0ced2ba1
commit b7bf5f5fe3
3 changed files with 50 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ import {
playerConfig,
playerMetaData,
viewportResizeDimention,
missingNextNodeMap,
} from '../types';
import { mirror } from '../utils';
@@ -216,6 +217,8 @@ export class Replayer {
parent.removeChild(target);
}
});
const missingNextNodeMap: missingNextNodeMap = {};
d.adds.forEach(mutation => {
const target = buildNodeWithSN(
mutation.node,
@@ -233,6 +236,14 @@ export class Replayer {
next = mirror.getNode(mutation.nextId) as Node;
}
if (mutation.nextId === -1) {
missingNextNodeMap[mutation.node.id] = {
node: target,
mutation,
};
return;
}
if (previous && previous.nextSibling) {
parent.insertBefore(target, previous.nextSibling);
} else if (next) {
@@ -240,7 +251,18 @@ export class Replayer {
} else {
parent.appendChild(target);
}
if (mutation.previousId) {
this.resolveMissingNode(
missingNextNodeMap,
parent,
target,
mutation.previousId,
);
}
});
// TODO: assert missingNextNodeMap has no key after resolve
d.texts.forEach(mutation => {
const target = (mirror.getNode(mutation.id) as Node) as Text;
target.textContent = mutation.value;
@@ -320,6 +342,22 @@ export class Replayer {
}
}
private resolveMissingNode(
map: missingNextNodeMap,
parent: Node,
target: Node,
previousId: number,
) {
if (map[previousId]) {
const { node, mutation } = map[previousId];
parent.insertBefore(node, target);
delete map[mutation.node.id];
if (mutation.previousId) {
this.resolveMissingNode(map, parent, node as Node, mutation.previousId);
}
}
}
private hoverElements(el: Element) {
this.iframe
.contentDocument!.querySelectorAll('.\\:hover')

View File

@@ -231,3 +231,10 @@ export type playerConfig = {
export type playerMetaData = {
totalTime: number;
};
export type missingNextNodeMap = {
[id: number]: {
node: Node;
mutation: addedNodeMutation;
};
};

View File

@@ -18,7 +18,11 @@ export function on(
export const mirror: Mirror = {
map: {},
getId(n) {
return n.__sn && n.__sn.id;
// if n is not a serialized INode, use -1 as its id.
if (!n.__sn) {
return -1;
}
return n.__sn.id;
},
getNode(id) {
return mirror.map[id] || null;