diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 4599d60c4..3e61e637d 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "16.1.3", + "version": "16.1.2-beta.2", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/nodes/index.ts b/tracker/tracker/src/main/app/nodes/index.ts index 47e913f81..feed5e841 100644 --- a/tracker/tracker/src/main/app/nodes/index.ts +++ b/tracker/tracker/src/main/app/nodes/index.ts @@ -98,6 +98,13 @@ export default class Nodes { return id } + unregisterNodeById(id: number): void { + const node = this.nodes.get(id) + if (node) { + this.unregisterNode(node) + } + } + cleanTree() { // sadly we keep empty items in array here resulting in some memory still being used // but its still better than keeping dead nodes or undef elements diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index 054159631..cb1fceb60 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -21,6 +21,7 @@ import { hasTag, isCommentNode, } from '../guards.js' +import vElTree from './vTree.js' const iconCache = {} const svgUrlCache = {} @@ -181,6 +182,11 @@ enum RecentsType { } export default abstract class Observer { + /** object tree where key is node id, value is null if it has no children, or object with same structure */ + private readonly vTree = new vElTree((id: number) => { + this.app.nodes.unregisterNodeById(id) + this.app.send(RemoveNode(id)) + }) private readonly observer: MutationObserver private readonly commited: Array = [] private readonly recents: Map = new Map() @@ -412,6 +418,7 @@ export default abstract class Observer { if (id !== undefined && this.recents.get(id) === RecentsType.Removed) { // Sending RemoveNode only for parent to maintain this.app.send(RemoveNode(id)) + this.vTree.removeNode(id) // Unregistering all the children in order to clear the memory const walker = document.createTreeWalker( @@ -506,7 +513,7 @@ export default abstract class Observer { ;(el as HTMLElement | SVGElement).style.width = `${width}px` ;(el as HTMLElement | SVGElement).style.height = `${height}px` } - + this.vTree.addNode(id, parentID ?? null) this.app.send(CreateElementNode(id, parentID, index, el.tagName, isSVGElement(node))) } for (let i = 0; i < el.attributes.length; i++) { @@ -514,6 +521,7 @@ export default abstract class Observer { this.sendNodeAttribute(id, el, attr.nodeName, attr.value) } } else if (isTextNode(node)) { + this.vTree.addNode(id, parentID ?? null) // for text node id != 0, hence parentID !== undefined and parent is Element this.app.send(CreateTextNode(id, parentID as number, index)) this.sendNodeData(id, parent as Element, node.data) @@ -586,5 +594,6 @@ export default abstract class Observer { disconnect(): void { this.observer.disconnect() this.clear() + this.vTree.clearAll() } } diff --git a/tracker/tracker/src/main/app/observer/vTree.ts b/tracker/tracker/src/main/app/observer/vTree.ts new file mode 100644 index 000000000..852fd16a8 --- /dev/null +++ b/tracker/tracker/src/main/app/observer/vTree.ts @@ -0,0 +1,122 @@ +export default class VirtualNodeTree { + tree: any = { 0: null } + nodeParents: any = { 0: null} + constructor( + private readonly onRemoveNode: (id: number) => void + ) {} + + addNode(id: number, parentId: number | null = null) { + this.nodeParents[id] = parentId; + console.log(id, parentId, this.tree, this.nodeParents) + if (parentId === null) { + this.tree[id] = null; + } else { + const parentNode = this.findNode(parentId); + if (parentNode === null) { + this.updateNode(parentId, {}); + } + + this.findNode(parentId)[id] = null; + } + } + + removeNode(id: number) { + console.log('del', id, this) + if (!(id in this.nodeParents)) { + // throw new Error(`Node ${id} doesn't exist`); ? since its just for tracking, nothing + return; + } + + const childrenIds = Object.keys(this.nodeParents).filter( + nodeId => this.nodeParents[nodeId] === id + ); + + for (const childId of childrenIds) { + this.removeNode(parseInt(childId)); + } + + const parentId = this.nodeParents[id]; + + if (parentId === null) { + delete this.tree[id]; + } else { + const parentNode = this.findNode(parentId); + delete parentNode[id]; + this.onRemoveNode(id); + if (Object.keys(parentNode).length === 0) { + this.updateNode(parentId, null); + } + } + delete this.nodeParents[id]; + } + + getPath(id: number) { + if (!(id in this.nodeParents) && id !== 0) { + // throw new Error(`Node ${id} doesn't exist`); + return; + } + + const path: any[] = []; + let currentId = id; + + while (currentId !== null) { + path.unshift(currentId); + currentId = this.nodeParents[currentId]; + } + + return path.join('.'); + } + + findNode(id: number) { + const path = this.getPath(id); + if (!path) return; + return this.getNodeByPath(path); + } + + updateNode(id: number, value: any) { + const path = this.getPath(id); + if (!path) return; + this.setNodeByPath(path, value); + } + + getNodeByPath(path: string) { + const ids = path.split('.'); + let node = this.tree; + + for (const id of ids) { + node = node[id]; + if (node === undefined) return null; + } + + return node; + } + + setNodeByPath(path: string, value: number) { + const ids = path.split('.'); + let node = this.tree; + + for (let i = 0; i < ids.length - 1; i++) { + node = node[ids[i]]; + } + + node[ids[ids.length - 1]] = value; + } + + clearAll = () => { + this.tree = {}; + this.nodeParents = {}; + } +} + +// TODO add tests +// const tree = new VirtualNodeTree(); + +// tree.addNode(0); +// tree.addNode(1, 0); +// tree.addNode(2, 1); +// tree.addNode(3, 1); +// tree.addNode(4, 3); +// console.log({ ...tree.tree }); + +// tree.removeNode(1) +// console.log(tree.tree)