trfactoring(frontend/player):phase 1 of componnts decomposition; use store per instance

This commit is contained in:
Alex Kaminskii 2022-11-15 21:01:40 +01:00
parent d3d28d705e
commit 7a3ef9bc21
72 changed files with 990 additions and 1690 deletions

View file

@ -30,7 +30,7 @@ let debounceTooltipChange = () => null;
disabled: state.cssLoading || state.messagesLoading || state.markedTargets,
endTime: state.endTime,
live: state.live,
notes: state.notes,
notes: state.notes || [], // TODO: implement notes without interaction with Player state
}))
@connect(
(state) => ({

View file

@ -1,23 +0,0 @@
import type { Message } from './messages'
import ListWalker from './managers/ListWalker';
export const LIST_NAMES = ["redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles"] as const;
export const INITIAL_STATE = {}
LIST_NAMES.forEach(name => {
INITIAL_STATE[`${name}ListNow`] = []
INITIAL_STATE[`${name}List`] = []
})
type ListsObject = {
[key in typeof LIST_NAMES[number]]: ListWalker<any>
}
export function initLists(): ListsObject {
const lists: Partial<ListsObject> = {};
for (var i = 0; i < LIST_NAMES.length; i++) {
lists[LIST_NAMES[i]] = new ListWalker();
}
return lists as ListsObject;
}

View file

@ -1,152 +0,0 @@
import Screen, { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen';
import { update, getState } from '../../store';
import type { Point } from './Screen/types';
function getOffset(el: Element, innerWindow: Window) {
const rect = el.getBoundingClientRect();
return {
fixedLeft: rect.left + innerWindow.scrollX,
fixedTop: rect.top + innerWindow.scrollY,
rect,
};
}
//export interface targetPosition
interface BoundingRect {
top: number,
left: number,
width: number,
height: number,
}
export interface MarkedTarget {
boundingRect: BoundingRect,
el: Element,
selector: string,
count: number,
index: number,
active?: boolean,
percent: number
}
export interface State extends SuperState {
messagesLoading: boolean,
cssLoading: boolean,
markedTargets: MarkedTarget[] | null,
activeTargetIndex: number,
}
export const INITIAL_STATE: State = {
...SUPER_INITIAL_STATE,
messagesLoading: false,
cssLoading: false,
markedTargets: null,
activeTargetIndex: 0
};
export default class StatedScreen extends Screen {
constructor() { super(); }
setMessagesLoading(messagesLoading: boolean) {
this.display(!messagesLoading);
update({ messagesLoading });
}
setCSSLoading(cssLoading: boolean) {
this.displayFrame(!cssLoading);
update({ cssLoading });
}
setSize({ height, width }: { height: number, width: number }) {
update({ width, height });
this.scale();
this.updateMarketTargets()
}
updateMarketTargets() {
const { markedTargets } = getState();
if (markedTargets) {
update({
markedTargets: markedTargets.map((mt: any) => ({
...mt,
boundingRect: this.calculateRelativeBoundingRect(mt.el),
})),
});
}
}
private calculateRelativeBoundingRect(el: Element): BoundingRect {
if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO
const { top, left, width, height } = el.getBoundingClientRect();
const s = this.getScale();
const scrinRect = this.screen.getBoundingClientRect();
const parentRect = this.parentElement.getBoundingClientRect();
return {
top: top*s + scrinRect.top - parentRect.top,
left: left*s + scrinRect.left - parentRect.left,
width: width*s,
height: height*s,
}
}
setActiveTarget(index: number) {
const window = this.window
const markedTargets: MarkedTarget[] | null = getState().markedTargets
const target = markedTargets && markedTargets[index]
if (target && window) {
const { fixedTop, rect } = getOffset(target.el, window)
const scrollToY = fixedTop - window.innerHeight / 1.5
if (rect.top < 0 || rect.top > window.innerHeight) {
// behavior hack TODO: fix it somehow when they will decide to remove it from browser api
// @ts-ignore
window.scrollTo({ top: scrollToY, behavior: 'instant' })
setTimeout(() => {
if (!markedTargets) { return }
update({
markedTargets: markedTargets.map(t => t === target ? {
...target,
boundingRect: this.calculateRelativeBoundingRect(target.el),
} : t)
})
}, 0)
}
}
update({ activeTargetIndex: index });
}
private actualScroll: Point | null = null
setMarkedTargets(selections: { selector: string, count: number }[] | null) {
if (selections) {
const totalCount = selections.reduce((a, b) => {
return a + b.count
}, 0);
const markedTargets: MarkedTarget[] = [];
let index = 0;
selections.forEach((s) => {
const el = this.getElementBySelector(s.selector);
if (!el) return;
markedTargets.push({
...s,
el,
index: index++,
percent: Math.round((s.count * 100) / totalCount),
boundingRect: this.calculateRelativeBoundingRect(el),
count: s.count,
})
});
this.actualScroll = this.getCurrentScroll()
update({ markedTargets });
} else {
if (this.actualScroll) {
this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y)
this.actualScroll = null
}
update({ markedTargets: null });
}
}
}

View file

@ -1,2 +0,0 @@
export { default } from './StatedScreen';
export * from './StatedScreen';

View file

@ -1,2 +0,0 @@
export { default } from './MessageDistributor';
export * from './MessageDistributor';

View file

@ -1,281 +0,0 @@
import { goTo as listsGoTo } from './lists';
import { update, getState } from './store';
import MessageDistributor, { INITIAL_STATE as SUPER_INITIAL_STATE } from './MessageDistributor/MessageDistributor';
import { Note } from 'App/services/NotesService';
const fps = 60;
const performance = window.performance || { now: Date.now.bind(Date) };
const requestAnimationFrame =
window.requestAnimationFrame ||
// @ts-ignore
window.webkitRequestAnimationFrame ||
// @ts-ignore
window.mozRequestAnimationFrame ||
// @ts-ignore
window.oRequestAnimationFrame ||
// @ts-ignore
window.msRequestAnimationFrame ||
((callback: (args: any) => void) => window.setTimeout(() => { callback(performance.now()); }, 1000 / fps));
const cancelAnimationFrame =
window.cancelAnimationFrame ||
// @ts-ignore
window.mozCancelAnimationFrame ||
window.clearTimeout;
const HIGHEST_SPEED = 16;
const SPEED_STORAGE_KEY = "__$player-speed$__";
const SKIP_STORAGE_KEY = "__$player-skip$__";
const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__";
const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__";
const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__";
const storedSpeed: number = parseInt(localStorage.getItem(SPEED_STORAGE_KEY) || "") ;
const initialSpeed = [1,2,4,8,16].includes(storedSpeed) ? storedSpeed : 1;
const initialSkip = localStorage.getItem(SKIP_STORAGE_KEY) === 'true';
const initialSkipToIssue = localStorage.getItem(SKIP_TO_ISSUE_STORAGE_KEY) === 'true';
const initialAutoplay = localStorage.getItem(AUTOPLAY_STORAGE_KEY) === 'true';
const initialShowEvents = localStorage.getItem(SHOW_EVENTS_STORAGE_KEY) === 'true';
export const INITIAL_STATE = {
...SUPER_INITIAL_STATE,
time: 0,
playing: false,
completed: false,
endTime: 0,
inspectorMode: false,
live: false,
livePlay: false,
liveTimeTravel: false,
notes: [],
} as const;
export const INITIAL_NON_RESETABLE_STATE = {
skip: initialSkip,
skipToIssue: initialSkipToIssue,
autoplay: initialAutoplay,
speed: initialSpeed,
showEvents: initialShowEvents,
}
export default class Player extends MessageDistributor {
private _animationFrameRequestId: number = 0;
private _setTime(time: number, index?: number) {
update({
time,
completed: false,
});
super.move(time, index);
listsGoTo(time, index);
}
private _startAnimation() {
let prevTime = getState().time;
let animationPrevTime = performance.now();
const nextFrame = (animationCurrentTime: number) => {
const {
speed,
skip,
autoplay,
skipIntervals,
endTime,
live,
livePlay,
disconnected,
messagesLoading,
cssLoading,
} = getState();
const diffTime = messagesLoading || cssLoading || disconnected
? 0
: Math.max(animationCurrentTime - animationPrevTime, 0) * (live ? 1 : speed);
let time = prevTime + diffTime;
const skipInterval = !live && skip && skipIntervals.find((si: Node) => si.contains(time)); // TODO: good skip by messages
if (skipInterval) time = skipInterval.end;
const fmt = super.getFirstMessageTime();
if (time < fmt) time = fmt; // ?
const lmt = super.getLastMessageTime();
if (livePlay && time < lmt) time = lmt;
if (endTime < lmt) {
update({
endTime: lmt,
});
}
prevTime = time;
animationPrevTime = animationCurrentTime;
const completed = !live && time >= endTime;
if (completed) {
this._setTime(endTime);
return update({
playing: false,
completed: true,
});
}
// throttle store updates
// TODO: make it possible to change frame rate
if (live && time - endTime > 100) {
update({
endTime: time,
livePlay: endTime - time < 900
});
}
this._setTime(time);
this._animationFrameRequestId = requestAnimationFrame(nextFrame);
};
this._animationFrameRequestId = requestAnimationFrame(nextFrame);
}
play() {
cancelAnimationFrame(this._animationFrameRequestId);
update({ playing: true });
this._startAnimation();
}
pause() {
cancelAnimationFrame(this._animationFrameRequestId);
update({ playing: false })
}
togglePlay() {
const { playing, completed } = getState();
if (playing) {
this.pause();
} else if (completed) {
this._setTime(0);
this.play();
} else {
this.play();
}
}
jump(setTime: number, index: number) {
const { live, liveTimeTravel, endTime } = getState();
if (live && !liveTimeTravel) return;
const time = setTime ? setTime : getState().time
if (getState().playing) {
cancelAnimationFrame(this._animationFrameRequestId);
// this._animationFrameRequestId = requestAnimationFrame(() => {
this._setTime(time, index);
this._startAnimation();
// throttilg the redux state update from each frame to nearly half a second
// which is better for performance and component rerenders
update({ livePlay: Math.abs(time - endTime) < 500 });
//});
} else {
//this._animationFrameRequestId = requestAnimationFrame(() => {
this._setTime(time, index);
update({ livePlay: Math.abs(time - endTime) < 500 });
//});
}
}
toggleSkip() {
const skip = !getState().skip;
localStorage.setItem(SKIP_STORAGE_KEY, `${skip}`);
update({ skip });
}
toggleInspectorMode(flag: boolean, clickCallback?: (args: any) => void) {
if (typeof flag !== 'boolean') {
const { inspectorMode } = getState();
flag = !inspectorMode;
}
if (flag) {
this.pause();
update({ inspectorMode: true });
return super.enableInspector(clickCallback);
} else {
super.disableInspector();
update({ inspectorMode: false });
}
}
markTargets(targets: { selector: string, count: number }[] | null) {
this.pause();
this.setMarkedTargets(targets);
}
activeTarget(index: number) {
this.setActiveTarget(index);
}
toggleSkipToIssue() {
const skipToIssue = !getState().skipToIssue;
localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`);
update({ skipToIssue });
}
toggleAutoplay() {
const autoplay = !getState().autoplay;
localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`);
update({ autoplay });
}
toggleEvents(shouldShow?: boolean) {
const showEvents = shouldShow || !getState().showEvents;
localStorage.setItem(SHOW_EVENTS_STORAGE_KEY, `${showEvents}`);
update({ showEvents });
}
_updateSpeed(speed: number) {
localStorage.setItem(SPEED_STORAGE_KEY, `${speed}`);
update({ speed });
}
toggleSpeed() {
const { speed } = getState();
this._updateSpeed(speed < HIGHEST_SPEED ? speed * 2 : 1);
}
speedUp() {
const { speed } = getState();
this._updateSpeed(Math.min(HIGHEST_SPEED, speed * 2));
}
speedDown() {
const { speed } = getState();
this._updateSpeed(Math.max(1, speed/2));
}
async toggleTimetravel() {
if (!getState().liveTimeTravel) {
return await this.reloadWithUnprocessedFile()
}
}
jumpToLive() {
cancelAnimationFrame(this._animationFrameRequestId);
this._setTime(getState().endTime);
this._startAnimation();
update({ livePlay: true });
}
toggleUserName(name?: string) {
this.cursor.toggleUserName(name)
}
injectNotes(notes: Note[]) {
update({ notes })
}
filterOutNote(noteId: number) {
const { notes } = getState()
update({ notes: notes.filter((note: Note) => note.noteId !== noteId) })
}
clean() {
this.pause();
super.clean();
}
}

View file

@ -1,4 +1,4 @@
import type { Timed } from '../messages/timed';
import type { Timed } from './messages/timed';
export default class ListWalker<T extends Timed> {
private p = 0
@ -79,6 +79,23 @@ export default class ListWalker<T extends Timed> {
return this.p;
}
private hasNext() {
return this.p < this.length
}
private hasPrev() {
return this.p > 0
}
protected moveNext(): T | null {
return this.hasNext()
? this.list[ this.p++ ]
: null
}
protected movePrev(): T | null {
return this.hasPrev()
? this.list[ --this.p ]
: null
}
/*
Returns last message with the time <= t.
Assumed that the current message is already handled so
@ -94,11 +111,11 @@ export default class ListWalker<T extends Timed> {
let changed = false;
while (this.p < this.length && this.list[this.p][key] <= val) {
this.p++;
this.moveNext()
changed = true;
}
while (this.p > 0 && this.list[ this.p - 1 ][key] > val) {
this.p--;
this.movePrev()
changed = true;
}
return changed ? this.list[ this.p - 1 ] : null;
@ -112,10 +129,10 @@ export default class ListWalker<T extends Timed> {
const list = this.list
while (list[this.p] && list[this.p].time <= t) {
fn(list[ this.p++ ]);
fn(this.moveNext())
}
while (fnBack && this.p > 0 && list[ this.p - 1 ].time > t) {
fnBack(list[ --this.p ]);
fnBack(this.movePrev());
}
}

View file

@ -0,0 +1,31 @@
import type { Timed } from './messages/timed';
import ListWalker from './ListWalker'
type CheckFn<T> = (t: T) => boolean
export default class ListWalkerWithMarks<T extends Timed> extends ListWalker<T> {
private _markCountNow: number = 0
constructor(private isMarked: CheckFn<T>, initialList?: T[]) {
super(initialList)
}
protected moveNext() {
const val = super.moveNext()
if (val && this.isMarked(val)) {
this._markCountNow++
}
return val
}
protected movePrev() {
const val = super.movePrev()
if (val && this.isMarked(val)) {
this._markCountNow--
}
return val
}
get markCountNow(): number {
return this._markCountNow
}
}

View file

@ -0,0 +1,15 @@
import { State } from './types'
// (not a type)
export default class SimpleSore<G, S=G> implements State<G, S> {
constructor(private state: G){}
get(): G {
return this.state
}
update(newState: Partial<S>) {
Object.assign(this.state, newState)
}
}

View file

@ -1,11 +1,40 @@
import Player from './Player';
import { update, cleanStore, getState } from './store';
import { clean as cleanLists } from './lists';
import WebPlayer from './_web/WebPlayer';
import reduxStore, {update, cleanStore} from './_store';
/** @type {Player} */
let instance = null;
import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager'
import { State as PState, INITIAL_STATE as PLAYER_INITIAL_STATE } from './player/Player'
import { Store } from './player/types'
const initCheck = method => (...args) => {
const INIT_STATE = {
...MM_INITIAL_STATE,
...PLAYER_INITIAL_STATE,
}
const myStore: Store<PState & MMState> = {
get() {
return reduxStore.getState()
},
update(s) {
update(s)
}
}
let instance: WebPlayer | null = null;
export function init(session, config, live = false) {
instance = new WebPlayer(myStore, session, config, live);
}
export function clean() {
if (instance === null) return;
instance.clean();
cleanStore()
instance = null;
}
const initCheck = (method) => (...args) => {
if (instance === null) {
console.error("Player method called before Player have been initialized.");
return;
@ -13,47 +42,7 @@ const initCheck = method => (...args) => {
return method(...args);
}
let autoPlay = true;
document.addEventListener("visibilitychange", function() {
if (instance === null) return;
if (document.hidden) {
const { playing } = getState();
autoPlay = playing
if (playing) {
instance.pause();
}
} else if (autoPlay) {
instance.play();
}
});
export function init(session, config, live = false) {
const endTime = !live && session.duration.valueOf();
instance = new Player(session, config, live);
update({
initialized: true,
live,
livePlay: live,
endTime, // : 0, //TODO: through initialState
session,
});
if (!document.hidden) {
instance.play();
}
}
export function clean() {
if (instance === null) return;
instance.clean();
cleanStore();
cleanLists();
instance = null;
}
export const jump = initCheck((...args) => instance.jump(...args));
export const togglePlay = initCheck((...args) => instance.togglePlay(...args));
export const pause = initCheck((...args) => instance.pause(...args));
export const toggleSkip = initCheck((...args) => instance.toggleSkip(...args));
@ -64,9 +53,22 @@ export const toggleEvents = initCheck((...args) => instance.toggleEvents(...args
export const speedUp = initCheck((...args) => instance.speedUp(...args));
export const speedDown = initCheck((...args) => instance.speedDown(...args));
export const attach = initCheck((...args) => instance.attach(...args));
export const markElement = initCheck((...args) => instance.marker && instance.marker.mark(...args));
export const markElement = initCheck((...args) => instance.mark(...args));
export const scale = initCheck(() => instance.scale());
/** @type {WebPlayer.toggleTimetravel} */
export const toggleTimetravel = initCheck((...args) => instance.toggleTimetravel(...args))
export const toggleInspectorMode = initCheck((...args) => instance.toggleInspectorMode(...args));
export const markTargets = initCheck((...args) => instance.markTargets(...args))
export const activeTarget =initCheck((...args) => instance.setActiveTarget(...args))
export const jumpToLive = initCheck((...args) => instance.jumpToLive(...args))
export const toggleUserName = initCheck((...args) => instance.toggleUserName(...args))
// !not related to player, but rather to the OR platform.
export const injectNotes = () => {} // initCheck((...args) => instance.injectNotes(...args))
export const filterOutNote = () => {} //initCheck((...args) => instance.filterOutNote(...args))
/** @type {Player.assistManager.call} */
export const callPeer = initCheck((...args) => instance.assistManager.call(...args))
/** @type {Player.assistManager.setCallArgs} */
@ -75,17 +77,10 @@ export const setCallArgs = initCheck((...args) => instance.assistManager.setCall
export const initiateCallEnd = initCheck((...args) => instance.assistManager.initiateCallEnd(...args))
export const requestReleaseRemoteControl = initCheck((...args) => instance.assistManager.requestReleaseRemoteControl(...args))
export const releaseRemoteControl = initCheck((...args) => instance.assistManager.releaseRemoteControl(...args))
export const markTargets = initCheck((...args) => instance.markTargets(...args))
export const activeTarget = initCheck((...args) => instance.activeTarget(...args))
export const toggleAnnotation = initCheck((...args) => instance.assistManager.toggleAnnotation(...args))
/** @type {Player.toggleTimetravel} */
export const toggleTimetravel = initCheck((...args) => instance.toggleTimetravel(...args))
export const jumpToLive = initCheck((...args) => instance.jumpToLive(...args))
export const toggleUserName = initCheck((...args) => instance.toggleUserName(...args))
export const injectNotes = initCheck((...args) => instance.injectNotes(...args))
export const filterOutNote = initCheck((...args) => instance.filterOutNote(...args))
/** @type {Player.assistManager.toggleVideoLocalStream} */
export const toggleVideoLocalStream = initCheck((...args) => instance.assistManager.toggleVideoLocalStream(...args))
export const toggleAnnotation = initCheck((...args) => instance.assistManager.toggleAnnotation(...args))
export const Controls = {
jump,
@ -99,4 +94,4 @@ export const Controls = {
speedUp,
speedDown,
callPeer
}
}

View file

@ -1,20 +1,20 @@
import { applyChange, revertChange } from 'deep-diff';
import { INITIAL_STATE as listsInitialState } from '../lists';
import { INITIAL_STATE as playerInitialState, INITIAL_NON_RESETABLE_STATE as playerInitialNonResetableState } from '../Player';
import { INITIAL_STATE as MM_INITIAL_STATE } from '../_web/MessageManager'
import { INITIAL_STATE as PLAYER_INITIAL_STATE } from '../player/Player'
const UPDATE = 'player/UPDATE';
const CLEAN = 'player/CLEAN';
const REDUX = 'player/REDUX';
const resetState = {
...listsInitialState,
...playerInitialState,
...MM_INITIAL_STATE,
...PLAYER_INITIAL_STATE,
initialized: false,
};
const initialState = {
...resetState,
...playerInitialNonResetableState,
}
export default (state = initialState, action = {}) => {

View file

@ -0,0 +1,71 @@
import ListWalker from '../_common/ListWalker';
import ListWalkerWithMarks from '../_common/ListWalkerWithMarks';
import type { Message } from './messages'
const SIMPLE_LIST_NAMES = [ "event", "redux", "mobx", "vuex", "zustand", "ngrx", "graphql", "exceptions", "profiles"] as const;
const MARKED_LIST_NAMES = [ "log", "resource", "fetch", "stack" ] as const;
//const entityNamesSimple = [ "event", "profile" ];
const LIST_NAMES = [...SIMPLE_LIST_NAMES, ...MARKED_LIST_NAMES ];
// TODO: provide correct types
export const INITIAL_STATE = LIST_NAMES.reduce((state, name) => {
state[`${name}List`] = []
state[`${name}ListNow`] = []
if (MARKED_LIST_NAMES.includes(name)) {
state[`${name}MarkedCountNow`] = 0
}
return state
}, {})
type SimpleListsObject = {
[key in typeof SIMPLE_LIST_NAMES[number]]: ListWalker<any>
}
type MarkedListsObject = {
[key in typeof MARKED_LIST_NAMES[number]]: ListWalkerWithMarks<any>
}
type ListsObject = SimpleListsObject & MarkedListsObject
type InitialLists = {
[key in typeof LIST_NAMES[number]]: any[]
}
export default class Lists {
lists: ListsObject
constructor(initialLists: Partial<InitialLists> = {}) {
const lists: Partial<ListsObject> = {}
for (const name of SIMPLE_LIST_NAMES) {
lists[name] = new ListWalker(initialLists[name])
}
for (const name of MARKED_LIST_NAMES) {
// TODO: provide types
lists[name] = new ListWalkerWithMarks((el) => el.isRed(), initialLists[name])
}
this.lists = lists as ListsObject
}
getFullListsState() {
return LIST_NAMES.reduce((state, name) => {
state[`${name}List`] = this.lists[name].list
return state
}, {})
}
moveGetState(t: number) {
return LIST_NAMES.reduce((state, name) => {
const lastMsg = this.lists[name].moveGetLast(t) // index: name === 'exceptions' ? undefined : index);
if (lastMsg != null) {
state[`${name}ListNow`] = this.lists[name].listNow
}
return state
}, MARKED_LIST_NAMES.reduce((state, name) => {
state[`${name}RedCountNow`] = this.lists[name].markCountNow // Red --> Marked
return state
}, {})
);
}
}

View file

@ -2,37 +2,31 @@
import { Decoder } from "syncod";
import logger from 'App/logger';
import Resource, { TYPES } from 'Types/session/resource'; // MBTODO: player types?
import Resource, { TYPES } from 'Types/session/resource';
import { TYPES as EVENT_TYPES } from 'Types/session/event';
import Log from 'Types/session/log';
import { update } from '../store';
import { toast } from 'react-toastify';
import {
init as initListsDepr,
append as listAppend,
setStartTime as setListsStartTime
} from '../lists';
import type { Store } from '../player/types';
import ListWalker from '../_common/ListWalker';
import StatedScreen from './StatedScreen/StatedScreen';
import Screen from './Screen/Screen';
import ListWalker from './managers/ListWalker';
import PagesManager from './managers/PagesManager';
import MouseMoveManager from './managers/MouseMoveManager';
import PerformanceTrackManager from './managers/PerformanceTrackManager';
import WindowNodeCounter from './managers/WindowNodeCounter';
import ActivityManager from './managers/ActivityManager';
import AssistManager from './managers/AssistManager';
import MFileReader from './messages/MFileReader';
import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles';
import { decryptSessionBytes } from './network/crypto';
import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './StatedScreen/StatedScreen';
import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './managers/AssistManager';
import { INITIAL_STATE as LISTS_INITIAL_STATE , LIST_NAMES, initLists } from './Lists';
import { INITIAL_STATE as SUPER_INITIAL_STATE, State as SuperState } from './Screen/Screen';
import { INITIAL_STATE as ASSIST_INITIAL_STATE, State as AssistState } from './assist/AssistManager';
import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE } from './Lists';
import type { PerformanceChartPoint } from './managers/PerformanceTrackManager';
import type { SkipInterval } from './managers/ActivityManager';
@ -50,8 +44,16 @@ export interface State extends SuperState, AssistState {
domBuildingTime?: any,
loadTime?: any,
error: boolean,
devtoolsLoading: boolean
devtoolsLoading: boolean,
liveTimeTravel: boolean,
messagesLoading: boolean,
cssLoading: boolean,
ready: boolean,
lastMessageTime: number,
}
export const INITIAL_STATE: State = {
...SUPER_INITIAL_STATE,
...LISTS_INITIAL_STATE,
@ -60,6 +62,14 @@ export const INITIAL_STATE: State = {
skipIntervals: [],
error: false,
devtoolsLoading: false,
liveTimeTravel: false,
messagesLoading: false,
cssLoading: false,
get ready() {
return !this.messagesLoading && !this.cssLoading
},
lastMessageTime: 0,
};
@ -82,7 +92,7 @@ const visualChanges = [
"set_viewport_scroll",
]
export default class MessageDistributor extends StatedScreen {
export default class MessageManager extends Screen {
// TODO: consistent with the other data-lists
private locationEventManager: ListWalker<any>/*<LocationEvent>*/ = new ListWalker();
private locationManager: ListWalker<SetPageLocation> = new ListWalker();
@ -95,12 +105,11 @@ export default class MessageDistributor extends StatedScreen {
private resizeManager: ListWalker<SetViewportSize> = new ListWalker([]);
private pagesManager: PagesManager;
private mouseMoveManager: MouseMoveManager;
private assistManager: AssistManager;
private scrollManager: ListWalker<SetViewportScroll> = new ListWalker();
private readonly decoder = new Decoder();
private readonly lists = initLists();
private readonly lists: Lists;
private activityManager: ActivityManager | null = null;
@ -109,37 +118,39 @@ export default class MessageDistributor extends StatedScreen {
private lastMessageTime: number = 0;
private lastMessageInFileTime: number = 0;
constructor(private readonly session: any /*Session*/, config: any, live: boolean) {
constructor(
private readonly session: any /*Session*/,
private readonly state: Store<State>,
config: any,
live: boolean,
) {
super();
this.pagesManager = new PagesManager(this, this.session.isMobile)
this.mouseMoveManager = new MouseMoveManager(this);
this.assistManager = new AssistManager(session, this, config);
this.sessionStart = this.session.startedAt;
if (live) {
initListsDepr({})
this.assistManager.connect(this.session.agentToken);
this.lists = new Lists()
} else {
this.activityManager = new ActivityManager(this.session.duration.milliseconds);
/* == REFACTOR_ME == */
const eventList = this.session.events.toJSON();
initListsDepr({
event: eventList,
stack: this.session.stackEvents.toJSON(),
resource: this.session.resources.toJSON(),
});
const eventList = session.events.toJSON();
// TODO: fix types for events, remove immutable js
eventList.forEach((e: Record<string, string>) => {
if (e.type === EVENT_TYPES.LOCATION) { //TODO type system
this.locationEventManager.append(e);
}
});
this.session.errors.forEach((e: Record<string, string>) => {
this.lists.exceptions.append(e);
});
})
this.lists = new Lists({
event: eventList,
stack: session.stackEvents.toJSON(),
resource: session.resources.toJSON(),
exceptions: session.errors,
})
/* === */
this.loadMessages();
}
@ -187,23 +198,21 @@ export default class MessageDistributor extends StatedScreen {
private waitingForFiles: boolean = false
private onFileReadSuccess = () => {
const stateToUpdate: {[key:string]: any} = {
const stateToUpdate = {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvaliability: this.performanceTrackManager.avaliability,
...this.lists.getFullListsState()
}
LIST_NAMES.forEach(key => {
stateToUpdate[ `${ key }List` ] = this.lists[ key ].list
})
if (this.activityManager) {
this.activityManager.end()
stateToUpdate.skipIntervals = this.activityManager.list
}
update(stateToUpdate)
this.state.update(stateToUpdate)
}
private onFileReadFailed = (e: any) => {
logger.error(e)
update({ error: true })
this.state.update({ error: true })
toast.error('Error requesting a session file')
}
private onFileReadFinally = () => {
@ -247,14 +256,14 @@ export default class MessageDistributor extends StatedScreen {
// load devtools
if (this.session.devtoolsURL.length) {
update({ devtoolsLoading: true })
this.state.update({ devtoolsLoading: true })
loadFiles(this.session.devtoolsURL, createNewParser())
.catch(() =>
requestEFSDevtools(this.session.sessionId)
.then(createNewParser(false))
)
//.catch() // not able to download the devtools file
.finally(() => update({ devtoolsLoading: false }))
.finally(() => this.state.update({ devtoolsLoading: false }))
}
}
@ -264,7 +273,7 @@ export default class MessageDistributor extends StatedScreen {
this.parseAndDistributeMessages(new MFileReader(byteArray, this.sessionStart), onMessage)
}
const updateState = () =>
update({
this.state.update({
liveTimeTravel: true,
});
@ -304,7 +313,7 @@ export default class MessageDistributor extends StatedScreen {
/* == REFACTOR_ME == */
const lastLoadedLocationMsg = this.loadedLocationManager.moveGetLast(t, index);
if (!!lastLoadedLocationMsg) {
setListsStartTime(lastLoadedLocationMsg.time)
// TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time)
this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart;
}
const llEvent = this.locationEventManager.moveGetLast(t, index);
@ -340,15 +349,8 @@ export default class MessageDistributor extends StatedScreen {
stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time;
}
LIST_NAMES.forEach(key => {
const lastMsg = this.lists[key].moveGetLast(t, key === 'exceptions' ? undefined : index);
if (lastMsg != null) {
// @ts-ignore TODO: fix types
stateToUpdate[`${key}ListNow`] = this.lists[key].listNow;
}
});
Object.keys(stateToUpdate).length > 0 && update(stateToUpdate);
this.lists.moveGetState(t)
Object.keys(stateToUpdate).length > 0 && this.state.update(stateToUpdate);
/* Sequence of the managers is important here */
// Preparing the size of "screen"
@ -405,6 +407,7 @@ export default class MessageDistributor extends StatedScreen {
private distributeMessage(msg: Message, index: number): void {
const lastMessageTime = Math.max(msg.time, this.lastMessageTime)
this.lastMessageTime = lastMessageTime
this.state.update({ lastMessageTime })
if (visualChanges.includes(msg.tp)) {
this.activityManager?.updateAcctivity(msg.time);
}
@ -414,15 +417,15 @@ export default class MessageDistributor extends StatedScreen {
/* Lists: */
case "console_log":
if (msg.level === 'debug') break;
listAppend("log", Log({
this.lists.lists.log.append(Log({
level: msg.level,
value: msg.value,
time,
index,
}));
}))
break;
case "fetch":
listAppend("fetch", Resource({
this.lists.lists.fetch.append(Resource({
method: msg.method,
url: msg.url,
payload: msg.request,
@ -469,42 +472,42 @@ export default class MessageDistributor extends StatedScreen {
decoded = this.decodeStateMessage(msg, ["state", "action"]);
logger.log('redux', decoded)
if (decoded != null) {
this.lists.redux.append(decoded);
this.lists.lists.redux.append(decoded);
}
break;
case "ng_rx":
decoded = this.decodeStateMessage(msg, ["state", "action"]);
logger.log('ngrx', decoded)
if (decoded != null) {
this.lists.ngrx.append(decoded);
this.lists.lists.ngrx.append(decoded);
}
break;
case "vuex":
decoded = this.decodeStateMessage(msg, ["state", "mutation"]);
logger.log('vuex', decoded)
if (decoded != null) {
this.lists.vuex.append(decoded);
this.lists.lists.vuex.append(decoded);
}
break;
case "zustand":
decoded = this.decodeStateMessage(msg, ["state", "mutation"])
logger.log('zustand', decoded)
if (decoded != null) {
this.lists.zustand.append(decoded)
this.lists.lists.zustand.append(decoded)
}
case "mob_x":
decoded = this.decodeStateMessage(msg, ["payload"]);
logger.log('mobx', decoded)
if (decoded != null) {
this.lists.mobx.append(decoded);
this.lists.lists.mobx.append(decoded);
}
break;
case "graph_ql":
this.lists.graphql.append(msg);
this.lists.lists.graphql.append(msg);
break;
case "profiler":
this.lists.profiles.append(msg);
this.lists.lists.profiles.append(msg);
break;
default:
switch (msg.tp) {
@ -540,11 +543,27 @@ export default class MessageDistributor extends StatedScreen {
return this.pagesManager.minTime;
}
setMessagesLoading(messagesLoading: boolean) {
this.display(!messagesLoading);
this.state.update({ messagesLoading });
}
setCSSLoading(cssLoading: boolean) {
this.displayFrame(!cssLoading);
this.state.update({ cssLoading });
}
private setSize({ height, width }: { height: number, width: number }) {
this.scale({ height, width });
this.state.update({ width, height });
//this.updateMarketTargets()
}
// TODO: clean managers?
clean() {
super.clean();
update(INITIAL_STATE);
this.assistManager.clear();
this.state.update(INITIAL_STATE);
this.incomingMessages.length = 0
}

View file

@ -1,5 +1,4 @@
import styles from './screen.module.css';
import { getState } from '../../../store';
import type { Point } from './types';
@ -86,9 +85,6 @@ export default abstract class BaseScreen {
parentElement.appendChild(this.screen);
this.parentElement = parentElement;
// parentElement.onresize = this.scale;
window.addEventListener('resize', this.scale);
this.scale();
/* == For the Inspecting Document content == */
this.overlay.addEventListener('contextmenu', () => {
@ -197,9 +193,8 @@ export default abstract class BaseScreen {
return this.s;
}
_scale() {
scale({ height, width }: { height: number, width: number }) {
if (!this.parentElement) return;
const { height, width } = getState();
const { offsetWidth, offsetHeight } = this.parentElement;
this.s = Math.min(offsetWidth / width, offsetHeight / height);
@ -216,13 +211,4 @@ export default abstract class BaseScreen {
this.boundingRect = this.overlay.getBoundingClientRect();
}
scale = () => { // TODO: solve classes inheritance issues in typescript
this._scale();
}
clean() {
window.removeEventListener('resize', this.scale);
}
}

View file

@ -1,32 +1,32 @@
import type BaseScreen from './BaseScreen'
import styles from './marker.module.css';
function escapeRegExp(string) {
function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
function escapeHtml(string) {
function escapeHtml(string: string) {
return string.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;');
}
function safeString(string) {
function safeString(string: string) {
return (escapeHtml(escapeRegExp(string)))
}
export default class Marker {
_target = null;
_selector = null;
_tooltip = null;
private _target: Element | null = null;
private selector: string | null = null;
private tooltip: HTMLDivElement
private marker: HTMLDivElement
constructor(overlay, screen) {
this.screen = screen;
this._tooltip = document.createElement('div');
this._tooltip.className = styles.tooltip;
this._tooltip.appendChild(document.createElement('div'));
constructor(overlay: HTMLElement, private readonly screen: BaseScreen) {
this.tooltip = document.createElement('div');
this.tooltip.className = styles.tooltip;
this.tooltip.appendChild(document.createElement('div'));
const htmlStr = document.createElement('div');
htmlStr.innerHTML = '<b>Right-click > Inspect</b> for more details.';
this._tooltip.appendChild(htmlStr);
this.tooltip.appendChild(htmlStr);
const marker = document.createElement('div');
marker.className = styles.marker;
@ -43,34 +43,34 @@ export default class Marker {
marker.appendChild(markerT);
marker.appendChild(markerB);
marker.appendChild(this._tooltip);
marker.appendChild(this.tooltip);
overlay.appendChild(marker);
this._marker = marker;
this.marker = marker;
}
get target() {
return this._target;
}
mark(element) {
mark(element: Element | null) {
if (this._target === element) {
return;
}
this._target = element;
this._selector = null;
this.selector = null;
this.redraw();
}
unmark() {
this.mark(null);
this.mark(null)
}
_autodefineTarget() {
private autodefineTarget() {
// TODO: put to Screen
if (this._selector) {
if (this.selector) {
try {
const fitTargets = this.screen.document.querySelectorAll(this._selector);
const fitTargets = this.screen.document.querySelectorAll(this.selector);
if (fitTargets.length === 0) {
this._target = null;
} else {
@ -90,9 +90,9 @@ export default class Marker {
}
}
markBySelector(selector) {
this._selector = selector;
this._autodefineTarget();
markBySelector(selector: string) {
this.selector = selector;
this.autodefineTarget();
this.redraw();
}
@ -116,20 +116,20 @@ export default class Marker {
}
redraw() {
if (this._selector) {
this._autodefineTarget();
if (this.selector) {
this.autodefineTarget();
}
if (!this._target) {
this._marker.style.display = 'none';
this.marker.style.display = 'none';
return;
}
const rect = this._target.getBoundingClientRect();
this._marker.style.display = 'block';
this._marker.style.left = rect.left + 'px';
this._marker.style.top = rect.top + 'px';
this._marker.style.width = rect.width + 'px';
this._marker.style.height = rect.height + 'px';
this.marker.style.display = 'block';
this.marker.style.left = rect.left + 'px';
this.marker.style.top = rect.top + 'px';
this.marker.style.width = rect.width + 'px';
this.marker.style.height = rect.height + 'px';
this._tooltip.firstChild.innerHTML = this.getTagString(this._target);
this.tooltip.firstChild.innerHTML = this.getTagString(this._target);
}
}

View file

@ -2,7 +2,6 @@ import Marker from './Marker';
import Cursor from './Cursor';
import Inspector from './Inspector';
// import styles from './screen.module.css';
// import { getState } from '../../../store';
import BaseScreen from './BaseScreen';
export { INITIAL_STATE } from './BaseScreen';
@ -12,7 +11,7 @@ export default class Screen extends BaseScreen {
public readonly cursor: Cursor;
private substitutor: BaseScreen | null = null;
private inspector: Inspector | null = null;
private marker: Marker | null = null;
public marker: Marker | null = null;
constructor() {
super();
this.cursor = new Cursor(this.overlay);
@ -26,14 +25,14 @@ export default class Screen extends BaseScreen {
return this.getElementsFromInternalPoint(this.cursor.getPosition());
}
_scale() {
super._scale();
scale(dims: { height: number, width: number }) {
super.scale(dims)
if (this.substitutor) {
this.substitutor._scale();
this.substitutor.scale(dims)
}
}
enableInspector(clickCallback: ({ target: Element }) => void): Document | null {
enableInspector(clickCallback: (e: { target: Element }) => void): Document | null {
if (!this.parentElement) return null;
if (!this.substitutor) {
this.substitutor = new Screen();

View file

@ -0,0 +1,178 @@
import type { Store } from '../player/types'
import Player, { State as PlayerState } from '../player/Player'
import MessageManager from './MessageManager'
import AssistManager from './assist/AssistManager'
import Screen from './Screen/Screen'
import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './MessageManager'
export default class WebPlayer extends Player {
private readonly screen: Screen
private readonly messageManager: MessageManager
assistManager: AssistManager // public so far
constructor(private wpState: Store<MMState & PlayerState>, session, config, live: boolean) {
// TODO: separate screen from manager
const screen = new MessageManager(session, wpState, config, live)
super(wpState, screen)
this.screen = screen
this.messageManager = screen
// TODO: separate LiveWebPlayer
this.assistManager = new AssistManager(session, this.messageManager, config, wpState)
const endTime = !live && session.duration.valueOf()
wpState.update({
//@ts-ignore
initialized: true,
//@ts-ignore
session,
live,
livePlay: live,
endTime, // : 0, //TODO: through initialState
})
if (live) {
this.assistManager.connect(session.agentToken)
}
}
attach(parent: HTMLElement) {
this.screen.attach(parent)
window.addEventListener('resize', this.scale)
this.scale()
}
scale = () => {
const { width, height } = this.wpState.get()
this.screen.scale({ width, height })
}
mark(e: Element) {
this.screen.marker.mark(e)
}
updateMarketTargets() {
// const { markedTargets } = getState();
// if (markedTargets) {
// update({
// markedTargets: markedTargets.map((mt: any) => ({
// ...mt,
// boundingRect: this.calculateRelativeBoundingRect(mt.el),
// })),
// });
// }
}
// private calculateRelativeBoundingRect(el: Element): BoundingRect {
// if (!this.parentElement) return {top:0, left:0, width:0,height:0} //TODO
// const { top, left, width, height } = el.getBoundingClientRect();
// const s = this.getScale();
// const scrinRect = this.screen.getBoundingClientRect();
// const parentRect = this.parentElement.getBoundingClientRect();
// return {
// top: top*s + scrinRect.top - parentRect.top,
// left: left*s + scrinRect.left - parentRect.left,
// width: width*s,
// height: height*s,
// }
// }
setActiveTarget(index: number) {
// const window = this.window
// const markedTargets: MarkedTarget[] | null = getState().markedTargets
// const target = markedTargets && markedTargets[index]
// if (target && window) {
// const { fixedTop, rect } = getOffset(target.el, window)
// const scrollToY = fixedTop - window.innerHeight / 1.5
// if (rect.top < 0 || rect.top > window.innerHeight) {
// // behavior hack TODO: fix it somehow when they will decide to remove it from browser api
// // @ts-ignore
// window.scrollTo({ top: scrollToY, behavior: 'instant' })
// setTimeout(() => {
// if (!markedTargets) { return }
// update({
// markedTargets: markedTargets.map(t => t === target ? {
// ...target,
// boundingRect: this.calculateRelativeBoundingRect(target.el),
// } : t)
// })
// }, 0)
// }
// }
// update({ activeTargetIndex: index });
}
// private actualScroll: Point | null = null
setMarkedTargets(selections: { selector: string, count: number }[] | null) {
// if (selections) {
// const totalCount = selections.reduce((a, b) => {
// return a + b.count
// }, 0);
// const markedTargets: MarkedTarget[] = [];
// let index = 0;
// selections.forEach((s) => {
// const el = this.getElementBySelector(s.selector);
// if (!el) return;
// markedTargets.push({
// ...s,
// el,
// index: index++,
// percent: Math.round((s.count * 100) / totalCount),
// boundingRect: this.calculateRelativeBoundingRect(el),
// count: s.count,
// })
// });
// this.actualScroll = this.getCurrentScroll()
// update({ markedTargets });
// } else {
// if (this.actualScroll) {
// this.window?.scrollTo(this.actualScroll.x, this.actualScroll.y)
// this.actualScroll = null
// }
// update({ markedTargets: null });
// }
}
markTargets(targets: { selector: string, count: number }[] | null) {
// this.animator.pause();
// this.setMarkedTargets(targets);
}
toggleInspectorMode(flag, clickCallback) {
// if (typeof flag !== 'boolean') {
// const { inspectorMode } = getState();
// flag = !inspectorMode;
// }
// if (flag) {
// this.pause()
// update({ inspectorMode: true });
// return super.enableInspector(clickCallback);
// } else {
// super.disableInspector();
// update({ inspectorMode: false });
// }
}
async toggleTimetravel() {
if (!this.wpState.get().liveTimeTravel) {
return await this.messageManager.reloadWithUnprocessedFile()
}
}
toggleUserName(name?: string) {
this.screen.cursor.toggleUserName(name)
}
clean() {
super.clean()
this.assistManager.clean()
window.removeEventListener('resize', this.scale)
}
}

View file

@ -1,11 +1,10 @@
import type { Socket } from 'socket.io-client';
import type Peer from 'peerjs';
import type { MediaConnection } from 'peerjs';
import type MessageDistributor from '../MessageDistributor';
import store from 'App/store';
import type MessageManager from '../MessageManager';
import appStore from 'App/store';
import type { LocalStream } from './LocalStream';
import { update, getState } from '../../store';
// import { iceServerConfigFromString } from 'App/utils'
import type { Store } from '../../player/types'
import AnnotationCanvas from './AnnotationCanvas';
import MStreamReader from '../messages/MStreamReader';
import JSONRawMessageReader from '../messages/JSONRawMessageReader'
@ -76,10 +75,15 @@ export default class AssistManager {
private videoStreams: Record<string, MediaStreamTrack> = {}
// TODO: Session type
constructor(private session: any, private md: MessageDistributor, private config: any) {}
constructor(
private session: any,
private md: MessageManager,
private config: any,
private store: Store<State>,
) {}
private setStatus(status: ConnectionStatus) {
if (getState().peerConnectionStatus === ConnectionStatus.Disconnected &&
if (this.store.get().peerConnectionStatus === ConnectionStatus.Disconnected &&
status !== ConnectionStatus.Connected) {
return
}
@ -94,7 +98,7 @@ export default class AssistManager {
} else {
this.md.display(false);
}
update({ peerConnectionStatus: status });
this.store.update({ peerConnectionStatus: status });
}
private get peerID(): string {
@ -106,7 +110,7 @@ export default class AssistManager {
this.socketCloseTimeout && clearTimeout(this.socketCloseTimeout)
if (document.hidden) {
this.socketCloseTimeout = setTimeout(() => {
const state = getState()
const state = this.store.get()
if (document.hidden &&
(state.calling === CallingState.NoCall && state.remoteControl === RemoteControlStatus.Enabled)) {
this.socket?.close()
@ -134,7 +138,7 @@ export default class AssistManager {
}
const now = +new Date()
update({ assistStart: now })
this.store.update({ assistStart: now })
import('socket.io-client').then(({ default: io }) => {
if (this.cleaned) { return }
@ -162,7 +166,7 @@ export default class AssistManager {
})
socket.on("disconnect", () => {
this.toggleRemoteControl(false)
update({ calling: CallingState.NoCall })
this.store.update({ calling: CallingState.NoCall })
})
socket.on('messages', messages => {
jmr.append(messages) // as RawMessage[]
@ -172,7 +176,7 @@ export default class AssistManager {
this.setStatus(ConnectionStatus.Connected)
// Call State
if (getState().calling === CallingState.Reconnecting) {
if (this.store.get().calling === CallingState.Reconnecting) {
this._callSessionPeer() // reconnecting call (todo improve code separation)
}
}
@ -222,15 +226,15 @@ export default class AssistManager {
this.setStatus(ConnectionStatus.Disconnected)
}, 30000)
if (getState().remoteControl === RemoteControlStatus.Requesting) {
if (this.store.get().remoteControl === RemoteControlStatus.Requesting) {
this.toggleRemoteControl(false) // else its remaining
}
// Call State
if (getState().calling === CallingState.OnCall) {
update({ calling: CallingState.Reconnecting })
} else if (getState().calling === CallingState.Requesting){
update({ calling: CallingState.NoCall })
if (this.store.get().calling === CallingState.OnCall) {
this.store.update({ calling: CallingState.Reconnecting })
} else if (this.store.get().calling === CallingState.Requesting){
this.store.update({ calling: CallingState.NoCall })
}
})
socket.on('error', e => {
@ -263,7 +267,7 @@ export default class AssistManager {
private onMouseClick = (e: MouseEvent): void => {
if (!this.socket) { return; }
if (getState().annotating) { return; } // ignore clicks while annotating
if (this.store.get().annotating) { return; } // ignore clicks while annotating
const data = this.md.getInternalViewportCoordinates(e)
// const el = this.md.getElementFromPoint(e); // requires requestiong node_id from domManager
@ -299,31 +303,31 @@ export default class AssistManager {
this.md.overlay.addEventListener("click", this.onMouseClick)
this.md.overlay.addEventListener("wheel", this.onWheel)
this.md.toggleRemoteControlStatus(true)
update({ remoteControl: RemoteControlStatus.Enabled })
this.store.update({ remoteControl: RemoteControlStatus.Enabled })
} else {
this.md.overlay.removeEventListener("mousemove", this.onMouseMove)
this.md.overlay.removeEventListener("click", this.onMouseClick)
this.md.overlay.removeEventListener("wheel", this.onWheel)
this.md.toggleRemoteControlStatus(false)
update({ remoteControl: RemoteControlStatus.Disabled })
this.store.update({ remoteControl: RemoteControlStatus.Disabled })
this.toggleAnnotation(false)
}
}
requestReleaseRemoteControl = () => {
if (!this.socket) { return }
const remoteControl = getState().remoteControl
const remoteControl = this.store.get().remoteControl
if (remoteControl === RemoteControlStatus.Requesting) { return }
if (remoteControl === RemoteControlStatus.Disabled) {
update({ remoteControl: RemoteControlStatus.Requesting })
this.store.update({ remoteControl: RemoteControlStatus.Requesting })
this.socket.emit("request_control", JSON.stringify({
...this.session.agentInfo,
query: document.location.search
}))
// setTimeout(() => {
// if (getState().remoteControl !== RemoteControlStatus.Requesting) { return }
// if (this.store.get().remoteControl !== RemoteControlStatus.Requesting) { return }
// this.socket?.emit("release_control")
// update({ remoteControl: RemoteControlStatus.Disabled })
// this.store.update({ remoteControl: RemoteControlStatus.Disabled })
// }, 8000)
} else {
this.socket.emit("release_control")
@ -416,15 +420,15 @@ export default class AssistManager {
private handleCallEnd() {
this.callArgs && this.callArgs.onCallEnd()
this.callConnection[0] && this.callConnection[0].close()
update({ calling: CallingState.NoCall })
this.store.update({ calling: CallingState.NoCall })
this.callArgs = null
this.toggleAnnotation(false)
}
public initiateCallEnd = async () => {
this.socket?.emit("call_end", store.getState().getIn([ 'user', 'account', 'name']))
this.socket?.emit("call_end", appStore.getState().getIn([ 'user', 'account', 'name']))
this.handleCallEnd()
const remoteControl = getState().remoteControl
const remoteControl = this.store.get().remoteControl
if (remoteControl === RemoteControlStatus.Enabled) {
this.socket.emit("release_control")
this.toggleRemoteControl(false)
@ -432,10 +436,10 @@ export default class AssistManager {
}
private onRemoteCallEnd = () => {
if (getState().calling === CallingState.Requesting) {
if (this.store.get().calling === CallingState.Requesting) {
this.callArgs && this.callArgs.onReject()
this.callConnection[0] && this.callConnection[0].close()
update({ calling: CallingState.NoCall })
this.store.update({ calling: CallingState.NoCall })
this.callArgs = null
this.toggleAnnotation(false)
} else {
@ -487,10 +491,10 @@ export default class AssistManager {
/** Connecting to the app user */
private _callSessionPeer() {
if (![CallingState.NoCall, CallingState.Reconnecting].includes(getState().calling)) { return }
update({ calling: CallingState.Connecting })
if (![CallingState.NoCall, CallingState.Reconnecting].includes(this.store.get().calling)) { return }
this.store.update({ calling: CallingState.Connecting })
this._peerConnection(this.peerID);
this.socket && this.socket.emit("_agent_name", store.getState().getIn([ 'user', 'account', 'name']))
this.socket && this.socket.emit("_agent_name", appStore.getState().getIn([ 'user', 'account', 'name']))
}
private async _peerConnection(remotePeerId: string) {
@ -509,7 +513,7 @@ export default class AssistManager {
})
call.on('stream', stream => {
getState().calling !== CallingState.OnCall && update({ calling: CallingState.OnCall })
this.store.get().calling !== CallingState.OnCall && this.store.update({ calling: CallingState.OnCall })
this.videoStreams[call.peer] = stream.getVideoTracks()[0]
@ -529,9 +533,9 @@ export default class AssistManager {
}
toggleAnnotation(enable?: boolean) {
// if (getState().calling !== CallingState.OnCall) { return }
// if (this.store.get().calling !== CallingState.OnCall) { return }
if (typeof enable !== "boolean") {
enable = !!getState().annotating
enable = !!this.store.get().annotating
}
if (enable && !this.annot) {
const annot = this.annot = new AnnotationCanvas()
@ -559,11 +563,11 @@ export default class AssistManager {
annot.move([ data.x, data.y ])
this.socket.emit("moveAnnotation", [ data.x, data.y ])
})
update({ annotating: true })
this.store.update({ annotating: true })
} else if (!enable && !!this.annot) {
this.annot.remove()
this.annot = null
update({ annotating: false })
this.store.update({ annotating: false })
}
}
@ -577,7 +581,7 @@ export default class AssistManager {
/* ==== Cleaning ==== */
private cleaned: boolean = false
clear() {
clean() {
this.cleaned = true // sometimes cleaned before modules loaded
this.initiateCallEnd();
if (this._peer) {

View file

@ -1,18 +1,18 @@
import ListWalker from './ListWalker';
import ListWalker from '../../_common/ListWalker';
class SkipIntervalCls {
constructor(private readonly start = 0, private readonly end = 0) {}
constructor(readonly start = 0, readonly end = 0) {}
get time(): number {
return this.start;
}
contains(ts: number) {
contains(ts) {
return ts > this.start && ts < this.end;
}
}
export type SkipInterval = InstanceType<typeof SkipIntervalCls>; // exporting only class' type
export type SkipInterval = InstanceType<typeof SkipIntervalCls>;
export default class ActivityManager extends ListWalker<SkipInterval> {

View file

@ -1,9 +1,9 @@
import logger from 'App/logger';
import type StatedScreen from '../../StatedScreen';
import type MessageManager from '../../MessageManager';
import type { Message, SetNodeScroll, CreateElementNode } from '../../messages';
import ListWalker from '../ListWalker';
import ListWalker from '../../../_common/ListWalker';
import StylesManager, { rewriteNodeStyleSheet } from './StylesManager';
import FocusManager from './FocusManager';
import {
@ -51,7 +51,7 @@ export default class DOMManager extends ListWalker<Message> {
constructor(
private readonly screen: StatedScreen,
private readonly screen: MessageManager,
private readonly isMobile: boolean,
public readonly time: number
) {

View file

@ -1,7 +1,7 @@
import logger from 'App/logger';
import type { SetNodeFocus } from '../../messages';
import type { VElement } from './VirtualDOM';
import ListWalker from '../ListWalker';
import ListWalker from '../../../_common/ListWalker';
const FOCUS_CLASS = "-openreplay-focus"

View file

@ -1,9 +1,9 @@
import type StatedScreen from '../../StatedScreen';
import type MessageManager from '../../MessageManager';
import type { CssInsertRule, CssDeleteRule } from '../../messages';
type CSSRuleMessage = CssInsertRule | CssDeleteRule;
import ListWalker from '../ListWalker';
import ListWalker from '../../../_common/ListWalker';
const HOVER_CN = "-openreplay-hover";
@ -26,7 +26,7 @@ export default class StylesManager extends ListWalker<CSSRuleMessage> {
private linkLoadPromises: Array<Promise<void>> = [];
private skipCSSLinks: Array<string> = []; // should be common for all pages
constructor(private readonly screen: StatedScreen) {
constructor(private readonly screen: MessageManager) {
super();
}

View file

@ -1,7 +1,7 @@
import type StatedScreen from '../StatedScreen';
import type Screen from '../Screen/Screen';
import type { MouseMove } from '../messages';
import ListWalker from './ListWalker';
import ListWalker from '../../_common/ListWalker';
const HOVER_CLASS = "-openreplay-hover";
const HOVER_CLASS_DEPR = "-asayer-hover";
@ -9,7 +9,7 @@ const HOVER_CLASS_DEPR = "-asayer-hover";
export default class MouseMoveManager extends ListWalker<MouseMove> {
private hoverElements: Array<Element> = [];
constructor(private screen: StatedScreen) {super()}
constructor(private screen: Screen) {super()}
private updateHover(): void {
const curHoverElements = this.screen.getCursorTargets();

View file

@ -1,7 +1,7 @@
import type StatedScreen from '../StatedScreen';
import type Screen from '../Screen/Screen';
import type { Message } from '../messages';
import ListWalker from './ListWalker';
import ListWalker from '../../_common/ListWalker';
import DOMManager from './DOM/DOMManager';
@ -9,9 +9,9 @@ export default class PagesManager extends ListWalker<DOMManager> {
private currentPage: DOMManager | null = null
private isMobile: boolean;
private screen: StatedScreen;
private screen: Screen;
constructor(screen: StatedScreen, isMobile: boolean) {
constructor(screen: Screen, isMobile: boolean) {
super()
this.screen = screen
this.isMobile = isMobile

View file

@ -1,6 +1,6 @@
import type { PerformanceTrack, SetPageVisibility } from '../messages';
import ListWalker from './ListWalker';
import ListWalker from '../../_common/ListWalker';
export type PerformanceChartPoint = {
time: number,

View file

@ -1,5 +1,5 @@
// import { applyChange, revertChange } from 'deep-diff';
// import ListWalker from './ListWalker';
// import ListWalker from '../../_common/ListWalker';
// import type { Redux } from '../messages';
// export default class ReduxStateManager extends ListWalker<Redux> {

View file

@ -0,0 +1,25 @@
import SimpleStore from './_common/SimpleStore'
import type { Store } from './player/types'
import { State as MMState, INITIAL_STATE as MM_INITIAL_STATE } from './_web/MessageManager'
import { State as PState, INITIAL_STATE as PLAYER_INITIAL_STATE } from './player/Player'
import WebPlayer from './_web/WebPlayer'
export function createWebPlayer(session, config): [WebPlayer, Store<PState & MMState>] {
const store = new SimpleStore<PState & MMState>({
...PLAYER_INITIAL_STATE,
...MM_INITIAL_STATE,
})
const player = new WebPlayer(store, session, config, false)
return [player, store]
}
export function createLiveWebPlayer(session, config): [WebPlayer, Store<PState & MMState>] {
const store = new SimpleStore<PState & MMState>({
...PLAYER_INITIAL_STATE,
...MM_INITIAL_STATE,
})
const player = new WebPlayer(store, session, config, true)
return [player, store]
}

View file

@ -1,4 +1,6 @@
export * from './store';
export * from './singletone';
export * from './MessageDistributor/managers/AssistManager';
export * from './MessageDistributor/managers/LocalStream';
export * from './_store';
export * from './_web/assist/AssistManager';
export * from './_web/assist/LocalStream';
export * from './_singletone';
export * from './create';

View file

@ -1,454 +0,0 @@
import { io } from 'socket.io-client';
import { makeAutoObservable, autorun } from 'mobx';
import logger from 'App/logger';
import {
createPlayerState,
createToolPanelState,
createToggleState,
PLAYING,
PAUSED,
COMPLETED,
SOCKET_ERROR,
CRASHES,
LOGS,
NETWORK,
PERFORMANCE,
CUSTOM,
EVENTS, // last evemt +clicks
} from "./state";
import {
createListState,
createScreenListState,
} from './lists';
import Parser from './Parser';
import PerformanceList from './PerformanceList';
const HIGHEST_SPEED = 3;
export default class ImagePlayer {
_screen = null
_wrapper = null
_socket = null
toolPanel = createToolPanelState()
fullscreen = createToggleState()
lists = {
[LOGS]: createListState(),
[NETWORK]: createListState(),
[CRASHES]: createListState(),
[EVENTS]: createListState(),
[CUSTOM]: createListState(),
[PERFORMANCE]: new PerformanceList(),
}
_clicks = createListState()
_screens = createScreenListState()
constructor(session) {
this.state = createPlayerState({
endTime: session.duration.valueOf(),
});
//const canvas = document.createElement("canvas");
// this._context = canvas.getContext('2d');
// this._img = new Image();
// this._img..onerror = function(e){
// logger.log('Error during loading image:', e);
// };
// wrapper.appendChild(this._img);
session.crashes.forEach(c => this.lists[CRASHES].append(c));
session.events.forEach(e => this.lists[EVENTS].append(e));
session.stackEvents.forEach(e => this.lists[CUSTOM].append(e));
window.fetch(session.mobsUrl)
.then(r => r.arrayBuffer())
.then(b => {
new Parser(new Uint8Array(b)).parseEach(m => {
m.time = m.timestamp - session.startedAt;
try {
if (m.tp === "ios_log") {
this.lists[LOGS].append(m);
} else if (m.tp === "ios_network_call") {
this.lists[NETWORK].append(m);
// } else if (m.tp === "ios_custom_event") {
// this.lists[CUSTOM].append(m);
} else if (m.tp === "ios_click_event") {
m.time -= 600; //for graphic initiation
this._clicks.append(m);
} else if (m.tp === "ios_performance_event") {
this.lists[PERFORMANCE].append(m);
}
} catch (e) {
logger.error(e);
}
});
Object.values(this.lists).forEach(list => list.moveGetLast(0)); // In case of negative values
})
if (session.socket == null || typeof session.socket.jwt !== "string" || typeof session.socket.url !== "string") {
logger.error("No socket info found fpr session", session);
return
}
const options = {
extraHeaders: {Authorization: `Bearer ${session.socket.jwt}`},
reconnectionAttempts: 5,
//transports: ['websocket'],
}
const socket = this._socket = io(session.socket.url, options);
socket.on("connect", () => {
logger.log("Socket Connected");
});
socket.on('disconnect', (reason) => {
if (reason === 'io client disconnect') {
return;
}
logger.error("Disconnected. Reason: ", reason)
// if (reason === 'io server disconnect') {
// socket.connect();
// }
});
socket.on('connect_error', (e) => {
this.state.setState(SOCKET_ERROR);
logger.error(e)
});
socket.on('screen', (time, width, height, binary) => {
//logger.log("New Screen!", time, width, height, binary);
this._screens.insertScreen(time, width, height, binary);
});
socket.on('buffered', (playTime) => {
if (playTime === this.state.time) {
this.state.setBufferingState(false);
}
logger.log("Play ack!", playTime);
});
let startPingInterval;
socket.on('start', () => {
logger.log("Started!");
clearInterval(startPingInterval)
this.state.setBufferingState(true);
socket.emit("speed", this.state.speed);
this.play();
});
startPingInterval = setInterval(() => socket.emit("start"), 1000);
socket.emit("start");
window.addEventListener("resize", this.scale);
autorun(this.scale);
}
_click
_getClickElement() {
if (this._click != null) {
return this._click;
}
const click = document.createElement('div');
click.style.position = "absolute";
click.style.background = "#ddd";
click.style.border = "solid 4px #bbb";
click.style.borderRadius = "50%";
click.style.width = "32px";
click.style.height = "32px";
click.style.transformOrigin = "center";
return this._click = click;
}
// More sufficient ways?
_animateClick({ x, y }) {
if (this._screen == null) {
return;
}
const click = this._getClickElement();
if (click.parentElement == null) {
this._screen.appendChild(click);
}
click.style.transition = "none";
click.style.left = `${x-18}px`;
click.style.top = `${y-18}px`;
click.style.transform = "scale(1)";
click.style.opacity = "1";
setTimeout(() => {
click.style.transition = "all ease-in .5s";
click.style.transform = "scale(0)";
click.style.opacity = "0";
}, 0)
}
_updateFrame({ image, width, height }) {
// const img = new Image();
// img.onload = () => {
// this._context.drawImage(img);
// };
// img.onerror = function(e){
// logger.log('Error during loading image:', e);
// };
// this._screen.style.backgroundImage = `url(${binaryToDataURL(binaryArray)})`;
this._canvas.getContext('2d').drawImage(image, 0, 0, this._canvas.width, this._canvas.height);
}
_setTime(ts) {
ts = Math.max(Math.min(ts, this.state.endTime), 0);
this.state.setTime(ts);
Object.values(this.lists).forEach(list => list.moveGetLast(ts));
const screen = this._screens.moveGetLast(ts);
if (screen != null) {
const { dataURL, width, height } = screen;
this.state.setSize(width, height);
//imagePromise.then(() => this._updateFrame({ image, width, height }));
//this._screen.style.backgroundImage = `url(${screen.dataURL})`;
screen.loadImage.then(() => this._screen.style.backgroundImage = `url(${screen.dataURL})`);
}
const lastClick = this._clicks.moveGetLast(ts);
if (lastClick != null && lastClick.time > ts - 600) {
this._animateClick(lastClick);
}
}
attach({ wrapperId, screenId }) {
const screen = document.getElementById(screenId);
if (!screen) {
throw new Error(`ImagePlayer: No screen element found with ID "${screenId}" `);
}
const wrapper = document.getElementById(wrapperId);
if (!wrapper) {
throw new Error(`ImagePlayer: No wrapper element found with ID "${wrapperId}" `);
}
screen.style.backgroundSize = "contain";
screen.style.backgroundPosition = "center";
wrapper.style.position = "absolute";
wrapper.style.transformOrigin = "left top";
wrapper.style.top = "50%";
wrapper.style.left = "50%";
// const canvas = document.createElement('canvas');
// canvas.style.width = "300px";
// canvas.style.height = "600px";
// screen.appendChild(canvas);
// this._canvas = canvas;
this._screen = screen;
this._wrapper = wrapper;
this.scale();
}
get loading() {
return this.state.initializing;
}
get buffering() {
return this.state.buffering;
}
// get timeTravelDisabled() {
// return this.state.initializing;
// }
get controlsDisabled() {
return this.state.initializing; //|| this.state.buffering;
}
_animationFrameRequestId = null
_stopAnimation() {
cancelAnimationFrame(this._animationFrameRequestId);
}
_startAnimation() {
let prevTime = this.state.time;
let animationPrevTime = performance.now();
const nextFrame = (animationCurrentTime) => {
const {
speed,
//skip,
//skipIntervals,
endTime,
playing,
buffering,
//live,
//livePlay,
//disconnected,
//messagesLoading,
//cssLoading,
} = this.state;
const diffTime = !playing || buffering
? 0
: Math.max(animationCurrentTime - animationPrevTime, 0) * speed;
let time = prevTime + diffTime;
//const skipInterval = skip && skipIntervals.find(si => si.contains(time)); // TODO: good skip by messages
//if (skipInterval) time = skipInterval.end;
//const fmt = this.getFirstMessageTime();
//if (time < fmt) time = fmt; // ?
//const lmt = this.getLastMessageTime();
//if (livePlay && time < lmt) time = lmt;
// if (endTime < lmt) {
// update({
// endTime: lmt,
// });
// }
prevTime = time;
animationPrevTime = animationCurrentTime;
const completed = time >= endTime;
if (completed) {
this._setComplete();
} else {
// if (live && time > endTime) {
// update({
// endTime: time,
// });
// }
this._setTime(time);
this._animationFrameRequestId = requestAnimationFrame(nextFrame);
}
};
this._animationFrameRequestId = requestAnimationFrame(nextFrame);
}
scale = () => {
const { height, width } = this.state; // should be before any return for mobx observing
if (this._wrapper === null) return;
const parent = this._wrapper.parentElement;
if (parent === null) return;
let s = 1;
const { offsetWidth, offsetHeight } = parent;
s = Math.min(offsetWidth / width, (offsetHeight - 20) / height);
if (s > 1) {
s = 1;
} else {
s = Math.round(s * 1e3) / 1e3;
}
this._wrapper.style.transform = `scale(${ s }) translate(-50%, -50%)`;
this._wrapper.style.width = width + 'px';
this._wrapper.style.height = height + 'px';
// this._canvas.style.width = width + 'px';
// this._canvas.style.height = height + 'px';
}
_setComplete() {
this.state.setStatus(COMPLETED);
this._setTime(this.state.endTime);
if (this._socket != null) {
this._socket.emit("pause");
}
}
_pause() {
this._stopAnimation();
this.state.setStatus(PAUSED);
}
pause = () => {
this._pause();
if (this._socket != null) {
this._socket.emit("pause");
}
}
_play() {
if (!this.state.playing) {
this._startAnimation();
}
this.state.setStatus(PLAYING);
}
play = () => {
this._play()
if (this._socket != null) {
this._socket.emit("resume");
}
}
_jump(ts) {
if (this.state.playing) {
this._stopAnimation();
this._setTime(ts);
this._startAnimation();
} else {
this._setTime(ts);
this.state.setStatus(PAUSED); // for the case when completed
}
}
jump = (ts) => {
ts = Math.round(ts); // Should be integer
this._jump(ts);
if (this._socket != null) {
this.state.setBufferingState(true);
console.log("Send play on jump!", ts)
this._socket.emit("jump", ts);
}
}
togglePlay = () => {
if (this.state.playing) {
this.pause()
} else {
if (this.state.completed) {
//this.state.time = 0;
this.jump(0)
}
this.play()
}
}
backTenSeconds = () => {
this.jump(Math.max(this.state.time - 10000, 0));
}
forthTenSeconds = () => {
this.jump(Math.min(this.state.time + 10000, this.state.endTime));
}
_setSpeed(speed) {
if (this._socket != null) {
this._socket.emit("speed", speed);
}
this.state.setSpeed(speed)
}
toggleSpeed = () => {
const speed = this.state.speed;
this._setSpeed(speed < HIGHEST_SPEED ? speed + 1 : 1);
}
speedUp = () => {
const speed = this.state.speed;
this._setSpeed(Math.min(HIGHEST_SPEED, speed + 1));
}
speedDown = () => {
const speed = this.state.speed;
this._setSpeed(Math.max(1, speed - 1));
}
togglePanel = (key) => {
this.toolPanel.toggle(key);
setTimeout(() => this.scale(), 0);
}
closePanel = () => {
this.toolPanel.close();
setTimeout(() => this.scale(), 0);
}
toggleFullscreen = (flag = true) => {
this.fullscreen.toggle(flag);
setTimeout(() => this.scale(), 0);
}
clean() {
this._stopAnimation();
if (this._socket != null) {
//this._socket.emit("close");
this._socket.close();
}
this._screens.clean();
}
}

View file

@ -1,34 +0,0 @@
import RawMessageReader from '../MessageDistributor/messages/RawMessageReader';
export default class Parser {
private reader: RawMessageReader
private error: boolean = false
constructor(byteArray) {
this.reader = new RawMessageReader(byteArray)
}
parseEach(cb) {
while (this.hasNext()) {
const msg = this.next();
if (msg !== null) {
cb(msg);
}
}
}
hasNext() {
return !this.error && this.reader.hasNextByte();
}
next() {
try {
return this.reader.readMessage()
} catch(e) {
console.warn(e)
this.error = true
return null
}
}
}

View file

@ -1,73 +0,0 @@
import {
createListState,
} from './lists';
const MIN_INTERVAL = 500;
const NAME_MAP = {
"mainThreadCPU": "cpu",
"batteryLevel": "battery",
"memoryUsage": "memory",
}
export default class PerformanceList {
_list = createListState()
availability = {
cpu: false,
memory: false,
battery: false,
}
get list() {
return this._list.list;
}
get count() {
return this._list.count;
}
moveGetLast(t) {
this._list.moveGetLast(t);
}
append(m) {
if (!["mainThreadCPU", "memoryUsage", "batteryLevel", "thermalState", "activeProcessorCount", "isLowPowerModeEnabled"].includes(m.name)) {
return;
}
let lastPoint = Object.assign({ time: 0, cpu: null, battery: null, memory: null }, this._list.last);
if (this._list.length === 0) {
this._list.append(lastPoint);
}
if (NAME_MAP[m.name] != null) {
this.availability[ NAME_MAP[m.name] ] = true;
if (lastPoint[NAME_MAP[m.name]] === null) {
this._list.forEach(p => p[NAME_MAP[m.name]] = m.value);
lastPoint[NAME_MAP[m.name]] = m.value;
}
}
const newPoint = Object.assign({}, lastPoint, {
time: m.time,
[ NAME_MAP[m.name] || m.name ]: m.value,
});
const dif = m.time - lastPoint.time;
const insertCount = Math.floor(dif/MIN_INTERVAL);
for (let i = 0; i < insertCount; i++){
const evalValue = (key) => lastPoint[key] + Math.floor((newPoint[key]-lastPoint[key])/insertCount*(i + 1))
this._list.append({
...lastPoint,
time: evalValue("time"),
cpu: evalValue("cpu") + (Math.floor(5*Math.random())-2),
battery: evalValue("battery"),
memory: evalValue("memory")*(1 + (0.1*Math.random() - 0.05)),
});
}
this._list.append(newPoint);
}
}

View file

@ -1,57 +0,0 @@
import ListWalker from '../MessageDistributor/managers/ListWalker';
//URL.revokeObjectURL() !!
function binaryToDataURL(arrayBuffer){
var blob = new Blob([new Uint8Array(arrayBuffer)], {'type' : 'image/jpeg'});
return URL.createObjectURL(blob);
}
function prepareImage(width, height, arrayBuffer) {
const dataURL = binaryToDataURL(arrayBuffer);
return {
loadImage: new Promise(resolve => {
const img = new Image();
img.onload = function() {
//URL.revokeObjectURL(this.src);
resolve(img);
};
img.src = dataURL;
}).then(),
dataURL,
};
}
export default class ScreenList {
_walker = new ListWalker();
_insertUnique(m) {
let p = this._walker._list.length;
while (p > 0 && this._walker._list[ p - 1 ].time > m.time) {
p--;
}
if (p > 0 && this._walker._list[ p - 1 ].time === m.time) {
return;
}
this._walker._list.splice(p, 0, m);
}
moveGetLast(time) {
return this._walker.moveGetLast(time);
}
insertScreen(time, width, height, arrayBuffer): void {
this._insertUnique({
time,
width,
height,
...prepareImage(width, height, arrayBuffer),
//image: new ImageData(new Uint8ClampedArray(arrayBuffer), width, height),
// dataURL: binaryToDataURL(arrayBuffer)
});
}
clean() {
this._walker.forEach(m => {
URL.revokeObjectURL(m.dataURL);
});
}
}

View file

@ -1,12 +0,0 @@
import { makeAutoObservable } from "mobx"
import ListWalker from '../MessageDistributor/managers/ListWalker';
import ScreenList from './ScreenList';
export function createListState(list) {
return makeAutoObservable(new ListWalker(list));
}
export function createScreenListState() {
return makeAutoObservable(new ScreenList());
}

View file

@ -1,112 +0,0 @@
import { makeAutoObservable } from "mobx"
//configure ({empceActions: true})
export const
NONE = 0,
CRASHES = 1,
NETWORK = 2,
LOGS = 3,
EVENTS = 4,
CUSTOM = 5,
PERFORMANCE = 6;
export function createToolPanelState() {
return makeAutoObservable({
key: NONE,
toggle(key) { // auto-bind??
this.key = this.key === key ? NONE : key;
},
close() {
this.key = NONE;
},
});
}
export function createToggleState() {
return makeAutoObservable({
enabled: false,
toggle(flag) {
this.enabled = typeof flag === 'boolean'
? flag
: !this.enabled;
},
enable() {
this.enabled = true;
},
disable() {
this.enabled = false;
},
});
}
const SPEED_STORAGE_KEY = "__$player-speed$__";
//const SKIP_STORAGE_KEY = "__$player-skip$__";
//const initialSkip = !!localStorage.getItem(SKIP_STORAGE_KEY);
export const
INITIALIZING = 0,
PLAYING = 1,
PAUSED = 2,
COMPLETED = 3,
SOCKET_ERROR = 5;
export const
PORTRAIT = 1,
LANDSCAPE = 2;
export function createPlayerState(state) {
const storedSpeed = +localStorage.getItem(SPEED_STORAGE_KEY);
const initialSpeed = [1,2,3].includes(storedSpeed) ? storedSpeed : 1;
return makeAutoObservable({
status: INITIALIZING,
_statusSaved: null,
setTime(t) {
this.time = t
},
time: 0,
endTime: 0,
setStatus(status) {
this.status = status;
},
get initializing() {
return this.status === INITIALIZING;
},
get playing() {
return this.status === PLAYING;
},
get completed() {
return this.status === COMPLETED;
},
_buffering: false,
get buffering() {
return this._buffering;
},
setBufferingState(flag = true) {
this._buffering = flag;
},
speed: initialSpeed,
setSpeed(speed) {
localStorage.setItem(SPEED_STORAGE_KEY, speed);
this.speed = speed;
},
width: 360,
height: 780,
orientation: PORTRAIT,
get orientationLandscape() {
return this.orientation === LANDSCAPE;
},
setSize(width, height) {
if (height < 0 || width < 0) {
console.log("Player: wrong non-positive size")
return;
}
this.width = width;
this.height = height;
this.orientation = width > height ? LANDSCAPE : PORTRAIT;
},
...state,
});
}

View file

@ -1,124 +0,0 @@
export default class ListReader {
_callback;
_p = -1;
_list = [];
_offset = 0;
constructor(callback = Function.prototype) {
if (typeof callback !== 'function') {
return console.error("List Reader: wrong constructor argument. `callback` must be a function.");
}
this._callback = callback;
}
static checkItem(item) {
if(typeof item !== 'object' || item === null) {
console.error("List Reader: expected item to be not null object but got ", item);
return false;
}
if (typeof item.time !== 'number') {
console.error("List Reader: expected item to have number property 'time', ", item);
return false;
}
// if (typeof item.index !== 'number') {
// console.error("List Reader: expected item to have number property 'index', ", item);
// return false;
// } // future: All will have index
return true;
}
/* EXTENDABLE METHODS */
_onIncrement() {}
_onDecrement() {}
_onStartTimeChange() {}
inc() {
const item = this._list[ ++this._p ];
this._onIncrement(item);
return item;
}
dec() {
const item = this._list[ this._p-- ];
this._onDecrement(item);
return item
}
get _goToReturn() {
return { listNow: this.listNow };
}
goTo(time) {
const prevPointer = this._p;
while (!!this._list[ this._p + 1 ] && this._list[ this._p + 1 ].time <= time) {
this.inc();
}
while (this._p >= 0 && this._list[ this._p ].time > time) {
this.dec();
}
if (prevPointer !== this._p) {
//this._notify([ "listNow" ]);
return this._goToReturn;
}
}
goToIndex(index) { // thinkaboutit
const prevPointer = this._p;
while (!!this._list[ this._p + 1 ] &&
this._list[ this._p + 1 ].index <= index
) {
this.inc();
}
while (this._p >= 0 && this._list[ this._p ].index > index) {
this.dec();
}
if (prevPointer !== this._p) {
//this._notify([ "listNow" ]);
return this._goToReturn;
}
}
// happens rare MBTODO only in class ResourceListReader extends ListReaderWithRed
set startTime(time) {
const prevOffset = this._offset;
const prevPointer = this._p;
this._offset = this._list.findIndex(({ time, duration = 0 }) => time + duration >= time); // TODO: strict for duration rrrrr
this._p = Math.max(this._p, this._offset - 1);
if (prevOffset !== this._offset || prevPointer !== this._p) {
this._notify([ "listNow" ]);
}
this._onStartTimeChange();
}
get list() {
return this._list;
}
get count() {
return this._list.length;
}
get listNow() {
return this._list.slice(this._offset, this._p + 1);
}
set list(_list) {
if (!Array.isArray(_list)) {
console.error("List Reader: wrong list value.", _list)
}
const valid = _list.every(this.constructor.checkItem);
if (!valid) return;
this._list = _list; // future: time + index sort
this._notify([ "list", "count" ]);
}
append(item) {
if (!this.constructor.checkItem(item)) return;
this._list.push(item); // future: time + index sort
this._notify([ "count" ]); // list is the same by ref, CAREFULL
}
_notify(propertyList) {
const changedState = {};
propertyList.forEach(p => changedState[ p ] = this[ p ]);
this._callback(changedState);
}
}

View file

@ -1,48 +0,0 @@
import ListReader from './ListReader';
export default class ListReaderWithRed extends ListReader {
_redCountNow = 0;
static checkItem(item) {
const superCheckResult = super.checkItem(item);
if (typeof item.isRed !== 'function') {
console.error("List Reader With Red: expected item to have method 'isRed', ", item);
return false;
}
return superCheckResult;
}
get _goToReturn() {
return {
listNow: this.listNow,
redCountNow: this.redCountNow,
}
}
_onIncrement(item) {
if (item.isRed()) {
this._redCountNow++;
//this._notify([ "redCountNow" ]);
}
}
_onDecrement(item) {
if (item.isRed()) {
this._redCountNow--;
//this._notify([ "redCountNow" ]);
}
}
_onStartTimeChange() {
this._redCountNow = this._list
.slice(this._offset, this._p + 1)
.filter(item => item.isRed())
.length;
this._notify([ "redCountNow" ]);
}
get redCountNow() {
return this._redCountNow;
}
}

View file

@ -1,68 +0,0 @@
import ListReader from './ListReader';
import ListReaderWithRed from './ListReaderWithRed';
import { update as updateStore } from '../store';
const l = n => `${ n }List`;
const c = n => `${ n }Count`;
const ln = n => `${ n }ListNow`;
const rcn = n => `${ n }RedCountNow`;
const entityNamesWithRed = [ "log", "resource", "fetch", "stack" ];
const entityNamesSimple = [ "event", "profile" ];
const entityNames = /*[ "redux" ].*/entityNamesWithRed.concat(entityNamesSimple);
const is = {};
entityNames.forEach(n => {
is[ l(n) ] = [];
is[ c(n) ] = 0;
is[ ln(n) ] = [];
if (entityNamesWithRed.includes(n)) {
is[ rcn(n) ] = 0;
}
});
//is["reduxState"] = {};
//is["reduxFinalStates"] = [];
const createCallback = n => {
const entityfy = s => `${ n }${ s[ 0 ].toUpperCase() }${ s.slice(1) }`;
return state => {
if (!state) return;
const namedState = {};
Object.keys(state).forEach(key => {
namedState[ entityfy(key) ] = state[ key ];
});
return updateStore(namedState);
}
}
let readers = null;
export function init(lists) {
readers = {};
entityNamesSimple.forEach(n => readers[ n ] = new ListReader(createCallback(n)));
entityNamesWithRed.forEach(n => readers[ n ] = new ListReaderWithRed(createCallback(n)));
entityNames.forEach(n => readers[ n ].list = lists[ n ] || []);
}
export function append(name, item) {
readers[ name ].append(item);
}
export function setStartTime(time) {
readers.resource.startTime = time;
}
const byTimeNames = [ "event", "stack" ]; // TEMP
const byIndexNames = entityNames.filter(n => !byTimeNames.includes(n));
export function goTo(time, index) {
if (readers === null) return;
if (typeof index === 'number') {
byTimeNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goTo(time)));
byIndexNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goToIndex(index)));
} else {
entityNames.forEach(n => readers[ n ] && readers[ n ]._callback(readers[ n ].goTo(time)));
}
}
export function clean() {
entityNames.forEach(n => delete readers[ n ]);
}
export const INITIAL_STATE = is;

View file

@ -0,0 +1,172 @@
import type { Store, Mover, Interval } from './types';
import * as localStorage from './localStorage';
const fps = 60
const performance: { now: () => number } = window.performance || { now: Date.now.bind(Date) }
const requestAnimationFrame: typeof window.requestAnimationFrame =
window.requestAnimationFrame ||
// @ts-ignore
window.webkitRequestAnimationFrame ||
// @ts-ignore
window.mozRequestAnimationFrame ||
// @ts-ignore
window.oRequestAnimationFrame ||
// @ts-ignore
window.msRequestAnimationFrame ||
(callback => window.setTimeout(() => { callback(performance.now()) }, 1000 / fps))
const cancelAnimationFrame =
window.cancelAnimationFrame ||
// @ts-ignore
window.mozCancelAnimationFrame ||
window.clearTimeout
export interface SetState {
time: number
playing: boolean
completed: boolean
endTime: number
live: boolean
livePlay: boolean
}
export interface GetState extends SetState {
skip: boolean
speed: number
skipIntervals: Interval[]
lastMessageTime: number
ready: boolean
}
export const INITIAL_STATE: SetState = {
time: 0,
playing: false,
completed: false,
endTime: 0,
live: false,
livePlay: false,
} as const
export default class Animator {
private animationFrameRequestId: number = 0
constructor(private state: Store<GetState, SetState>, private mm: Mover) {}
private setTime(time: number) {
this.state.update({
time,
completed: false,
})
this.mm.move(time)
}
private startAnimation() {
let prevTime = this.state.get().time
let animationPrevTime = performance.now()
const frameHandler = (animationCurrentTime: number) => {
const {
speed,
skip,
skipIntervals,
endTime,
live,
livePlay,
ready, // = messagesLoading || cssLoading || disconnected
lastMessageTime, // should be updated
} = this.state.get()
const diffTime = !ready
? 0
: Math.max(animationCurrentTime - animationPrevTime, 0) * (live ? 1 : speed)
let time = prevTime + diffTime
const skipInterval = skip && skipIntervals.find(si => si.contains(time)) // TODO: good skip by messages
if (skipInterval) time = skipInterval.end
if (time < 0) { time = 0 } // ?
//const fmt = getFirstMessageTime();
//if (time < fmt) time = fmt; // ?
if (livePlay && time < lastMessageTime) { time = lastMessageTime }
if (endTime < lastMessageTime) {
this.state.update({
endTime: lastMessageTime,
})
}
prevTime = time
animationPrevTime = animationCurrentTime
const completed = !live && time >= endTime
if (completed) {
this.setTime(endTime)
return this.state.update({
playing: false,
completed: true,
})
}
if (live && time > endTime) {
this.state.update({
endTime: time,
})
}
this.setTime(time)
this.animationFrameRequestId = requestAnimationFrame(frameHandler)
}
this.animationFrameRequestId = requestAnimationFrame(frameHandler)
}
play() {
cancelAnimationFrame(this.animationFrameRequestId)
this.state.update({ playing: true })
this.startAnimation()
}
pause() {
cancelAnimationFrame(this.animationFrameRequestId)
this.state.update({ playing: false })
}
togglePlay() {
const { playing, completed } = this.state.get()
if (playing) {
this.pause()
} else if (completed) {
this.setTime(0)
this.play()
} else {
this.play()
}
}
// jump by index?
jump(time: number) {
const { live } = this.state.get()
if (live) return
if (this.state.get().playing) {
cancelAnimationFrame(this.animationFrameRequestId)
this.setTime(time)
this.startAnimation()
this.state.update({ livePlay: time === this.state.get().endTime })
} else {
this.setTime(time)
this.state.update({ livePlay: time === this.state.get().endTime })
}
}
// TODO: clearify logic of live time-travel
jumpToLive() {
cancelAnimationFrame(this.animationFrameRequestId)
this.setTime(this.state.get().endTime)
this.startAnimation()
this.state.update({ livePlay: true })
}
}

View file

@ -0,0 +1,125 @@
import * as typedLocalStorage from './localStorage';
import type { Mover, Cleaner, Store } from './types';
import Animator from './Animator';
import { INITIAL_STATE as ANIMATOR_INITIAL_STATE } from './Animator';
import type { GetState as AnimatorGetState, SetState as AnimatorSetState } from './Animator';
/* == separate this == */
const HIGHEST_SPEED = 16
const SPEED_STORAGE_KEY = "__$player-speed$__"
const SKIP_STORAGE_KEY = "__$player-skip$__"
const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__"
const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__"
const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__"
const storedSpeed: number = typedLocalStorage.number(SPEED_STORAGE_KEY)
const initialSpeed = [1, 2, 4, 8, 16].includes(storedSpeed) ? storedSpeed : 1
const initialSkip = typedLocalStorage.boolean(SKIP_STORAGE_KEY)
const initialSkipToIssue = typedLocalStorage.boolean(SKIP_TO_ISSUE_STORAGE_KEY)
const initialAutoplay = typedLocalStorage.boolean(AUTOPLAY_STORAGE_KEY)
const initialShowEvents = typedLocalStorage.boolean(SHOW_EVENTS_STORAGE_KEY)
export const INITIAL_STATE = {
...ANIMATOR_INITIAL_STATE,
skipToIssue: initialSkipToIssue,
showEvents: initialShowEvents,
autoplay: initialAutoplay,
skip: initialSkip,
speed: initialSpeed,
}
export type State = typeof INITIAL_STATE & AnimatorGetState
/* == */
export default class Player extends Animator {
constructor(private pState: Store<State>, private manager: Mover & Cleaner) {
super(pState, manager)
// Autoplay
if (pState.get().autoplay) {
let autoPlay = true;
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
const { playing } = pState.get();
autoPlay = playing
if (playing) {
this.pause();
}
} else if (autoPlay) {
this.play();
}
})
if (!document.hidden) {
this.play();
}
}
}
/* === TODO: incapsulate in LSCache === */
toggleAutoplay() {
const autoplay = !this.pState.get().autoplay
localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`);
this.pState.update({ autoplay })
}
// Shouldn't it be in the react part as a fully (with localStorage-cache react hook)?
toggleEvents() {
const showEvents = !this.pState.get().showEvents
localStorage.setItem(SHOW_EVENTS_STORAGE_KEY, `${showEvents}`);
this.pState.update({ showEvents })
}
// move to React part?
toggleSkipToIssue() {
const skipToIssue = !this.pState.get().skipToIssue
localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`);
this.pState.update({ skipToIssue })
}
toggleSkip() {
const skip = !this.pState.get().skip
localStorage.setItem(SKIP_STORAGE_KEY, `${skip}`);
this.pState.update({ skip })
}
private updateSpeed(speed: number) {
localStorage.setItem(SPEED_STORAGE_KEY, `${speed}`);
this.pState.update({ speed })
}
toggleSpeed() {
const { speed } = this.pState.get()
this.updateSpeed(speed < HIGHEST_SPEED ? speed * 2 : 1)
}
speedUp() {
const { speed } = this.pState.get()
this.updateSpeed(Math.min(HIGHEST_SPEED, speed * 2))
}
speedDown() {
const { speed } = this.pState.get()
this.updateSpeed(Math.max(1, speed / 2))
}
/* === === */
// TODO: move theese to React hooks
// injectNotes(notes: Note[]) {
// update({ notes })
// }
// filterOutNote(noteId: number) {
// const { notes } = getState()
// update({ notes: notes.filter((note: Note) => note.noteId !== noteId) })
// }
clean() {
this.pause()
this.manager.clean()
}
}

View file

@ -0,0 +1,63 @@
import * as lstore from './localStorage'
import type { SimpleState } from './PlayerState'
const SPEED_STORAGE_KEY = "__$player-speed$__";
const SKIP_STORAGE_KEY = "__$player-skip$__";
const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__";
const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__";
const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__";
const storedSpeed = lstore.number(SPEED_STORAGE_KEY, 1)
const initialSpeed = [1,2,4,8,16].includes(storedSpeed) ? storedSpeed : 1;
const initialSkip = lstore.boolean(SKIP_STORAGE_KEY)
const initialSkipToIssue = lstore.boolean(SKIP_TO_ISSUE_STORAGE_KEY)
const initialAutoplay = lstore.boolean(AUTOPLAY_STORAGE_KEY)
const initialShowEvents = lstore.boolean(SHOW_EVENTS_STORAGE_KEY)
export const INITIAL_STATE = {
skipToIssue: initialSkipToIssue,
autoplay: initialAutoplay,
showEvents: initialShowEvents,
skip: initialSkip,
speed: initialSpeed,
}
const KEY_MAP = {
speed: SPEED_STORAGE_KEY,
skip: SKIP_STORAGE_KEY,
skipToIssue: SKIP_TO_ISSUE_STORAGE_KEY,
autoplay: AUTOPLAY_STORAGE_KEY,
showEvents: SHOW_EVENTS_STORAGE_KEY,
}
type KeysOfBoolean<T> = keyof T & keyof { [ K in keyof T as T[K] extends boolean ? K : never ] : K };
type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
export default class LSCache<G extends Record<string, boolean | number | string> {
constructor(private state: SimpleState<G>, private keyMap: Record<keyof Partial<G>, string>) {
}
update(newState: Partial<G>) {
for (let [k, v] of Object.entries(newState) as Entries<Partial<G>>) {
if (k in this.keyMap) {
// @ts-ignore TODO: nice typing
//lstore[typeof v](this.keyMap[k], v)
localStorage.setItem(this.keyMap[k], String(v))
}
}
this.state.update(newState)
}
toggle(key: KeysOfBoolean<G>) {
// @ts-ignore TODO: nice typing
this.update({
[key]: !this.get()[key]
})
}
get() {
return this.state.get()
}
}

View file

@ -0,0 +1,19 @@
export function number(key: string, dflt = 0): number {
const stVal = localStorage.getItem(key)
if (stVal === null) {
return dflt
}
const val = parseInt(stVal)
if (isNaN(val)) {
return dflt
}
return val
}
export function boolean(key: string, dflt = false): boolean {
return localStorage.getItem(key) === "true"
}
export function string(key: string, dflt = ''): string {
return localStorage.getItem(key) || ''
}

View file

@ -0,0 +1,21 @@
export interface Mover {
move(time: number): void
}
export interface Cleaner {
clean(): void
}
export interface Interval {
contains(t: number): boolean
start: number
end: number
}
export interface Store<G extends Object, S extends Object = G> {
get(): G
update(state: Partial<S>): void
}