refactor(tracker): get rid of instanceof checks in observer (use nodeName and nodeType guards)

This commit is contained in:
Alex Kaminskii 2022-06-24 12:17:13 +02:00
parent febdfd72e3
commit 015fe57355
4 changed files with 41 additions and 136 deletions

View file

@ -1,99 +0,0 @@
// TODO: global type
export interface Window extends globalThis.Window {
HTMLInputElement: typeof HTMLInputElement,
HTMLLinkElement: typeof HTMLLinkElement,
HTMLStyleElement: typeof HTMLStyleElement,
SVGStyleElement: typeof SVGStyleElement,
HTMLIFrameElement: typeof HTMLIFrameElement,
Text: typeof Text,
Element: typeof Element,
ShadowRoot: typeof ShadowRoot,
//parent: Window,
}
type WindowConstructor =
Document |
Element |
Text |
ShadowRoot |
HTMLInputElement |
HTMLLinkElement |
HTMLStyleElement |
HTMLIFrameElement
// type ConstructorNames =
// 'Element' |
// 'Text' |
// 'HTMLInputElement' |
// 'HTMLLinkElement' |
// 'HTMLStyleElement' |
// 'HTMLIFrameElement'
type Constructor<T> = { new (...args: any[]): T , name: string };
// TODO: we need a type expert here so we won't have to ignore the lines
// TODO: use it everywhere (static function; export from which file? <-- global Window typing required)
// TODO: most efficient and common way
// Problem: on YouTube there is context[constr.name] undefined for constr=ShadowDom due to some minimisations
export function isInstance<T extends WindowConstructor>(node: Node, constr: Constructor<T>): node is T {
const doc = node.ownerDocument;
if (!doc) { // null if Document
return constr.name === 'Document';
}
let context: Window =
// @ts-ignore (for EI, Safary)
doc.parentWindow ||
doc.defaultView; // TODO: smart global typing for Window object
while(context !== window) {
// @ts-ignore
if (context[constr.name] && node instanceof context[constr.name]) {
return true
}
// @ts-ignore
context = context.parent || window
}
// @ts-ignore
return context[constr.name] ? node instanceof context[constr.name] : node instanceof constr
}
// TODO: ensure 1. it works in every cases (iframes/detached nodes) and 2. the most efficient
export function inDocument(node: Node) {
const doc = node.ownerDocument
if (!doc) { return true } // Document
let current: Node | null = node
while(current) {
if (current === doc) {
return true
} else if(isInstance(current, ShadowRoot)) {
current = current.host
} else {
current = current.parentNode
}
}
return false
}
// export function inDocument(node: Node): boolean {
// // @ts-ignore compatability
// if (node.getRootNode) {
// let root: Node
// while ((root = node.getRootNode()) !== node) {
// ////
// }
// }
// const doc = node.ownerDocument
// if (!doc) { return false }
// if (doc.contains(node)) { return true }
// let context: Window =
// // @ts-ignore (for EI, Safary)
// doc.parentWindow ||
// doc.defaultView;
// while(context.parent && context.parent !== context) {
// if (context.document.contains(node)) {
// return true
// }
// // @ts-ignore
// context = context.parent
// }
// return false;
// }

View file

@ -24,9 +24,10 @@ type TagTypeMap = {
SELECT: HTMLSelectElement
LABEL: HTMLLabelElement
IFRAME: HTMLIFrameElement
STYLE: HTMLStyleElement | SVGStyleElement
STYLE: HTMLStyleElement
style: SVGStyleElement
LINK: HTMLLinkElement
}
export function hasTag<T extends keyof TagTypeMap>(el: Node, tagName: T): el is TagTypeMap[typeof tagName] {
return el.nodeName.toUpperCase() === tagName
return el.nodeName === tagName
}

View file

@ -10,20 +10,17 @@ import {
RemoveNode,
} from "../../../common/messages.js";
import App from "../index.js";
import { isInstance, inDocument } from "../context.js";
function isSVGElement(node: Element): node is SVGElement {
return node.namespaceURI === 'http://www.w3.org/2000/svg';
}
import {
isRootNode,
isTextNode,
isElementNode,
isSVGElement,
hasTag,
} from "../guards.js";
function isIgnored(node: Node): boolean {
if (isInstance(node, Text)) {
return false;
}
if (!isInstance(node, Element)) {
return true;
}
if (isTextNode(node)) { return false }
if (!isElementNode(node)) { return true }
const tag = node.tagName.toUpperCase();
if (tag === 'LINK') {
const rel = node.getAttribute('rel');
@ -39,16 +36,17 @@ function isIgnored(node: Node): boolean {
);
}
function isRootNode(node: Node): boolean {
return isInstance(node, Document) || isInstance(node, ShadowRoot);
function isObservable(node: Node): boolean {
if (isRootNode(node)) { return true }
return !isIgnored(node)
}
function isObservable(node: Node): boolean {
if (isRootNode(node)) {
return true;
}
return !isIgnored(node);
}
/*
TODO:
- fix unbinding logic + send all removals first (ensure sequence is correct)
- use document as a 0-node in the upper context
*/
export default abstract class Observer {
private readonly observer: MutationObserver;
@ -61,11 +59,11 @@ export default abstract class Observer {
constructor(protected readonly app: App, protected readonly isTopContext = false) {
this.observer = new MutationObserver(
this.app.safe((mutations) => {
for (const mutation of mutations) {
for (const mutation of mutations) { // mutations order is sequential
const target = mutation.target;
const type = mutation.type;
if (!isObservable(target) || !inDocument(target)) {
if (!isObservable(target)) {
continue;
}
if (type === 'childList') {
@ -147,7 +145,7 @@ export default abstract class Observer {
}
if (
name === 'value' &&
isInstance(node, HTMLInputElement) &&
hasTag(node, "INPUT") &&
node.type !== 'button' &&
node.type !== 'reset' &&
node.type !== 'submit'
@ -158,7 +156,7 @@ export default abstract class Observer {
this.app.send(new RemoveNodeAttribute(id, name));
return;
}
if (name === 'style' || name === 'href' && isInstance(node, HTMLLinkElement)) {
if (name === 'style' || name === 'href' && hasTag(node, "LINK")) {
this.app.send(new SetNodeAttributeURLBased(id, name, value, this.app.getBaseHref()));
return;
}
@ -169,7 +167,7 @@ export default abstract class Observer {
}
private sendNodeData(id: number, parentElement: Element, data: string): void {
if (isInstance(parentElement, HTMLStyleElement) || isInstance(parentElement, SVGStyleElement)) {
if (hasTag(parentElement, "STYLE") || hasTag(parentElement, "style")) {
this.app.send(new SetCSSDataURLBased(id, data, this.app.getBaseHref()));
return;
}
@ -214,6 +212,7 @@ export default abstract class Observer {
}
}
// A top-consumption function on the infinite lists test. (~1% of performance resources)
private _commitNode(id: number, node: Node): boolean {
if (isRootNode(node)) {
return true;
@ -224,7 +223,7 @@ export default abstract class Observer {
// Disable parent check for the upper context HTMLHtmlElement, because it is root there... (before)
// TODO: get rid of "special" cases (there is an issue with CreateDocument altered behaviour though)
// TODO: Clean the logic (though now it workd fine)
if (!isInstance(node, HTMLHtmlElement) || !this.isTopContext) {
if (!hasTag(node, "HTML") || !this.isTopContext) {
if (parent === null) {
this.unbindNode(node);
return false;
@ -243,6 +242,7 @@ export default abstract class Observer {
return false;
}
}
// From here parentID === undefined if node is top context HTML node
let sibling = node.previousSibling;
while (sibling !== null) {
const siblingID = this.app.nodes.getID(sibling);
@ -254,7 +254,7 @@ export default abstract class Observer {
sibling = sibling.previousSibling;
}
if (sibling === null) {
this.indexes[id] = 0; //
this.indexes[id] = 0;
}
const isNew = this.recents[id];
const index = this.indexes[id];
@ -262,7 +262,7 @@ export default abstract class Observer {
throw 'commitNode: missing node index';
}
if (isNew === true) {
if (isInstance(node, Element)) {
if (isElementNode(node)) {
let el: Element = node
if (parentID !== undefined) {
if (this.app.sanitizer.isMaskedContainer(id)) {
@ -287,7 +287,7 @@ export default abstract class Observer {
const attr = el.attributes[i];
this.sendNodeAttribute(id, el, attr.nodeName, attr.value);
}
} else if (isInstance(node, Text)) {
} else if (isTextNode(node)) {
// for text node id != 0, hence parentID !== undefined and parent is Element
this.app.send(new CreateTextNode(id, parentID as number, index));
this.sendNodeData(id, parent as Element, node.data);
@ -299,7 +299,7 @@ export default abstract class Observer {
}
const attr = this.attributesList[id];
if (attr !== undefined) {
if (!isInstance(node, Element)) {
if (!isElementNode(node)) {
throw 'commitNode: node is not an element';
}
for (const name of attr) {
@ -307,7 +307,7 @@ export default abstract class Observer {
}
}
if (this.textSet.has(id)) {
if (!isInstance(node, Text)) {
if (!isTextNode(node)) {
throw 'commitNode: node is not a text';
}
// for text node id != 0, hence parent is Element

View file

@ -1,6 +1,9 @@
import Observer from "./observer.js";
import { isInstance } from "../context.js";
import type { Window } from "../context.js";
import {
isElementNode,
hasTag,
} from "../guards.js";
import IFrameObserver from "./iframe_observer.js";
import ShadowRootObserver from "./shadow_root_observer.js";
@ -24,7 +27,7 @@ export default class TopObserver extends Observer {
// IFrames
this.app.nodes.attachNodeCallback(node => {
if (isInstance(node, HTMLIFrameElement) &&
if (hasTag(node, "IFRAME") &&
((this.options.captureIFrames && !hasOpenreplayAttribute(node, "obscured"))
|| hasOpenreplayAttribute(node, "capture"))
) {
@ -34,7 +37,7 @@ export default class TopObserver extends Observer {
// ShadowDOM
this.app.nodes.attachNodeCallback(node => {
if (isInstance(node, Element) && node.shadowRoot !== null) {
if (isElementNode(node) && node.shadowRoot !== null) {
this.handleShadowRoot(node.shadowRoot)
}
})