diff --git a/frontend/app/player/MessageDistributor/MessageDistributor.ts b/frontend/app/player/MessageDistributor/MessageDistributor.ts index a51430b41..b65e4aa40 100644 --- a/frontend/app/player/MessageDistributor/MessageDistributor.ts +++ b/frontend/app/player/MessageDistributor/MessageDistributor.ts @@ -27,10 +27,12 @@ import AssistManager from './managers/AssistManager'; import MessageReader from './MessageReader'; -import { INITIAL_STATE as PARENT_INITIAL_STATE } from './StatedScreen'; -import { INITIAL_STATE as ASSIST_INITIAL_STATE } from './managers/AssistManager'; +import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './StatedScreen'; +import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './managers/AssistManager'; import type { TimedMessage } from './Timed'; +import type { PerformanceChartPoint } from './managers/PerformanceTrackManager'; +import type { SkipInterval } from './managers/ActivityManager'; const LIST_NAMES = [ "redux", "mobx", "vuex", "ngrx", "graphql", "exceptions", "profiles", "longtasks" ] as const; const LISTS_INITIAL_STATE = {}; @@ -38,13 +40,26 @@ LIST_NAMES.forEach(name => { LISTS_INITIAL_STATE[`${name}ListNow`] = []; LISTS_INITIAL_STATE[`${name}List`] = []; }) -export const INITIAL_STATE = { - ...PARENT_INITIAL_STATE, + +export interface State extends SuperState, AssistState { + performanceChartData: PerformanceChartPoint[], + skipIntervals: SkipInterval[], + connType?: string, + connBandwidth?: number, + location?: string, + performanceChartTime?: number, + + domContentLoadedTime?: any, + domBuildingTime?: any, + loadTime?: any, +} +export const INITIAL_STATE: State = { + ...SUPER_INITIAL_STATE, ...LISTS_INITIAL_STATE, ...ASSIST_INITIAL_STATE, performanceChartData: [], skipIntervals: [], -} as const; +}; type ListsObject = { [key in typeof LIST_NAMES[number]]: ListWalker // @@ -206,7 +221,7 @@ export default class MessageDistributor extends StatedScreen { } return 0; }) - // + logger.info("Messages count: ", msgs.length, msgs); @@ -233,7 +248,7 @@ export default class MessageDistributor extends StatedScreen { } move(t: number, index?: number):void { - const stateToUpdate: typeof INITIAL_STATE = {}; + const stateToUpdate: Partial = {}; /* == REFACTOR_ME == */ const lastLoadedLocationMsg = this.loadedLocationManager.moveToLast(t, index); if (!!lastLoadedLocationMsg) { diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts index 95a7e3cb3..bf17777fa 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/BaseScreen.ts @@ -3,10 +3,13 @@ import { getState } from '../../../store'; import type { Point } from './types'; -export const INITIAL_STATE: { + +export interface State { width: number; height: number; -} = { +} + +export const INITIAL_STATE: State = { width: 0, height: 0, } diff --git a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts b/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts index 91b789ac1..f4848292e 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/Screen/Screen.ts @@ -5,7 +5,7 @@ import styles from './screen.css'; import { getState } from '../../../store'; import BaseScreen from './BaseScreen'; -export { INITIAL_STATE } from './BaseScreen'; +export { INITIAL_STATE, State } from './BaseScreen'; export default class Screen extends BaseScreen { private cursor: Cursor; diff --git a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts index 61fce0532..50147f90a 100644 --- a/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts +++ b/frontend/app/player/MessageDistributor/StatedScreen/StatedScreen.ts @@ -1,7 +1,15 @@ -import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE } from './Screen'; +import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen'; import { update, getState } from '../../store'; -export const INITIAL_STATE = { + +export interface State extends SuperState { + messagesLoading: boolean, + cssLoading: boolean, + disconnected: boolean, + userPageLoading: boolean, +} + +export const INITIAL_STATE: State = { ...SUPER_INITIAL_STATE, messagesLoading: false, cssLoading: false, @@ -10,6 +18,7 @@ export const INITIAL_STATE = { } export default class StatedScreen extends Screen { + constructor() { super(); } setMessagesLoading(messagesLoading: boolean) { // @ts-ignore diff --git a/frontend/app/player/MessageDistributor/managers/ActivityManager.js b/frontend/app/player/MessageDistributor/managers/ActivityManager.js deleted file mode 100644 index 9881ecac1..000000000 --- a/frontend/app/player/MessageDistributor/managers/ActivityManager.js +++ /dev/null @@ -1,43 +0,0 @@ -import ListWalker from './ListWalker'; - - -class SkipInterval { - constructor({ start = 0, end = 0 }) { - this.start = start; - this.end = end; - } - get time(): number { - return this.start; - } - contains(ts) { - return ts > this.start && ts < this.end; - } -} - - -export default class ActivityManager extends ListWalker { - #endTime: number = 0; - #minInterval: number = 0; - #lastActivity: number = 0; - constructor(duration: number) { - super(); - this.#endTime = duration; - this.#minInterval = duration * 0.1; - } - - updateAcctivity(time: number) { - if (time - this.#lastActivity >= this.#minInterval) { - this.add(new SkipInterval({ start: this.#lastActivity, end: time })); - } - this.#lastActivity = time; - } - - end() { - if (this.#endTime - this.#lastActivity >= this.#minInterval) { - this.add(new SkipInterval({ start: this.#lastActivity, end: this.#endTime })); - } - - } - - -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/ActivityManager.ts b/frontend/app/player/MessageDistributor/managers/ActivityManager.ts new file mode 100644 index 000000000..ba17d295e --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/ActivityManager.ts @@ -0,0 +1,42 @@ +import ListWalker from './ListWalker'; + + +class SkipIntervalCls { + constructor(private readonly start = 0, private readonly end = 0) {} + + get time(): number { + return this.start; + } + contains(ts) { + return ts > this.start && ts < this.end; + } +} + +export type SkipInterval = InstanceType; + + +export default class ActivityManager extends ListWalker { + private endTime: number = 0; + private minInterval: number = 0; + private lastActivity: number = 0; + constructor(duration: number) { + super(); + this.endTime = duration; + this.minInterval = duration * 0.1; + } + + updateAcctivity(time: number) { + if (time - this.lastActivity >= this.minInterval) { + this.add(new SkipIntervalCls(this.lastActivity, time)); + } + this.lastActivity = time; + } + + end() { + if (this.endTime - this.lastActivity >= this.minInterval) { + this.add(new SkipIntervalCls(this.lastActivity, this.endTime)); + } + + } + +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/AssistManager.ts b/frontend/app/player/MessageDistributor/managers/AssistManager.ts index 6aa6a77ad..c63f9af65 100644 --- a/frontend/app/player/MessageDistributor/managers/AssistManager.ts +++ b/frontend/app/player/MessageDistributor/managers/AssistManager.ts @@ -14,8 +14,11 @@ export enum CallingState { False, }; +export interface State { + calling: CallingState, +} -export const INITIAL_STATE = { +export const INITIAL_STATE: State = { calling: CallingState.False, } @@ -99,7 +102,7 @@ export default class AssistManager { // @ts-ignore host: new URL(window.ENV.API_EDP).host, path: '/assist', - port: 443 //location.protocol === 'https:' ? 443 : 80, + port: location.protocol === 'https:' ? 443 : 80, }); this.peer = peer; this.peer.on('error', e => { diff --git a/frontend/app/player/MessageDistributor/managers/DOMManager.js b/frontend/app/player/MessageDistributor/managers/DOMManager.js deleted file mode 100644 index d2faf4b68..000000000 --- a/frontend/app/player/MessageDistributor/managers/DOMManager.js +++ /dev/null @@ -1,283 +0,0 @@ -//@flow -import type StatedScreen from '../StatedScreen'; -import type { Message, SetNodeScroll, CreateElementNode } from '../messages'; -import type { TimedMessage } from '../Timed'; - -import logger from 'App/logger'; -import StylesManager from './StylesManager'; -import ListWalker from './ListWalker'; - -const IGNORED_ATTRS = [ "autocomplete", "name" ]; - -const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~ - -export default class DOMManager extends ListWalker { - #isMobile: boolean; - #screen: StatedScreen; - // #prop compiles to method that costs mor than strict property call. - _nl: Array = []; - _isLink: Array = []; // Optimisations - _bodyId: number = -1; - _postponedBodyMessage: ?CreateElementNode = null; - #nodeScrollManagers: Array> = []; - - #stylesManager: StylesManager; - - #startTime: number; - - constructor(screen: StatedScreen, isMobile: boolean, startTime: number) { - super(); - this.#startTime = startTime; - this.#isMobile = isMobile; - this.#screen = screen; - this.#stylesManager = new StylesManager(screen); - } - - get time(): number { - return this.#startTime; - } - - add(m: TimedMessage): void { - switch (m.tp) { - case "set_node_scroll": - if (!this.#nodeScrollManagers[ m.id ]) { - this.#nodeScrollManagers[ m.id ] = new ListWalker(); - } - this.#nodeScrollManagers[ m.id ].add(m); - return; - //case "css_insert_rule": // || //set_css_data ??? - //case "css_delete_rule": - // (m.tp === "set_node_attribute" && this._isLink[ m.id ] && m.key === "href")) { - // this.#stylesManager.add(m); - // return; - default: - if (m.tp === "create_element_node") { - switch(m.tag) { - case "LINK": - this._isLink[ m.id ] = true; - break; - case "BODY": - this._bodyId = m.id; // Can be several body nodes at one document session? - break; - } - } else if (m.tp === "set_node_attribute" && - (IGNORED_ATTRS.includes(m.key) || !ATTR_NAME_REGEXP.test(m.key))) { - logger.log("Ignorring message: ", m) - return; // Ignoring... - } - super.add(m); - } - - } - - _removeBodyScroll(id: number): void { - if (this.#isMobile && this._bodyId === id) { - this._nl[ id ].style.overflow = "hidden"; - } - } - - // May be make it as a message on message add? - _removeAutocomplete({ id, tag }: { id: number, tag: string }): boolean { - const node = this._nl[ id ]; - if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) { - node.setAttribute("autocomplete", "off"); - return true; - } - if (tag === "INPUT") { - node.setAttribute("autocomplete", "new-password"); - return true; - } - return false; - } - - // type = NodeMessage ? - _insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void { - if (!this._nl[ id ]) { - logger.error("Insert error. Node not found", id); - return; - } - if (!this._nl[ parentID ]) { - logger.error("Insert error. Parent node not found", parentID); - return; - } - // WHAT if text info contains some rules and the ordering is just wrong??? - if ((this._nl[ parentID ] instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker - this._nl[ parentID ].sheet && - this._nl[ parentID ].sheet.cssRules && - this._nl[ parentID ].sheet.cssRules.length > 0) { - logger.log("Trying to insert child to style tag with virtual rules: ", this._nl[ parentID ], this._nl[ id ]); - return; - } - - const childNodes = this._nl[ parentID ].childNodes; - if (!childNodes) { - logger.error("Node has no childNodes", this._nl[ parentID ]); - return; - } - this._nl[ parentID ] - .insertBefore(this._nl[ id ], childNodes[ index ]); - } - - #applyMessage: (Message => void) = msg => { - let node; - switch (msg.tp) { - case "create_document": - this.#screen.document.open(); - this.#screen.document.write(`${ msg.doctype || "" }`); - this.#screen.document.close(); - const fRoot = this.#screen.document.documentElement; - fRoot.innerText = ''; - //this._nl[ 0 ] = fRoot; // vs - this._nl = [ fRoot ]; - - // the last load event I can control - //if (this.document.fonts) { - // this.document.fonts.onloadingerror = () => this.marker.redraw(); - // this.document.fonts.onloadingdone = () => this.marker.redraw(); - //} - - //this.#screen.setDisconnected(false); - this.#stylesManager.reset(); - break; - case "create_text_node": - this._nl[ msg.id ] = document.createTextNode(''); - this._insertNode(msg); - break; - case "create_element_node": - if (msg.svg) { - this._nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag); - } else { - this._nl[ msg.id ] = document.createElement(msg.tag); - } - if (this._bodyId === msg.id) { - this._postponedBodyMessage = msg; - } else { - this._insertNode(msg); - } - this._removeBodyScroll(msg.id); - this._removeAutocomplete(msg); - break; - case "move_node": - this._insertNode(msg); - break; - case "remove_node": - if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; } - if (!this._nl[ msg.id ].parentElement) { logger.error("Parent node not found", msg); break; } - this._nl[ msg.id ].parentElement.removeChild(this._nl[ msg.id ]); - break; - case "set_node_attribute": - let { id, name, value } = msg; - node = this._nl[ id ]; - if (!node) { logger.error("Node not found", msg); break; } - if (this._isLink[ id ] && name === "href") { - if (value.startsWith(window.ENV.ASSETS_HOST)) { // Hack for queries in rewrited urls - value = value.replace("?", "%3F"); - } - this.#stylesManager.setStyleHandlers(node, value); - } - try { - node.setAttribute(name, value); - } catch(e) { - logger.error(e, msg); - } - this._removeBodyScroll(msg.id); - break; - case "remove_node_attribute": - if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; } - try { - this._nl[ msg.id ].removeAttribute(msg.name); - } catch(e) { - logger.error(e, msg); - } - break; - case "set_input_value": - if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; } - const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value; - this._nl[ msg.id ].value = val; - break; - case "set_input_checked": - if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; } - this._nl[ msg.id ].checked = msg.checked; - break; - case "set_node_data": - case "set_css_data": - if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; } - this._nl[ msg.id ].data = msg.data; - break; - case "css_insert_rule": - if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; } - if (!(this._nl[ msg.id ] instanceof HTMLStyleElement) // link or null - || this._nl[ msg.id ].sheet == null) { - logger.warn("Non-style node in CSS rules message (or sheet is null)", msg); - - // prev version fallback (TODO: delete on 30.10.20) - let styleSheet = this.#screen.document.styleSheets[ msg.id ]; - if (!styleSheet) { - styleSheet = this.#screen.document.styleSheets[0]; - } - if (!styleSheet) { - logger.log("Old-fasion insert rule: No stylesheet found;", msg); - break; - } - try { - styleSheet.insertRule(msg.rule, msg.index); - } catch(e) { - logger.log("Old-fasion insert rule:", e, msg); - styleSheet.insertRule(msg.rule); - } - // - - break; - } - try { - this._nl[ msg.id ].sheet.insertRule(msg.rule, msg.index) - } catch (e) { - logger.warn(e, msg) - this._nl[ msg.id ].sheet.insertRule(msg.rule) - } - break; - case "css_delete_rule": - if (!this._nl[ msg.id ]) { logger.error("Node not found", msg); break; } - if (!this._nl[ msg.id ] instanceof HTMLStyleElement) { // link or null - logger.warn("Non-style node in CSS rules message", msg); - break; - } - try { - this._nl[ msg.id ].sheet.deleteRule(msg.rule, msg.index) - } catch (e) { - logger.warn(e, msg) - } - break; - //not sure what to do with this one - //case "disconnected": - //setTimeout(() => { - // if last one - //if (this.msgs[ this.msgs.length - 1 ] === msg) { - // this.setDisconnected(true); - // } - //}, 10000); - //break; - } - } - - moveReady(t: number): Promise { - this.moveApply(t, this.#applyMessage); // This function autoresets pointer if necessary (better name?) - this.#nodeScrollManagers.forEach(manager => { - const msg = manager.moveToLast(t); // TODO: reset (?) - if (!!msg && !!this._nl[msg.id]) { - this._nl[msg.id].scrollLeft = msg.x; - this._nl[msg.id].scrollTop = msg.y; - } - }); - - /* Mount body as late as possible */ - if (this._postponedBodyMessage != null) { - this._insertNode(this._postponedBodyMessage) - this._postponedBodyMessage = null; - } - - // Thinkabout (read): css preload - // What if we go back before it is ready? We'll have two handlres? - return this.#stylesManager.moveReady(t); - } -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/DOMManager.ts b/frontend/app/player/MessageDistributor/managers/DOMManager.ts new file mode 100644 index 000000000..1d723a923 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/DOMManager.ts @@ -0,0 +1,277 @@ +import type StatedScreen from '../StatedScreen'; +import type { Message, SetNodeScroll, CreateElementNode } from '../messages'; +import type { TimedMessage } from '../Timed'; + +import logger from 'App/logger'; +import StylesManager from './StylesManager'; +import ListWalker from './ListWalker'; +import type { Timed }from '../Timed'; + +const IGNORED_ATTRS = [ "autocomplete", "name" ]; + +const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~ + +export default class DOMManager extends ListWalker { + private isMobile: boolean; + private screen: StatedScreen; + private nl: Array = []; + private isLink: Array = []; // Optimisations + private bodyId: number = -1; + private postponedBodyMessage: CreateElementNode | null = null; + private nodeScrollManagers: Array> = []; + + private stylesManager: StylesManager; + + private startTime: number; + + constructor(screen: StatedScreen, isMobile: boolean, startTime: number) { + super(); + this.startTime = startTime; + this.isMobile = isMobile; + this.screen = screen; + this.stylesManager = new StylesManager(screen); + } + + get time(): number { + return this.startTime; + } + + add(m: TimedMessage): void { + switch (m.tp) { + case "set_node_scroll": + if (!this.nodeScrollManagers[ m.id ]) { + this.nodeScrollManagers[ m.id ] = new ListWalker(); + } + this.nodeScrollManagers[ m.id ].add(m); + return; + //case "css_insert_rule": // || //set_css_data ??? + //case "css_delete_rule": + // (m.tp === "set_node_attribute" && this.isLink[ m.id ] && m.key === "href")) { + // this.stylesManager.add(m); + // return; + default: + if (m.tp === "create_element_node") { + switch(m.tag) { + case "LINK": + this.isLink[ m.id ] = true; + break; + case "BODY": + this.bodyId = m.id; // Can be several body nodes at one document session? + break; + } + } else if (m.tp === "set_node_attribute" && + (IGNORED_ATTRS.includes(m.name) || !ATTR_NAME_REGEXP.test(m.name))) { + logger.log("Ignorring message: ", m) + return; // Ignoring... + } + super.add(m); + } + + } + + private removeBodyScroll(id: number): void { + if (this.isMobile && this.bodyId === id) { + (this.nl[ id ] as HTMLBodyElement).style.overflow = "hidden"; + } + } + + // May be make it as a message on message add? + private removeAutocomplete({ id, tag }: CreateElementNode): boolean { + const node = this.nl[ id ] as HTMLElement; + if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) { + node.setAttribute("autocomplete", "off"); + return true; + } + if (tag === "INPUT") { + node.setAttribute("autocomplete", "new-password"); + return true; + } + return false; + } + + // type = NodeMessage ? + private insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void { + if (!this.nl[ id ]) { + logger.error("Insert error. Node not found", id); + return; + } + if (!this.nl[ parentID ]) { + logger.error("Insert error. Parent node not found", parentID); + return; + } + // WHAT if text info contains some rules and the ordering is just wrong??? + const el = this.nl[ parentID ] + if ((el instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker + el.sheet && + el.sheet.cssRules && + el.sheet.cssRules.length > 0) { + logger.log("Trying to insert child to style tag with virtual rules: ", this.nl[ parentID ], this.nl[ id ]); + return; + } + + const childNodes = this.nl[ parentID ].childNodes; + if (!childNodes) { + logger.error("Node has no childNodes", this.nl[ parentID ]); + return; + } + this.nl[ parentID ] + .insertBefore(this.nl[ id ], childNodes[ index ]); + } + + private applyMessage = (msg: Message): void => { + let node; + switch (msg.tp) { + case "create_document": + // @ts-ignore ?? + this.screen.document.open(); + // @ts-ignore ?? + this.screen.document.write(`${ msg.doctype || "" }`); + // @ts-ignore ?? + this.screen.document.close(); + // @ts-ignore ?? + const fRoot = this.screen.document.documentElement; + fRoot.innerText = ''; + this.nl = [ fRoot ]; + + // the last load event I can control + //if (this.document.fonts) { + // this.document.fonts.onloadingerror = () => this.marker.redraw(); + // this.document.fonts.onloadingdone = () => this.marker.redraw(); + //} + + //this.screen.setDisconnected(false); + this.stylesManager.reset(); + break; + case "create_text_node": + this.nl[ msg.id ] = document.createTextNode(''); + this.insertNode(msg); + break; + case "create_element_node": + if (msg.svg) { + this.nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag); + } else { + this.nl[ msg.id ] = document.createElement(msg.tag); + } + if (this.bodyId === msg.id) { + this.postponedBodyMessage = msg; + } else { + this.insertNode(msg); + } + this.removeBodyScroll(msg.id); + this.removeAutocomplete(msg); + break; + case "move_node": + this.insertNode(msg); + break; + case "remove_node": + node = this.nl[ msg.id ] + if (!node) { logger.error("Node not found", msg); break; } + if (!node.parentElement) { logger.error("Parent node not found", msg); break; } + node.parentElement.removeChild(node); + break; + case "set_node_attribute": + let { id, name, value } = msg; + node = this.nl[ id ]; + if (!node) { logger.error("Node not found", msg); break; } + if (this.isLink[ id ] && name === "href") { + // @ts-ignore TODO: global ENV type + if (value.startsWith(window.ENV.ASSETS_HOST)) { // Hack for queries in rewrited urls + value = value.replace("?", "%3F"); + } + this.stylesManager.setStyleHandlers(node, value); + } + try { + node.setAttribute(name, value); + } catch(e) { + logger.error(e, msg); + } + this.removeBodyScroll(msg.id); + break; + case "remove_node_attribute": + if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; } + try { + (this.nl[ msg.id ] as HTMLElement).removeAttribute(msg.name); + } catch(e) { + logger.error(e, msg); + } + break; + case "set_input_value": + if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; } + const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value; + (this.nl[ msg.id ] as HTMLInputElement).value = val; + break; + case "set_input_checked": + node = this.nl[ msg.id ]; + if (!node) { logger.error("Node not found", msg); break; } + (node as HTMLInputElement).checked = msg.checked; + break; + case "set_node_data": + case "set_css_data": + if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; } + // @ts-ignore + this.nl[ msg.id ].data = msg.data; + break; + case "css_insert_rule": + node = this.nl[ msg.id ]; + if (!node) { logger.error("Node not found", msg); break; } + if (!(node instanceof HTMLStyleElement) // link or null + || node.sheet == null) { + logger.warn("Non-style node in CSS rules message (or sheet is null)", msg); + break; + } + try { + node.sheet.insertRule(msg.rule, msg.index) + } catch (e) { + logger.warn(e, msg) + node.sheet.insertRule(msg.rule) + } + break; + case "css_delete_rule": + node = this.nl[ msg.id ]; + if (!node) { logger.error("Node not found", msg); break; } + if (!(node instanceof HTMLStyleElement) // link or null + || node.sheet == null) { + logger.warn("Non-style node in CSS rules message (or sheet is null)", msg); + break; + } + try { + node.sheet.deleteRule(msg.index) + } catch (e) { + logger.warn(e, msg) + } + break; + //not sure what to do with this one + //case "disconnected": + //setTimeout(() => { + // if last one + //if (this.msgs[ this.msgs.length - 1 ] === msg) { + // this.setDisconnected(true); + // } + //}, 10000); + //break; + } + } + + moveReady(t: number): Promise { + this.moveApply(t, this.applyMessage); // This function autoresets pointer if necessary (better name?) + this.nodeScrollManagers.forEach(manager => { + const msg = manager.moveToLast(t); // TODO: reset (?) + + if (!!msg && !!this.nl[msg.id]) { + const node = this.nl[msg.id] as HTMLElement; + node.scrollLeft = msg.x; + node.scrollTop = msg.y; + } + }); + + /* Mount body as late as possible */ + if (this.postponedBodyMessage != null) { + this.insertNode(this.postponedBodyMessage) + this.postponedBodyMessage = null; + } + + // Thinkabout (read): css preload + // What if we go back before it is ready? We'll have two handlres? + return this.stylesManager.moveReady(t); + } +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/MouseManager.js b/frontend/app/player/MessageDistributor/managers/MouseManager.ts similarity index 53% rename from frontend/app/player/MessageDistributor/managers/MouseManager.js rename to frontend/app/player/MessageDistributor/managers/MouseManager.ts index 7352043c1..d7980dd1d 100644 --- a/frontend/app/player/MessageDistributor/managers/MouseManager.js +++ b/frontend/app/player/MessageDistributor/managers/MouseManager.ts @@ -1,4 +1,3 @@ -//@flow import type StatedScreen from '../StatedScreen'; import type { MouseMove } from '../messages'; import type { Timed } from '../Timed'; @@ -10,32 +9,34 @@ type MouseMoveTimed = MouseMove & Timed; const HOVER_CLASS = "-openreplay-hover"; export default class MouseManager extends ListWalker { - #screen: StatedScreen; - #hoverElements: Array = []; + private screen: StatedScreen; + private hoverElements: Array = []; constructor(screen: StatedScreen): void { super(); - this.#screen = screen; + this.screen = screen; } - _updateHover(): void { - const curHoverElements = this.#screen.getCursorTargets(); - const diffAdd = curHoverElements.filter(elem => !this.#hoverElements.includes(elem)); - const diffRemove = this.#hoverElements.filter(elem => !curHoverElements.includes(elem)); - this.#hoverElements = curHoverElements; + private updateHover(): void { + // @ts-ignore TODO + const curHoverElements = this.screen.getCursorTargets(); + const diffAdd = curHoverElements.filter(elem => !this.hoverElements.includes(elem)); + const diffRemove = this.hoverElements.filter(elem => !curHoverElements.includes(elem)); + this.hoverElements = curHoverElements; diffAdd.forEach(elem => elem.classList.add(HOVER_CLASS)); diffRemove.forEach(elem => elem.classList.remove(HOVER_CLASS)); } reset(): void { - this.#hoverElements = []; + this.hoverElements = []; } move(t: number) { const lastMouseMove = this.moveToLast(t); if (!!lastMouseMove){ - this.#screen.cursor.move(lastMouseMove); - this._updateHover(); + // @ts-ignore TODO + this.screen.cursor.move(lastMouseMove); + this.updateHover(); } } diff --git a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.js b/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.js deleted file mode 100644 index 005b7004a..000000000 --- a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.js +++ /dev/null @@ -1,104 +0,0 @@ -// @flow - -import type { PerformanceTrack, SetPageVisibility } from '../messages'; -import type { Timed } from '../Timed'; - -import ListWalker from './ListWalker'; - -type TimedPerformanceTrack = Timed & PerformanceTrack; -type TimedSetPageVisibility = Timed & SetPageVisibility; - -type PerformanceChartPoint = { - time: number, - usedHeap: number, - totalHeap: number, - fps: ?number, - cpu: ?number, - nodesCount: number, -} - -export default class PerformanceTrackManager extends ListWalker { - #chart: Array = []; - #isHidden: boolean = false; - #timeCorrection: number = 0; - #heapAvaliable: boolean = false; - #fpsAvaliable: boolean = false; - #cpuAvaliable: boolean = false; - #prevTime: ?number = null; - #prevNodesCount: number = 0; - - - add(msg: TimedPerformanceTrack):void { - let fps = null; - let cpu = null; - if (!this.#isHidden && this.#prevTime != null) { - let timePassed = msg.time - this.#prevTime + this.#timeCorrection; - - if (timePassed > 0 && msg.frames >= 0) { - if (msg.frames > 0) { this.#fpsAvaliable = true; } - fps = msg.frames*1e3/timePassed; // Multiply by 1e3 as time in ms; - fps = Math.min(fps,60); // What if 120? TODO: alert if more than 60 - if (this.#chart.length === 1) { - this.#chart[0].fps = fps; - } - } - - if (timePassed > 0 && msg.ticks >= 0) { - this.#cpuAvaliable = true; - let tickRate = msg.ticks * 30 / timePassed; - if (tickRate > 1) { - tickRate = 1; - } - cpu = Math.round(100 - tickRate*100); - if (this.#chart.length === 1) { - this.#chart[0].cpu = cpu; - } - } - } - - this.#prevTime = msg.time; - this.#timeCorrection = 0 - - this.#heapAvaliable = this.#heapAvaliable || msg.usedJSHeapSize > 0; - this.#chart.push({ - usedHeap: msg.usedJSHeapSize, - totalHeap: msg.totalJSHeapSize, - fps, - cpu, - time: msg.time, - nodesCount: this.#prevNodesCount, - }); - super.add(msg); - } - - setCurrentNodesCount(count: number) { - this.#prevNodesCount = count; - if (this.#chart.length > 0) { - this.#chart[ this.#chart.length - 1 ].nodesCount = count; - } - } - - handleVisibility(msg: TimedSetPageVisibility):void { - if (!this.#isHidden && msg.hidden && this.#prevTime != null) { - this.#timeCorrection = msg.time - this.#prevTime; - } - if (this.#isHidden && !msg.hidden) { - this.#prevTime = msg.time; - } - this.#isHidden = msg.hidden; - } - - get chartData(): Array { - return this.#chart; - } - - get avaliability(): { cpu: boolean, fps: boolean, heap: boolean } { - return { - cpu: this.#cpuAvaliable, - fps: this.#fpsAvaliable, - heap: this.#heapAvaliable, - nodes: true, - } - } - -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts b/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts new file mode 100644 index 000000000..1b11813b2 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/PerformanceTrackManager.ts @@ -0,0 +1,102 @@ +import type { PerformanceTrack, SetPageVisibility } from '../messages'; +import type { Timed } from '../Timed'; + +import ListWalker from './ListWalker'; + +type TimedPerformanceTrack = Timed & PerformanceTrack; +type TimedSetPageVisibility = Timed & SetPageVisibility; + +export type PerformanceChartPoint = { + time: number, + usedHeap: number, + totalHeap: number, + fps?: number, + cpu?: number, + nodesCount: number, +} + +export default class PerformanceTrackManager extends ListWalker { + private chart: Array = []; + private isHidden: boolean = false; + private timeCorrection: number = 0; + private heapAvaliable: boolean = false; + private fpsAvaliable: boolean = false; + private cpuAvaliable: boolean = false; + private prevTime: number | null = null; + private prevNodesCount: number = 0; + + + add(msg: TimedPerformanceTrack):void { + let fps: number | undefined; + let cpu: number | undefined; + if (!this.isHidden && this.prevTime != null) { + let timePassed = msg.time - this.prevTime + this.timeCorrection; + + if (timePassed > 0 && msg.frames >= 0) { + if (msg.frames > 0) { this.fpsAvaliable = true; } + fps = msg.frames*1e3/timePassed; // Multiply by 1e3 as time in ms; + fps = Math.min(fps,60); // What if 120? TODO: alert if more than 60 + if (this.chart.length === 1) { + this.chart[0].fps = fps; + } + } + + if (timePassed > 0 && msg.ticks >= 0) { + this.cpuAvaliable = true; + let tickRate = msg.ticks * 30 / timePassed; + if (tickRate > 1) { + tickRate = 1; + } + cpu = Math.round(100 - tickRate*100); + if (this.chart.length === 1) { + this.chart[0].cpu = cpu; + } + } + } + + this.prevTime = msg.time; + this.timeCorrection = 0 + + this.heapAvaliable = this.heapAvaliable || msg.usedJSHeapSize > 0; + this.chart.push({ + usedHeap: msg.usedJSHeapSize, + totalHeap: msg.totalJSHeapSize, + fps, + cpu, + time: msg.time, + nodesCount: this.prevNodesCount, + }); + super.add(msg); + } + + setCurrentNodesCount(count: number) { + this.prevNodesCount = count; + if (this.chart.length > 0) { + this.chart[ this.chart.length - 1 ].nodesCount = count; + } + } + + handleVisibility(msg: TimedSetPageVisibility):void { + if (!this.isHidden && msg.hidden && this.prevTime != null) { + this.timeCorrection = msg.time - this.prevTime; + } + if (this.isHidden && !msg.hidden) { + this.prevTime = msg.time; + } + this.isHidden = msg.hidden; + } + + get chartData(): Array { + return this.chart; + } + + get avaliability(): { cpu: boolean, fps: boolean, heap: boolean, nodes: boolean } { + return { + cpu: this.cpuAvaliable, + fps: this.fpsAvaliable, + heap: this.heapAvaliable, + nodes: true, + } + } + +} \ No newline at end of file