feature: make mirror independent in Replayer (#407)

Co-authored-by: zhaoziqiu <zhaoziqiu@meituan.com>
Co-authored-by: yz-yu <yanzhen@smartx.com>
This commit is contained in:
Ziqiu Zhao
2026-04-01 12:00:00 +08:00
committed by GitHub
parent e66b5d9af6
commit 394d94e6c7
2 changed files with 80 additions and 57 deletions

View File

@@ -26,13 +26,14 @@ import {
scrollData,
inputData,
canvasMutationData,
Mirror,
ElementState,
LogReplayConfig,
logData,
ReplayLogger,
} from '../types';
import {
mirror,
createMirror,
polyfill,
TreeIndex,
queueToResolveTrees,
@@ -120,6 +121,7 @@ export class Replayer {
private imageMap: Map<eventWithTime, HTMLImageElement> = new Map();
private mirror: Mirror = createMirror();
/** The first time the player is playing. */
private firstPlayedEvent: eventWithTime | null = null;
@@ -178,10 +180,27 @@ export class Replayer {
for (const d of inputMap.values()) {
this.applyInput(d);
}
for (const [frag, parent] of this.fragmentParentMap.entries()) {
this.mirror.map[parent.__sn.id] = parent;
/**
* If we have already set value attribute on textarea,
* then we could not apply text content as default value any more.
*/
if (
parent.__sn.type === NodeType.Element &&
parent.__sn.tagName === 'textarea' &&
frag.textContent
) {
((parent as unknown) as HTMLTextAreaElement).value = frag.textContent;
}
parent.appendChild(frag);
}
this.fragmentParentMap.clear();
});
this.emitter.on(ReplayerEvents.PlayBack, () => {
this.firstPlayedEvent = null;
mirror.reset();
this.mirror.reset();
});
const timer = new Timer([], config?.speed || defaultConfig.speed);
@@ -557,7 +576,7 @@ export class Replayer {
}
this.legacy_missingNodeRetryMap = {};
const collected: AppendedIframe[] = [];
mirror.map = rebuild(event.data.node, {
this.mirror.map = rebuild(event.data.node, {
doc: this.iframe.contentDocument,
afterAppend: (builtNode) => {
this.collectIframeAndAttachDocument(collected, builtNode);
@@ -629,7 +648,7 @@ export class Replayer {
}
buildNodeWithSN(mutation.node, {
doc: iframeEl.contentDocument!,
map: mirror.map,
map: this.mirror.map,
hackCss: true,
skipChild: false,
afterAppend: (builtNode) => {
@@ -810,7 +829,7 @@ export class Replayer {
break;
}
const event = new Event(MouseInteractions[d.type].toLowerCase());
const target = mirror.getNode(d.id);
const target = this.mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
@@ -893,7 +912,7 @@ export class Replayer {
break;
}
case IncrementalSource.MediaInteraction: {
const target = mirror.getNode(d.id);
const target = this.mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
@@ -921,7 +940,7 @@ export class Replayer {
break;
}
case IncrementalSource.StyleSheetRule: {
const target = mirror.getNode(d.id);
const target = this.mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
@@ -993,7 +1012,7 @@ export class Replayer {
if (!this.config.UNSAFE_replayCanvas) {
return;
}
const target = mirror.getNode(d.id);
const target = this.mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
@@ -1061,11 +1080,11 @@ export class Replayer {
private applyMutation(d: mutationData, useVirtualParent: boolean) {
d.removes.forEach((mutation) => {
const target = mirror.getNode(mutation.id);
const target = this.mirror.getNode(mutation.id);
if (!target) {
return this.warnNodeNotFound(d, mutation.id);
}
let parent: INode | null | ShadowRoot = mirror.getNode(mutation.parentId);
let parent: INode | null | ShadowRoot = this.mirror.getNode(mutation.parentId);
if (!parent) {
return this.warnNodeNotFound(d, mutation.parentId);
}
@@ -1073,7 +1092,7 @@ export class Replayer {
parent = parent.shadowRoot;
}
// target may be removed with its parents before
mirror.removeNodeFromMap(target);
this.mirror.removeNodeFromMap(target);
if (parent) {
const realParent =
'__sn' in parent ? this.fragmentParentMap.get(parent) : undefined;
@@ -1100,10 +1119,10 @@ export class Replayer {
const queue: addedNodeMutation[] = [];
// next not present at this moment
function nextNotInDOM(mutation: addedNodeMutation) {
const nextNotInDOM = (mutation: addedNodeMutation) => {
let next: Node | null = null;
if (mutation.nextId) {
next = mirror.getNode(mutation.nextId) as Node;
next = this.mirror.getNode(mutation.nextId) as Node;
}
// next not present at this moment
if (
@@ -1115,13 +1134,13 @@ export class Replayer {
return true;
}
return false;
}
};
const appendNode = (mutation: addedNodeMutation) => {
if (!this.iframe.contentDocument) {
return console.warn('Looks like your replayer has been destroyed.');
}
let parent: INode | null | ShadowRoot = mirror.getNode(mutation.parentId);
let parent: INode | null | ShadowRoot = this.mirror.getNode(mutation.parentId);
if (!parent) {
if (mutation.node.type === NodeType.Document) {
// is newly added document, maybe the document node of an iframe
@@ -1153,7 +1172,7 @@ export class Replayer {
!hasIframeChild
) {
const virtualParent = (document.createDocumentFragment() as unknown) as INode;
mirror.map[mutation.parentId] = virtualParent;
this.mirror.map[mutation.parentId] = virtualParent;
this.fragmentParentMap.set(virtualParent, parent);
// store the state, like scroll position, of child nodes before they are unmounted from dom
@@ -1172,21 +1191,21 @@ export class Replayer {
let previous: Node | null = null;
let next: Node | null = null;
if (mutation.previousId) {
previous = mirror.getNode(mutation.previousId) as Node;
previous = this.mirror.getNode(mutation.previousId) as Node;
}
if (mutation.nextId) {
next = mirror.getNode(mutation.nextId) as Node;
next = this.mirror.getNode(mutation.nextId) as Node;
}
if (nextNotInDOM(mutation)) {
return queue.push(mutation);
}
if (mutation.node.rootId && !mirror.getNode(mutation.node.rootId)) {
if (mutation.node.rootId && !this.mirror.getNode(mutation.node.rootId)) {
return;
}
const targetDoc = mutation.node.rootId
? mirror.getNode(mutation.node.rootId)
? this.mirror.getNode(mutation.node.rootId)
: this.iframe.contentDocument;
if (isIframeINode(parent)) {
this.attachDocumentToIframe(mutation, parent);
@@ -1194,7 +1213,7 @@ export class Replayer {
}
const target = buildNodeWithSN(mutation.node, {
doc: targetDoc as Document,
map: mirror.map,
map: this.mirror.map,
skipChild: true,
hackCss: true,
}) as INode;
@@ -1272,7 +1291,7 @@ export class Replayer {
break;
}
for (const tree of resolveTrees) {
let parent = mirror.getNode(tree.value.parentId);
let parent = this.mirror.getNode(tree.value.parentId);
if (!parent) {
this.debug(
'Drop resolve tree since there is no parent for the root node.',
@@ -1291,7 +1310,7 @@ export class Replayer {
}
d.texts.forEach((mutation) => {
let target = mirror.getNode(mutation.id);
let target = this.mirror.getNode(mutation.id);
if (!target) {
return this.warnNodeNotFound(d, mutation.id);
}
@@ -1304,7 +1323,7 @@ export class Replayer {
target.textContent = mutation.value;
});
d.attributes.forEach((mutation) => {
let target = mirror.getNode(mutation.id);
let target = this.mirror.getNode(mutation.id);
if (!target) {
return this.warnNodeNotFound(d, mutation.id);
}
@@ -1334,7 +1353,7 @@ export class Replayer {
}
private applyScroll(d: scrollData) {
const target = mirror.getNode(d.id);
const target = this.mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
@@ -1358,7 +1377,7 @@ export class Replayer {
}
private applyInput(d: inputData) {
const target = mirror.getNode(d.id);
const target = this.mirror.getNode(d.id);
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
@@ -1453,7 +1472,7 @@ export class Replayer {
}
private moveAndHover(d: incrementalData, x: number, y: number, id: number) {
const target = mirror.getNode(id);
const target = this.mirror.getNode(id);
if (!target) {
return this.debugNodeNotFound(d, id);
}
@@ -1546,7 +1565,7 @@ export class Replayer {
* @param parent real parent element
*/
private restoreRealParent(frag: INode, parent: INode) {
mirror.map[parent.__sn.id] = parent;
this.mirror.map[parent.__sn.id] = parent;
/**
* If we have already set value attribute on textarea,
* then we could not apply text content as default value any more.

View File

@@ -34,35 +34,39 @@ export function on(
return () => target.removeEventListener(type, fn, options);
}
export const mirror: Mirror = {
map: {},
getId(n) {
// 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;
},
// TODO: use a weakmap to get rid of manually memory management
removeNodeFromMap(n) {
const id = n.__sn && n.__sn.id;
delete mirror.map[id];
if (n.childNodes) {
n.childNodes.forEach((child) =>
mirror.removeNodeFromMap((child as Node) as INode),
);
}
},
has(id) {
return mirror.map.hasOwnProperty(id);
},
reset() {
mirror.map = {};
},
};
export function createMirror (): Mirror {
return {
map: {},
getId(n) {
// if n is not a serialized INode, use -1 as its id.
if (!n.__sn) {
return -1;
}
return n.__sn.id;
},
getNode(id) {
return this.map[id] || null;
},
// TODO: use a weakmap to get rid of manually memory management
removeNodeFromMap(n) {
const id = n.__sn && n.__sn.id;
delete this.map[id];
if (n.childNodes) {
n.childNodes.forEach((child) =>
this.removeNodeFromMap((child as Node) as INode),
);
}
},
has(id) {
return this.map.hasOwnProperty(id);
},
reset() {
this.map = {};
},
};
}
export const mirror: Mirror = createMirror();
// copy from underscore and modified
export function throttle<T>(