feat(player/DOMManager): VirtualNodes insertion prioretization (for styles)

This commit is contained in:
Alex Kaminskii 2023-04-07 18:59:04 +02:00
parent 8eaa5e94fb
commit 98fdf73bd5
3 changed files with 78 additions and 89 deletions

View file

@ -1,6 +1,5 @@
import { Store } from './types'
// (not a type)
export default class SimpleSore<G, S=G> implements Store<G, S> {
constructor(private state: G){}
get(): G {

View file

@ -20,7 +20,10 @@ import {
} from './VirtualDOM';
import { deleteRule, insertRule } from './safeCSSRules';
type HTMLElementWithValue = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
function isStyleVElement(vElem: VElement): vElem is VElement & { node: StyleElement } {
return vElem.tagName.toLowerCase() === "style"
}
const IGNORED_ATTRS = [ "autocomplete" ]
const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/
@ -36,7 +39,6 @@ export default class DOMManager extends ListWalker<Message> {
/** @depreacted since tracker 4.0.2 Mapping by nodeID */
private olStyleSheetsDeprecated: Map<number, OnloadStyleSheet> = new Map()
private stringDict: Record<number,string> = {}
private attrsBacktrack: Message[] = []
private upperBodyId: number = -1;
private nodeScrollManagers: Map<number, ListWalker<SetNodeScroll>> = new Map()
@ -116,9 +118,9 @@ export default class DOMManager extends ListWalker<Message> {
return;
}
if (parent instanceof VElement && parent.tagName.toLowerCase() === "style") {
if (parent instanceof VElement && isStyleVElement(parent)) {
// TODO: if this ever happens? ; Maybe do not send empty TextNodes in tracker
const styleNode = parent.node as StyleElement
const styleNode = parent.node
if (styleNode.sheet &&
styleNode.sheet.cssRules &&
styleNode.sheet.cssRules.length > 0 &&
@ -154,7 +156,7 @@ export default class DOMManager extends ListWalker<Message> {
return
}
// TODO: check if node actually exists on the page, not just in memory
// TODOTODO: check if node actually exists on the page, not just in memory
this.stylesManager.setStyleHandlers(vn.node as HTMLLinkElement, value);
}
if (vn.isSVG && value.startsWith("url(")) {
@ -201,13 +203,13 @@ export default class DOMManager extends ListWalker<Message> {
}
case MType.CreateElementNode: {
const vElem = new VElement(msg.tag, msg.svg)
if (['STYLE', 'style', 'LINK'].includes(msg.tag)) {
vElem.prioritized = true
}
this.vElements.set(msg.id, vElem)
this.insertNode(msg)
this.removeBodyScroll(msg.id, vElem)
this.removeAutocomplete(vElem)
if (['STYLE', 'style', 'LINK'].includes(msg.tag)) { // Styles in priority
vElem.enforceInsertion() //TODOTODO priority mounting instead
}
return
}
case MType.MoveNode:
@ -223,8 +225,7 @@ export default class DOMManager extends ListWalker<Message> {
return
}
case MType.SetNodeAttribute:
if (msg.name === 'href') this.attrsBacktrack.push(msg)
else this.setNodeAttribute(msg)
this.setNodeAttribute(msg)
return
case MType.StringDict:
this.stringDict[msg.key] = msg.value
@ -233,14 +234,11 @@ export default class DOMManager extends ListWalker<Message> {
this.stringDict[msg.nameKey] === undefined && logger.error("No dictionary key for msg 'name': ", msg)
this.stringDict[msg.valueKey] === undefined && logger.error("No dictionary key for msg 'value': ", msg)
if (this.stringDict[msg.nameKey] === undefined || this.stringDict[msg.valueKey] === undefined ) { return }
if (this.stringDict[msg.nameKey] === 'href') this.attrsBacktrack.push(msg)
else {
this.setNodeAttribute({
id: msg.id,
name: this.stringDict[msg.nameKey],
value: this.stringDict[msg.valueKey],
})
}
this.setNodeAttribute({
id: msg.id,
name: this.stringDict[msg.nameKey],
value: this.stringDict[msg.valueKey],
})
return
case MType.RemoveNodeAttribute: {
const vElem = this.vElements.get(msg.id)
@ -280,10 +278,6 @@ export default class DOMManager extends ListWalker<Message> {
const vText = this.vTexts.get(msg.id)
if (!vText) { logger.error("SetNodeData/SetCssData: Node not found", msg); return }
vText.setData(msg.data)
if (msg.tp === MType.SetCssData) { //TODOTODO
vText.applyChanges() // Styles in priority (do we need inlines as well?)
}
return
}
@ -295,8 +289,8 @@ export default class DOMManager extends ListWalker<Message> {
if (!styleSheet) {
const vElem = this.vElements.get(msg.id)
if (!vElem) { logger.error("CssInsertRule: Node not found", msg); return }
if (vElem.tagName.toLowerCase() !== "style") { logger.error("CssInsertRule: Non-style elemtn", msg); return }
styleSheet = OnloadStyleSheet.fromStyleElement(vElem.node as StyleElement)
if (!isStyleVElement(vElem)) { logger.error("CssInsertRule: Non-style element", msg); return }
styleSheet = OnloadStyleSheet.fromStyleElement(vElem.node)
this.olStyleSheetsDeprecated.set(msg.id, styleSheet)
}
styleSheet.insertRule(msg.rule, msg.index)
@ -351,8 +345,8 @@ export default class DOMManager extends ListWalker<Message> {
/* <style> tag case */
const vElem = this.vElements.get(msg.id)
if (!vElem) { logger.error("AdoptedSsAddOwner: Node not found", msg); return }
if (vElem.tagName.toLowerCase() !== "style") { logger.error("Non-style owner", msg); return }
this.olStyleSheets.set(msg.sheetID, OnloadStyleSheet.fromStyleElement(vElem.node as StyleElement))
if (!isStyleVElement(vElem)) { logger.error("Non-style owner", msg); return }
this.olStyleSheets.set(msg.sheetID, OnloadStyleSheet.fromStyleElement(vElem.node))
return
}
/* Constructed StyleSheet case */
@ -406,34 +400,6 @@ export default class DOMManager extends ListWalker<Message> {
}
}
applyBacktrack(msg: Message) {
// @ts-ignore
const target = this.vElements.get(msg.id)
if (!target) {
return
}
switch (msg.tp) {
case MType.SetNodeAttribute: {
this.setNodeAttribute(msg)
return
}
case MType.SetNodeAttributeDict: {
this.stringDict[msg.nameKey] === undefined && logger.error("No dictionary key for msg 'name': ", msg)
this.stringDict[msg.valueKey] === undefined && logger.error("No dictionary key for msg 'value': ", msg)
if (this.stringDict[msg.nameKey] === undefined || this.stringDict[msg.valueKey] === undefined) {
return
}
this.setNodeAttribute({
id: msg.id,
name: this.stringDict[msg.nameKey],
value: this.stringDict[msg.valueKey],
})
return;
}
}
}
/**
* Moves and applies all the messages from the current (or from the beginning, if t < current.time)
* to the one with msg.time >= `t`
@ -444,21 +410,8 @@ export default class DOMManager extends ListWalker<Message> {
* (the async part exists mostly due to styles loading)
*/
async moveReady(t: number): Promise<void> {
/**
* Basically just skipping all set attribute with attrs being "href" if user is 'jumping'
* to the other point of replay to save time on NOT downloading any resources before the dom tree changes
* are applied, so it won't try to download and then cancel when node is created in msg N and removed in msg N+2
* which produces weird bug when asset is cached (10-25ms delay)
* */
// http://0.0.0.0:3333/5/session/8452905874437457
// 70 iframe, 8 create element - STYLE tag
this.moveApply(t, this.applyMessage)
this.attrsBacktrack.forEach(msg => {
this.applyBacktrack(msg)
})
this.attrsBacktrack = []
this.olVRoots.forEach(rt => rt.applyChanges())
// Thinkabout (read): css preload
// What if we go back before it is ready? We'll have two handlres?

View file

@ -49,39 +49,53 @@ export abstract class VNode<T extends Node = Node> {
public abstract applyChanges(): void
}
type VChild = VElement | VText
abstract class VParent<T extends Node = Node> extends VNode<T>{
protected children: VChild[] = []
private recentlyInserted: Set<VChild> = new Set()
private childrenToMount: Set<VChild> = new Set()
insertChildAt(child: VChild, index: number) {
if (child.parentNode) {
child.parentNode.removeChild(child)
}
this.children.splice(index, 0, child)
this.recentlyInserted.add(child)
this.childrenToMount.add(child)
child.parentNode = this
}
removeChild(child: VChild) {
this.children = this.children.filter(ch => ch !== child)
this.recentlyInserted.delete(child)
this.childrenToMount.delete(child)
child.parentNode = null
}
applyChanges() {
const node = this.node
/* Inserting */
protected mountChildren(shouldInsert?: (child: VChild) => boolean) {
let nextMounted: VChild | null = null
for (let i = this.children.length-1; i >= 0; i--) {
const child = this.children[i]
child.applyChanges() /* Building the sub-tree in memory first */
if (this.recentlyInserted.has(child)) {
const nextVSibling = this.children[i+1]
node.insertBefore(child.node, nextVSibling ? nextVSibling.node : null)
if (this.childrenToMount.has(child) &&
(!shouldInsert || shouldInsert(child)) // is there a better way of not-knowing about subclass logic on prioritized insertion?
) {
this.node.insertBefore(child.node, nextMounted ? nextMounted.node : null)
this.childrenToMount.delete(child)
}
if (!this.childrenToMount.has(child)) {
nextMounted = child
}
}
this.recentlyInserted.clear()
}
applyChanges() {
/* Building a sub-trees first (in-memory for non-mounted children) */
this.children.forEach(child => child.applyChanges())
/* Inserting */
this.mountChildren()
if (this.childrenToMount.size !== 0) {
console.error("VParent: Something went wrong with children insertion")
}
/* Removing in-between */
const node = this.node
const realChildren = node.childNodes
for(let j = 0; j < this.children.length; j++) {
while (realChildren[j] !== this.children[j].node) {
@ -118,7 +132,7 @@ export class VShadowRoot extends VParent<ShadowRoot> {
export type VRoot = VDocument | VShadowRoot
export class VElement extends VParent<Element> {
parentNode: VParent | null = null
parentNode: VParent | null = null /** Should be modified only by he parent itself */
private newAttributes: Map<string, string | false> = new Map()
constructor(readonly tagName: string, readonly isSVG = false) { super() }
@ -134,7 +148,7 @@ export class VElement extends VParent<Element> {
this.newAttributes.set(name, false)
}
applyChanges() {
private applyAttributeChanges() { // "changes" -> "updates" ?
this.newAttributes.forEach((value, key) => {
if (value === false) {
this.node.removeAttribute(key)
@ -147,18 +161,41 @@ export class VElement extends VParent<Element> {
}
})
this.newAttributes.clear()
}
applyChanges() {
this.prioritized && this.applyPrioritizedChanges()
this.applyAttributeChanges()
super.applyChanges()
}
// TODO: priority insertion instead.
// rn this is for styles that should be inserted as prior,
// otherwise it will show visual styling lag if there is a transition CSS property)
enforceInsertion() {
let vNode: VElement = this
while (vNode.parentNode instanceof VElement) {
vNode = vNode.parentNode
/** Insertion Prioritization
* Made for styles that should be inserted as prior,
* otherwise it will show visual styling lag if there is a transition CSS property)
*/
prioritized = false
insertChildAt(child: VChild, index: number) {
super.insertChildAt(child, index)
/* Bubble prioritization */
if (child instanceof VElement && child.prioritized) {
let parent: VParent | null = this
while (parent instanceof VElement && !parent.prioritized) {
parent.prioritized = true
parent = parent.parentNode
}
}
(vNode.parentNode || vNode).applyChanges()
}
private applyPrioritizedChanges() {
this.children.forEach(child => {
if (child instanceof VText) {
child.applyChanges()
} else if (child.prioritized) {
/* Update prioritized VElement-s */
child.applyPrioritizedChanges()
child.applyAttributeChanges()
}
})
this.mountChildren(child => child instanceof VText || child.prioritized)
}
}