fix(ui): refactor state types, prep to integrate lscache, fix session types

This commit is contained in:
nick-delirium 2024-01-16 13:04:12 +01:00
parent 0dea805366
commit cf9cad7f75
12 changed files with 213 additions and 181 deletions

View file

@ -458,7 +458,6 @@ function Performance({
const availableCount = [fps, cpu, heap, nodes].reduce((c, av) => (av ? c + 1 : c), 0);
const height = availableCount === 0 ? '0' : `${100 / availableCount}%`;
console.log(_data)
return (
<BottomBlock>
<BottomBlock.Header>

View file

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

View file

@ -1,3 +1,5 @@
import { Message } from "Player/web/messages";
export interface Timed {
time: number
/** present in mobile events and in db events */
@ -33,7 +35,10 @@ export interface SessionFilesInfo {
sessionId: string
isMobile: boolean
agentToken?: string
duration: number
duration: {
milliseconds: number
valueOf: () => number
}
domURL: string[]
devtoolsURL: string[]
/** deprecated */
@ -45,3 +50,5 @@ export interface SessionFilesInfo {
errors: Record<string, any>[]
agentInfo?: { email: string, name: string }
}
export type PlayerMsg = Message & { tabId: string }

View file

@ -1,63 +1,64 @@
import * as lstore from './localStorage'
import * as lstore from './localStorage';
import SimpleStore from 'App/player/common/SimpleStore';
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 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 = [0.5, 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);
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 = {
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,
}
skip: SKIP_STORAGE_KEY,
speed: SPEED_STORAGE_KEY,
} as const
type KeysOfBoolean<T> = keyof T & keyof { [ K in keyof T as T[K] extends boolean ? K : never ] : K };
const keys = Object.keys(KEY_MAP) as (keyof typeof KEY_MAP)[];
const booleanKeys = ['skipToIssue', 'autoplay', 'showEvents', 'skip'] as const;
type LSCState = typeof INITIAL_STATE
type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
export default class LSCache {
static readonly INITIAL_STATE = INITIAL_STATE;
private readonly state: SimpleStore<typeof LSCache.INITIAL_STATE>;
export default class LSCache<G extends Record<string, boolean | number | string>> {
constructor(private state: SimpleState<G>, private keyMap: Record<keyof Partial<G>, string>) {
constructor() {
this.state = new SimpleStore<typeof LSCache.INITIAL_STATE>(LSCache.INITIAL_STATE);
}
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))
update(newState: Partial<LSCState>) {
for (let [k, v] of Object.entries(newState) as [keyof LSCState, LSCState[keyof LSCState]][]) {
if (k in keys) {
localStorage.setItem(KEY_MAP[k], String(v));
}
}
this.state.update(newState)
this.state.update(newState);
}
toggle(key: KeysOfBoolean<G>) {
toggle(key: typeof booleanKeys[number]) {
// @ts-ignore TODO: nice typing
this.update({
[key]: !this.get()[key]
})
[key]: !this.get()[key],
});
}
get() {
return this.state.get()
return this.state.get();
}
}

View file

@ -1,8 +1,7 @@
import type { Store, SessionFilesInfo } from 'Player';
import type { Store, SessionFilesInfo, PlayerMsg } from "Player";
import { decryptSessionBytes } from './network/crypto';
import MFileReader from './messages/MFileReader';
import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles';
import type { Message } from './messages';
import logger from 'App/logger';
import unpack from 'Player/common/unpack';
import MessageManager from 'Player/web/MessageManager';
@ -33,21 +32,21 @@ export default class MessageLoader {
createNewParser(
shouldDecrypt = true,
onMessagesDone: (msgs: PlayerMsg[], file?: string) => void,
file?: string
) {
const decrypt =
shouldDecrypt && this.session.fileKey
? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey!)
: (b: Uint8Array) => Promise.resolve(b);
// Each time called - new fileReader created
const fileReader = new MFileReader(new Uint8Array(), this.session.startedAt);
return (b: Uint8Array) => {
return decrypt(b)
.then((b) => {
const data = unpack(b);
return async (b: Uint8Array) => {
try {
const mobBytes = await decrypt(b);
const data = unpack(mobBytes);
fileReader.append(data);
fileReader.checkForIndexes();
const msgs: Array<Message & { tabId: string }> = [];
const msgs: Array<PlayerMsg> = [];
let finished = false;
while (!finished) {
const msg = fileReader.readNext();
@ -59,29 +58,32 @@ export default class MessageLoader {
}
}
const sortedMessages = msgs.sort((m1, m2) => {
const sortedMsgs = msgs.sort((m1, m2) => {
return m1.time - m2.time;
});
sortedMessages.forEach((msg) => {
this.messageManager.distributeMessage(msg);
});
logger.info('Messages count: ', msgs.length, sortedMessages, file);
this.messageManager.sortDomRemoveMessages(sortedMessages);
this.messageManager.setMessagesLoading(false);
})
.catch((e) => {
onMessagesDone(sortedMsgs, file);
} catch (e) {
console.error(e);
this.uiErrorHandler?.error('Error parsing file: ' + e.message);
});
}
};
}
loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
processMessages = (msgs: PlayerMsg[], file?: string) => {
msgs.forEach((msg) => {
this.messageManager.distributeMessage(msg);
});
logger.info('Messages count: ', msgs.length, msgs, file);
this.messageManager.sortDomRemoveMessages(msgs);
this.messageManager.setMessagesLoading(false);
};
async loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
if (urls.length > 0) {
this.store.update({ domLoading: true });
return loadFiles(urls, parser, true).then(() => this.store.update({ domLoading: false }));
await loadFiles(urls, parser, true);
return this.store.update({ domLoading: false });
} else {
return Promise.resolve();
}
@ -111,11 +113,17 @@ export default class MessageLoader {
const loadMethod =
this.session.domURL && this.session.domURL.length > 0
? { mobUrls: this.session.domURL, parser: () => this.createNewParser(true, 'dom') }
: { mobUrls: this.session.mobsUrl, parser: () => this.createNewParser(false, 'dom') };
? {
mobUrls: this.session.domURL,
parser: () => this.createNewParser(true, this.processMessages, 'dom'),
}
: {
mobUrls: this.session.mobsUrl,
parser: () => this.createNewParser(false, this.processMessages, 'dom'),
};
const parser = loadMethod.parser();
const devtoolsParser = this.createNewParser(true, 'devtools');
const devtoolsParser = this.createNewParser(true, this.processMessages, 'devtools');
/**
* to speed up time to replay
* we load first dom mob file before the rest
@ -140,8 +148,8 @@ export default class MessageLoader {
efsDomFilePromise,
efsDevtoolsFilePromise,
]);
const domParser = this.createNewParser(false, 'domEFS');
const devtoolsParser = this.createNewParser(false, 'devtoolsEFS');
const domParser = this.createNewParser(false, this.processMessages, 'domEFS');
const devtoolsParser = this.createNewParser(false, this.processMessages, 'devtoolsEFS');
const parseDomPromise: Promise<any> =
domData.status === 'fulfilled'
? domParser(domData.value)

View file

@ -2,7 +2,7 @@
import { Decoder } from 'syncod';
import logger from 'App/logger';
import type { Store, ILog } from 'Player';
import type { Store, ILog, SessionFilesInfo } from 'Player';
import ListWalker from '../common/ListWalker';
import MouseMoveManager from './managers/MouseMoveManager';
@ -108,7 +108,7 @@ export default class MessageManager {
private activeTab = '';
constructor(
private readonly session: Record<string, any>,
private readonly session: SessionFilesInfo,
private readonly state: Store<State & { time: number }>,
private readonly screen: Screen,
private readonly initialLists?: Partial<InitialLists>,

View file

@ -125,6 +125,9 @@ export default class TabSessionManager {
});
}
/**
* Because we use main state (from messageManager), we have to update it this way
* */
updateLocalState(state: Partial<TabState>) {
this.state.update({
tabStates: {
@ -283,22 +286,23 @@ export default class TabSessionManager {
// TODO: page-wise resources list // setListsStartTime(lastLoadedLocationMsg.time)
this.navigationStartOffset = lastLoadedLocationMsg.navigationStart - this.sessionStart;
}
const llEvent = this.locationEventManager.moveGetLast(t, index);
if (!!llEvent) {
if (llEvent.domContentLoadedTime != null) {
const lastLocationEvent = this.locationEventManager.moveGetLast(t, index);
if (!!lastLocationEvent) {
if (lastLocationEvent.domContentLoadedTime != null) {
stateToUpdate.domContentLoadedTime = {
time: llEvent.domContentLoadedTime + this.navigationStartOffset, //TODO: predefined list of load event for the network tab (merge events & SetPageLocation: add navigationStart to db)
value: llEvent.domContentLoadedTime,
time: lastLocationEvent.domContentLoadedTime + this.navigationStartOffset,
// TODO: predefined list of load event for the network tab (merge events & SetPageLocation: add navigationStart to db)
value: lastLocationEvent.domContentLoadedTime,
};
}
if (llEvent.loadTime != null) {
if (lastLocationEvent.loadTime != null) {
stateToUpdate.loadTime = {
time: llEvent.loadTime + this.navigationStartOffset,
value: llEvent.loadTime,
time: lastLocationEvent.loadTime + this.navigationStartOffset,
value: lastLocationEvent.loadTime,
};
}
if (llEvent.domBuildingTime != null) {
stateToUpdate.domBuildingTime = llEvent.domBuildingTime;
if (lastLocationEvent.domBuildingTime != null) {
stateToUpdate.domBuildingTime = lastLocationEvent.domBuildingTime;
}
}
/* === */
@ -307,12 +311,7 @@ export default class TabSessionManager {
// @ts-ignore comes from parent state
this.state.update({ location: lastLocationMsg.url });
}
// ConnectionInformation message is not used at this moment
// const lastConnectionInfoMsg = this.connectionInfoManger.moveGetLast(t, index);
// if (!!lastConnectionInfoMsg) {
// stateToUpdate.connType = lastConnectionInfoMsg.type;
// stateToUpdate.connBandwidth = lastConnectionInfoMsg.downlink;
// }
const lastPerformanceTrackMessage = this.performanceTrackManager.moveGetLast(t, index);
if (!!lastPerformanceTrackMessage) {
stateToUpdate.performanceChartTime = lastPerformanceTrackMessage.time;
@ -347,6 +346,9 @@ export default class TabSessionManager {
});
}
/**
* Used to decode state messages, because they can be large we only want to decode whats rendered atm
* */
public decodeMessage(msg: Message) {
return this.decoder.decode(msg);
}

View file

@ -1,23 +1,19 @@
import type { Store, SessionFilesInfo } from 'Player'
import type { Message } from './messages'
import WebPlayer from './WebPlayer'
import AssistManager from './assist/AssistManager'
import MFileReader from './messages/MFileReader'
import { requestEFSDom } from './network/loadFiles'
import type { Store, SessionFilesInfo, PlayerMsg } from 'Player';
import WebPlayer from './WebPlayer';
import AssistManager from './assist/AssistManager';
import { requestEFSDom } from './network/loadFiles';
export default class WebLivePlayer extends WebPlayer {
static readonly INITIAL_STATE = {
...WebPlayer.INITIAL_STATE,
...AssistManager.INITIAL_STATE,
liveTimeTravel: false,
}
};
assistManager: AssistManager // public so far
private readonly incomingMessages: Message[] = []
private historyFileIsLoading = false
private lastMessageInFileTime = 0
assistManager: AssistManager; // public so far
private readonly incomingMessages: PlayerMsg[] = [];
private historyFileIsLoading = false;
private lastMessageInFileTime = 0;
constructor(
wpState: Store<typeof WebLivePlayer.INITIAL_STATE>,
@ -25,79 +21,91 @@ export default class WebLivePlayer extends WebPlayer {
config: RTCIceServer[] | null,
agentId: number,
projectId: number,
uiErrorHandler?: { error: (msg: string) => void },
uiErrorHandler?: { error: (msg: string) => void }
) {
super(wpState, session, true, false, uiErrorHandler)
super(wpState, session, true, false, uiErrorHandler);
this.assistManager = new AssistManager(
session,
f => this.messageManager.setMessagesLoading(f),
(f) => this.messageManager.setMessagesLoading(f),
(msg) => {
this.incomingMessages.push(msg)
this.incomingMessages.push(msg);
if (!this.historyFileIsLoading) {
// TODO: fix index-ing after historyFile-load
this.messageManager.distributeMessage(msg)
this.messageManager.distributeMessage(msg);
}
},
this.screen,
config,
wpState,
(id) => this.messageManager.getNode(id),
uiErrorHandler,
)
this.assistManager.connect(session.agentToken!, agentId, projectId)
uiErrorHandler
);
this.assistManager.connect(session.agentToken!, agentId, projectId);
}
/**
* Loads in-progress dom file from EFS directly
* then reads it to add everything happened before "now" to message manager
* to be able to replay it like usual
* */
toggleTimetravel = async () => {
if (this.wpState.get().liveTimeTravel) {
return
if ((this.wpState.get() as typeof WebLivePlayer.INITIAL_STATE).liveTimeTravel) {
return;
}
let result = false;
this.historyFileIsLoading = true
this.messageManager.setMessagesLoading(true) // do it in one place. update unique loading states each time instead
this.messageManager.resetMessageManagers()
this.historyFileIsLoading = true;
this.messageManager.setMessagesLoading(true); // do it in one place. update unique loading states each time instead
this.messageManager.resetMessageManagers();
try {
const bytes = await requestEFSDom(this.session.sessionId)
const fileReader = new MFileReader(bytes, this.session.startedAt)
for (let msg = fileReader.readNext();msg !== null;msg = fileReader.readNext()) {
this.messageManager.distributeMessage(msg)
}
const bytes = await requestEFSDom(this.session.sessionId);
const reader = this.messageLoader.createNewParser(
false,
(msgs) => {
msgs.forEach((msg) => {
this.messageManager.distributeMessage(msg);
});
},
'cobrowse dom'
);
await reader(bytes);
this.wpState.update({
liveTimeTravel: true,
})
result = true
// here we need to update also lists state, if we gonna use them this.messageManager.onFileReadSuccess
} catch(e) {
this.uiErrorHandler?.error('Error requesting a session file')
console.error("EFS file download error:", e)
});
result = true;
// here we need to update also lists state, if we're going use them this.messageManager.onFileReadSuccess
} catch (e) {
this.uiErrorHandler?.error('Error requesting a session file');
console.error('EFS file download error:', e);
}
// Append previously received messages
this.incomingMessages
.filter(msg => msg.time >= this.lastMessageInFileTime)
.forEach((msg) => this.messageManager.distributeMessage(msg))
this.incomingMessages.length = 0
.filter((msg) => msg.time >= this.lastMessageInFileTime)
.forEach((msg) => this.messageManager.distributeMessage(msg));
this.incomingMessages.length = 0;
this.historyFileIsLoading = false
this.messageManager.setMessagesLoading(false)
this.historyFileIsLoading = false;
this.messageManager.setMessagesLoading(false);
return result;
}
};
jumpToLive = () => {
this.wpState.update({
live: true,
livePlay: true,
})
this.jump(this.wpState.get().lastMessageTime)
}
});
this.jump(this.wpState.get().lastMessageTime);
};
clean = () => {
this.incomingMessages.length = 0
this.assistManager.clean()
this.screen?.clean?.()
this.incomingMessages.length = 0;
this.assistManager.clean();
this.screen?.clean?.();
// @ts-ignore
this.screen = undefined;
super.clean()
}
super.clean();
};
}

View file

@ -16,7 +16,7 @@ export default class WebPlayer extends Player {
...TargetMarker.INITIAL_STATE,
...MessageManager.INITIAL_STATE,
...MessageLoader.INITIAL_STATE,
liveTimeTravel: false,
inspectorMode: false,
}

View file

@ -1,8 +1,7 @@
import MessageManager from 'Player/web/MessageManager';
import type { Socket } from 'socket.io-client';
import type Screen from '../Screen/Screen';
import type { Store } from '../../common/types';
import type { Message } from '../messages';
import type { PlayerMsg, Store } from 'App/player';
import MStreamReader from '../messages/MStreamReader';
import JSONRawMessageReader from '../messages/JSONRawMessageReader';
import Call, { CallingState } from './Call';
@ -71,7 +70,7 @@ export default class AssistManager {
constructor(
private session: any,
private setMessagesLoading: (flag: boolean) => void,
private handleMessage: (m: Message, index: number) => void,
private handleMessage: (m: PlayerMsg, index: number) => void,
private screen: Screen,
private config: RTCIceServer[] | null,
private store: Store<typeof AssistManager.INITIAL_STATE>,
@ -159,7 +158,7 @@ export default class AssistManager {
const reader = new MStreamReader(jmr, this.session.startedAt);
let waitingForMessages = true;
const now = +new Date();
const now = new Date().getTime();
this.store.update({ assistStart: now });
// @ts-ignore
@ -168,7 +167,8 @@ export default class AssistManager {
return;
}
// @ts-ignore
const urlObject = new URL(window.env.API_EDP || window.location.origin); // does it handle ssl automatically?
const urlObject = new URL(window.env.API_EDP || window.location.origin);
// does it handle ssl automatically?
const socket: Socket = (this.socket = io(urlObject.origin, {
withCredentials: true,
@ -192,7 +192,8 @@ export default class AssistManager {
}));
socket.on('connect', () => {
waitingForMessages = true;
this.setStatus(ConnectionStatus.WaitingMessages); // TODO: reconnect happens frequently on bad network
// TODO: reconnect happens frequently on bad network
this.setStatus(ConnectionStatus.WaitingMessages);
});
socket.on('messages', (messages) => {
@ -229,9 +230,12 @@ export default class AssistManager {
const { tabId } = meta;
const usedData = this.assistVersion === 1 ? evData : data;
const { active } = usedData;
const currentTab = this.store.get().currentTab;
this.clearDisconnectTimeout();
!this.inactiveTimeout && this.setStatus(ConnectionStatus.Connected);
if (typeof active === 'boolean') {
this.clearInactiveTimeout();
if (active) {
@ -299,6 +303,9 @@ export default class AssistManager {
});
}
/**
* Sends event ping to stats service
* */
public ping(event: StatsEvent, id: number) {
this.socket?.emit(event, id);
}

View file

@ -13,7 +13,7 @@ export default class MStreamReader {
private idx: number = 0
currentTab = 'back-compatability'
readNext(): Message & { _index: number } | null {
readNext(): Message & { _index: number, tabId: string } | null {
let msg = this.r.readMessage()
if (msg === null) { return null }
if (msg.tp === MType.Timestamp) {

View file

@ -166,7 +166,7 @@ export default class Session {
canvasURL: ISession['canvasURL'];
live: ISession['live'];
startedAt: ISession['startedAt'];
duration: ISession['duration'];
duration: Duration;
events: ISession['events'];
stackEvents: ISession['stackEvents'];
metadata: ISession['metadata'];