From 0fb8fc4e0b31402b917902a7843c81ef90f45abd Mon Sep 17 00:00:00 2001 From: Yanzhen Yu Date: Fri, 2 Nov 2018 19:23:03 +0800 Subject: [PATCH] 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. --- src/replay/index.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/types.ts | 7 +++++++ src/utils.ts | 6 +++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/replay/index.ts b/src/replay/index.ts index fd922e43..134f55af 100644 --- a/src/replay/index.ts +++ b/src/replay/index.ts @@ -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') diff --git a/src/types.ts b/src/types.ts index c3275322..df29c00e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -231,3 +231,10 @@ export type playerConfig = { export type playerMetaData = { totalTime: number; }; + +export type missingNextNodeMap = { + [id: number]: { + node: Node; + mutation: addedNodeMutation; + }; +}; diff --git a/src/utils.ts b/src/utils.ts index 66e5c85c..afaa1d6a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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;