diff --git a/frontend/app/components/Session_/Performance/Performance.tsx b/frontend/app/components/Session_/Performance/Performance.tsx index 33cbda4f9..236d97422 100644 --- a/frontend/app/components/Session_/Performance/Performance.tsx +++ b/frontend/app/components/Session_/Performance/Performance.tsx @@ -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 ( diff --git a/frontend/app/player/common/SimpleStore.ts b/frontend/app/player/common/SimpleStore.ts index 973fa4cb8..82ddd3433 100644 --- a/frontend/app/player/common/SimpleStore.ts +++ b/frontend/app/player/common/SimpleStore.ts @@ -1,6 +1,6 @@ import { Store } from './types' -export default class SimpleSore implements Store { +export default class SimpleSore implements Store { constructor(private state: G){} get(): G { return this.state diff --git a/frontend/app/player/common/types.ts b/frontend/app/player/common/types.ts index 0289114da..060bb94ae 100644 --- a/frontend/app/player/common/types.ts +++ b/frontend/app/player/common/types.ts @@ -1,4 +1,6 @@ -export interface Timed { +import { Message } from "Player/web/messages"; + +export interface Timed { time: number /** present in mobile events and in db events */ timestamp?: number @@ -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 */ @@ -44,4 +49,6 @@ export interface SessionFilesInfo { frustrations: Record[] errors: Record[] agentInfo?: { email: string, name: string } -} \ No newline at end of file +} + +export type PlayerMsg = Message & { tabId: string } \ No newline at end of file diff --git a/frontend/app/player/player/_LSCache.ts b/frontend/app/player/player/_LSCache.ts index 70425bdda..38fcc4395 100644 --- a/frontend/app/player/player/_LSCache.ts +++ b/frontend/app/player/player/_LSCache.ts @@ -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, + 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 + +const keys = Object.keys(KEY_MAP) as (keyof typeof KEY_MAP)[]; +const booleanKeys = ['skipToIssue', 'autoplay', 'showEvents', 'skip'] as const; +type LSCState = typeof INITIAL_STATE + +export default class LSCache { + static readonly INITIAL_STATE = INITIAL_STATE; + private readonly state: SimpleStore; + + constructor() { + this.state = new SimpleStore(LSCache.INITIAL_STATE); + } + + update(newState: Partial) { + 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); + } + + toggle(key: typeof booleanKeys[number]) { + // @ts-ignore TODO: nice typing + this.update({ + [key]: !this.get()[key], + }); + } + + get() { + return this.state.get(); + } } - -type KeysOfBoolean = keyof T & keyof { [ K in keyof T as T[K] extends boolean ? K : never ] : K }; - -type Entries = { - [K in keyof T]: [K, T[K]]; -}[keyof T][]; - -export default class LSCache> { - constructor(private state: SimpleState, private keyMap: Record, string>) { - } - update(newState: Partial) { - for (let [k, v] of Object.entries(newState) as Entries>) { - 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) { - // @ts-ignore TODO: nice typing - this.update({ - [key]: !this.get()[key] - }) - } - get() { - return this.state.get() - } -} \ No newline at end of file diff --git a/frontend/app/player/web/MessageLoader.ts b/frontend/app/player/web/MessageLoader.ts index e6ee25130..97e58fb0c 100644 --- a/frontend/app/player/web/MessageLoader.ts +++ b/frontend/app/player/web/MessageLoader.ts @@ -1,8 +1,7 @@ -import type { Store, SessionFilesInfo } from 'Player'; -import { decryptSessionBytes } from './network/crypto'; +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,55 +32,58 @@ 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); - fileReader.append(data); - fileReader.checkForIndexes(); - const msgs: Array = []; - let finished = false; - while (!finished) { - const msg = fileReader.readNext(); - if (msg) { - msgs.push(msg); - } else { - finished = true; - break; - } + return async (b: Uint8Array) => { + try { + const mobBytes = await decrypt(b); + const data = unpack(mobBytes); + fileReader.append(data); + fileReader.checkForIndexes(); + const msgs: Array = []; + let finished = false; + while (!finished) { + const msg = fileReader.readNext(); + if (msg) { + msgs.push(msg); + } else { + finished = true; + break; } + } - const sortedMessages = 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) => { - console.error(e); - this.uiErrorHandler?.error('Error parsing file: ' + e.message); + const sortedMsgs = msgs.sort((m1, m2) => { + return m1.time - m2.time; }); + onMessagesDone(sortedMsgs, file); + } catch (e) { + console.error(e); + this.uiErrorHandler?.error('Error parsing file: ' + e.message); + } }; } - loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise) { + 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) { 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 = domData.status === 'fulfilled' ? domParser(domData.value) diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index ff9d3e4c3..a4473a360 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -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, + private readonly session: SessionFilesInfo, private readonly state: Store, private readonly screen: Screen, private readonly initialLists?: Partial, diff --git a/frontend/app/player/web/TabManager.ts b/frontend/app/player/web/TabManager.ts index f5e39aeda..62b08301d 100644 --- a/frontend/app/player/web/TabManager.ts +++ b/frontend/app/player/web/TabManager.ts @@ -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) { 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); } diff --git a/frontend/app/player/web/WebLivePlayer.ts b/frontend/app/player/web/WebLivePlayer.ts index a163eb83e..6b5e1782d 100644 --- a/frontend/app/player/web/WebLivePlayer.ts +++ b/frontend/app/player/web/WebLivePlayer.ts @@ -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, @@ -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() - } -} \ No newline at end of file + super.clean(); + }; +} diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index fb1c604b6..23b36ed7c 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -16,7 +16,7 @@ export default class WebPlayer extends Player { ...TargetMarker.INITIAL_STATE, ...MessageManager.INITIAL_STATE, ...MessageLoader.INITIAL_STATE, - + liveTimeTravel: false, inspectorMode: false, } diff --git a/frontend/app/player/web/assist/AssistManager.ts b/frontend/app/player/web/assist/AssistManager.ts index 5fccb2e3d..69ca39383 100644 --- a/frontend/app/player/web/assist/AssistManager.ts +++ b/frontend/app/player/web/assist/AssistManager.ts @@ -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, @@ -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) { @@ -291,14 +295,17 @@ export default class AssistManager { this.getAssistVersion ); this.canvasReceiver = new CanvasReceiver(this.peerID, this.config, this.getNode, { - ...this.session.agentInfo, - id: agentId, - }); + ...this.session.agentInfo, + id: agentId, + }); document.addEventListener('visibilitychange', this.onVisChange); }); } + /** + * Sends event ping to stats service + * */ public ping(event: StatsEvent, id: number) { this.socket?.emit(event, id); } diff --git a/frontend/app/player/web/messages/MStreamReader.ts b/frontend/app/player/web/messages/MStreamReader.ts index 1954c53e5..2b2a490bf 100644 --- a/frontend/app/player/web/messages/MStreamReader.ts +++ b/frontend/app/player/web/messages/MStreamReader.ts @@ -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) { diff --git a/frontend/app/types/session/session.ts b/frontend/app/types/session/session.ts index accf5035a..f75b71587 100644 --- a/frontend/app/types/session/session.ts +++ b/frontend/app/types/session/session.ts @@ -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'];