This commit is contained in:
Alex Kaminskii 2023-04-21 18:08:07 +02:00
parent 607047f022
commit ecf4e0e8a2
15 changed files with 469 additions and 200 deletions

View file

@ -24,19 +24,17 @@ function Overlay({
const { store } = React.useContext<ILivePlayerContext>(PlayerContext)
const {
messagesLoading,
cssLoading,
ready,
peerConnectionStatus,
livePlay,
calling,
remoteControl,
recordingState,
} = store.get()
const loading = messagesLoading || cssLoading
const liveStatusText = getStatusText(peerConnectionStatus)
const connectionStatus = peerConnectionStatus
const showLiveStatusText = livePlay && liveStatusText && !loading;
const showLiveStatusText = livePlay && liveStatusText && ready;
const showRequestWindow =
(calling === CallingState.Connecting ||
@ -66,7 +64,7 @@ function Overlay({
connectionStatus={closedLive ? ConnectionStatus.Closed : connectionStatus}
/>
)}
{loading ? <Loader /> : null}
{!ready ? <Loader /> : null}
</>
);
}

View file

@ -66,8 +66,7 @@ function Controls(props: any) {
completed,
skip,
speed,
cssLoading,
messagesLoading,
ready,
inspectorMode,
markedTargets,
exceptionsList,
@ -88,7 +87,7 @@ function Controls(props: any) {
} = props;
const storageType = selectStorageType(store.get());
const disabled = disabledRedux || cssLoading || messagesLoading || inspectorMode || markedTargets;
const disabled = !ready || disabledRedux || inspectorMode || markedTargets;
const profilesCount = profilesList.length;
const graphqlCount = graphqlList.length;
const showGraphql = graphqlCount > 0;
@ -326,7 +325,6 @@ export default connect(
// nextProps.showFetch !== props.showFetch ||
// nextProps.fetchCount !== props.fetchCount ||
// nextProps.graphqlCount !== props.graphqlCount ||
// nextProps.liveTimeTravel !== props.liveTimeTravel ||
// nextProps.skipInterval !== props.skipInterval
// )
// return true;

View file

@ -21,23 +21,21 @@ function Overlay({
const togglePlay = () => player.togglePlay()
const {
playing,
messagesLoading,
cssLoading,
ready,
completed,
autoplay,
inspectorMode,
markedTargets,
activeTargetIndex,
} = store.get()
const loading = messagesLoading || cssLoading
const showAutoplayTimer = completed && autoplay && nextId
const showPlayIconLayer = !isClickmap && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer;
const showPlayIconLayer = !isClickmap && !markedTargets && !inspectorMode && ready && !showAutoplayTimer;
return (
<>
{showAutoplayTimer && <AutoplayTimer />}
{loading ? <Loader /> : null}
{!ready ? <Loader /> : null}
{showPlayIconLayer && <PlayIconLayer playing={playing} togglePlay={togglePlay} />}
{markedTargets && <ElementsMarker targets={markedTargets} activeIndex={activeTargetIndex} />}
</>

View file

@ -0,0 +1,23 @@
import { Store } from './types'
export default class StoreSubscriber<G, S=G> implements Store<G, S> {
constructor(private store: Store<G, S>) {}
get() { return this.store.get() }
update(newState: Partial<S>) {
this.store.update(newState)
this.subscriptions.forEach(sb => sb())
}
private subscriptions: Function[] = []
subscribe<T>(selector: (g: G) => T, cb: (val: T) => void) {
let prevVal = selector(this.get())
const checkSubscription = () => {
const newVal = selector(this.get())
if (newVal !== prevVal) {
prevVal = newVal
cb(newVal)
}
}
this.subscriptions.push(checkSubscription)
}
}

View file

@ -80,7 +80,7 @@ export default class Animator {
endTime,
live,
livePlay,
ready, // = messagesLoading || cssLoading || disconnected
ready,
lastMessageTime,
} = this.store.get()

View file

@ -25,10 +25,10 @@ export type State = typeof Player.INITIAL_STATE
export default class Player extends Animator {
static INITIAL_STATE = {
...Animator.INITIAL_STATE,
skipToIssue: initialSkipToIssue,
showEvents: initialShowEvents,
showEvents: initialShowEvents,
autoplay: initialAutoplay,
skip: initialSkip,
speed: initialSpeed,
} as const
@ -36,29 +36,27 @@ export default class Player extends Animator {
constructor(private pState: Store<State & AnimatorGetState>, private manager: Moveable) {
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();
// Autostart
let autostart = true // TODO: configurable
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
const { playing } = pState.get();
autostart = playing
if (playing) {
this.pause();
}
})
if (!document.hidden) {
} else if (autostart) {
this.play();
}
})
if (!document.hidden) {
this.play();
}
}
/* === TODO: incapsulate in LSCache === */
//TODO: move to react part ("autoplay" responsible for auto-playing-next)
toggleAutoplay() {
const autoplay = !this.pState.get().autoplay
localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`);
@ -72,13 +70,6 @@ export default class Player extends Animator {
this.pState.update({ showEvents })
}
// TODO: 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}`);

View file

@ -0,0 +1,92 @@
import logger from 'App/logger';
import { decryptSessionBytes } from './network/crypto';
import MFileReader from './messages/MFileReader';
import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles';
import type {
Message,
} from './messages';
import type { Store } from '../common/types';
interface SessionFilesInfo {
startedAt: number
sessionId: string
domURL: string[]
devtoolsURL: string[]
mobsUrl: string[] // back-compatibility. TODO: Remove in the 1.11.0
fileKey: string | null
}
type State = typeof MessageLoader.INITIAL_STATE
export default class MessageLoader {
static INITIAL_STATE = {
firstFileLoading: false,
domLoading: false,
devtoolsLoading: false,
}
private lastMessageInFileTime: number = 0;
constructor(
private readonly session: SessionFilesInfo,
private store: Store<State>,
private distributeMessage: (msg: Message, index: number) => void,
) {}
private createNewParser(shouldDecrypt=true) {
const fKey = this.session.fileKey
const decrypt = shouldDecrypt && fKey
? (b: Uint8Array) => decryptSessionBytes(b, fKey)
: (b: Uint8Array) => Promise.resolve(b)
// Each time called - new fileReader created. TODO: reuseable decryptor instance
const fileReader = new MFileReader(new Uint8Array(), this.session.startedAt)
return (b: Uint8Array) => decrypt(b).then(b => {
fileReader.append(b)
const msgs: Array<Message> = []
for (let msg = fileReader.readNext();msg !== null;msg = fileReader.readNext()) {
this.distributeMessage(msg, msg._index)
msgs.push(msg)
}
logger.info("Messages loaded: ", msgs.length, msgs)
//this._sortMessagesHack(fileReader) // TODO
this.store.update({ firstFileLoading: false }) // How to do it more explicit: on the first file loading?
})
}
requestFallbackDOM = () =>
requestEFSDom(this.session.sessionId)
.then(this.createNewParser(false))
loadDOM() {
this.store.update({
domLoading: true,
firstFileLoading: true,
})
const loadMethod = this.session.domURL && this.session.domURL.length > 0
? { url: this.session.domURL, needsDecryption: true }
: { url: this.session.mobsUrl, needsDecryption: false }
return loadFiles(loadMethod.url, this.createNewParser(loadMethod.needsDecryption))
// EFS fallback
.catch((e) => this.requestFallbackDOM())
.finally(() => this.store.update({ domLoading: false }))
}
loadDevtools() {
this.store.update({ devtoolsLoading: true })
return loadFiles(this.session.devtoolsURL, this.createNewParser())
// EFS fallback
.catch(() =>
requestEFSDevtools(this.session.sessionId)
.then(this.createNewParser(false))
)
.finally(() => this.store.update({ devtoolsLoading: false }))
}
}

View file

@ -6,8 +6,6 @@ import { TYPES as EVENT_TYPES } from 'Types/session/event';
import { Log } from './types/log';
import { Resource, ResourceType, getResourceFromResourceTiming, getResourceFromNetworkRequest } from './types/resource'
import { toast } from 'react-toastify';
import type { Store, Timed } from '../common/types';
import ListWalker from '../common/ListWalker';
@ -36,7 +34,6 @@ import { decryptSessionBytes } from './network/crypto';
import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE, State as ListsState } from './Lists';
import Screen, {
INITIAL_STATE as SCREEN_INITIAL_STATE,
State as ScreenState,
} from './Screen/Screen';
@ -57,13 +54,9 @@ export interface State extends ScreenState, ListsState {
domContentLoadedTime?: { time: number, value: number },
domBuildingTime?: number,
loadTime?: { time: number, value: number },
error: boolean,
devtoolsLoading: boolean,
messagesLoading: boolean,
cssLoading: boolean,
ready: boolean,
lastMessageTime: number,
}
@ -80,16 +73,12 @@ const visualChanges = [
export default class MessageManager {
static INITIAL_STATE: State = {
...SCREEN_INITIAL_STATE,
...Screen.INITIAL_STATE,
...LISTS_INITIAL_STATE,
performanceChartData: [],
skipIntervals: [],
error: false,
devtoolsLoading: false,
messagesLoading: false,
cssLoading: false,
ready: false,
lastMessageTime: 0,
}
@ -137,117 +126,6 @@ export default class MessageManager {
this.activityManager = new ActivityManager(this.session.duration.milliseconds) // only if not-live
}
private setCSSLoading = (cssLoading: boolean) => {
this.screen.displayFrame(!cssLoading)
this.state.update({ cssLoading, ready: !this.state.get().messagesLoading && !cssLoading })
}
private _sortMessagesHack(msgs: Message[]) {
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
this.pagesManager.sortPages((m1, m2) => {
if (m1.time === m2.time) {
if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
if (headChildrenIds.includes(m1.id)) {
return -1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
if (headChildrenIds.includes(m2.id)) {
return 1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
const m1FromHead = headChildrenIds.includes(m1.id);
const m2FromHead = headChildrenIds.includes(m2.id);
if (m1FromHead && !m2FromHead) {
return -1;
} else if (m2FromHead && !m1FromHead) {
return 1;
}
}
}
return 0;
})
}
private waitingForFiles: boolean = false
private onFileReadSuccess = () => {
const stateToUpdate : Partial<State>= {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvaliability: this.performanceTrackManager.avaliability,
...this.lists.getFullListsState(),
}
if (this.activityManager) {
this.activityManager.end()
stateToUpdate.skipIntervals = this.activityManager.list
}
this.state.update(stateToUpdate)
}
private onFileReadFailed = (e: any) => {
logger.error(e)
this.state.update({ error: true })
toast.error('Error requesting a session file')
}
private onFileReadFinally = () => {
this.waitingForFiles = false
// this.setMessagesLoading(false)
// this.state.update({ filesLoaded: true })
}
async loadMessages(isClickmap: boolean = false) {
this.setMessagesLoading(true)
// TODO: reusable decryptor instance
const createNewParser = (shouldDecrypt = true) => {
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.sessionStart)
return (b: Uint8Array) => decrypt(b).then(b => {
fileReader.append(b)
const msgs: Array<Message> = []
for (let msg = fileReader.readNext();msg !== null;msg = fileReader.readNext()) {
this.distributeMessage(msg, msg._index)
msgs.push(msg)
}
logger.info("Messages count: ", msgs.length, msgs)
this._sortMessagesHack(msgs)
this.setMessagesLoading(false)
})
}
this.waitingForFiles = true
const loadMethod = this.session.domURL && this.session.domURL.length > 0
? { url: this.session.domURL, parser: createNewParser }
: { url: this.session.mobsUrl, parser: () => createNewParser(false)}
loadFiles(loadMethod.url, loadMethod.parser())
// EFS fallback
.catch((e) =>
requestEFSDom(this.session.sessionId)
.then(createNewParser(false))
)
.then(this.onFileReadSuccess)
.catch(this.onFileReadFailed)
.finally(this.onFileReadFinally);
// load devtools (TODO: start after the first DOM file download)
if (isClickmap) return;
this.state.update({ devtoolsLoading: true })
loadFiles(this.session.devtoolsURL, createNewParser())
// EFS fallback
.catch(() =>
requestEFSDevtools(this.session.sessionId)
.then(createNewParser(false))
)
.then(() => {
this.state.update(this.lists.getFullListsState()) // TODO: also in case of dynamic update through assist
})
.catch(e => logger.error("Can not download the devtools file", e))
.finally(() => this.state.update({ devtoolsLoading: false }))
}
resetMessageManagers() {
this.locationEventManager = new ListWalker();
this.locationManager = new ListWalker();
@ -327,10 +205,6 @@ export default class MessageManager {
this.screen.cursor.click();
}
})
if (this.waitingForFiles && this.lastMessageTime <= t && t !== this.session.duration.milliseconds) {
this.setMessagesLoading(true)
}
}
private decodeStateMessage(msg: any, keys: Array<string>) {
@ -347,7 +221,8 @@ export default class MessageManager {
return { ...msg, ...decoded };
}
distributeMessage(msg: Message, index: number): void {
distributeMessage = (msg: Message, index: number): void => {
this._handeleForSortHack(msg)
const lastMessageTime = Math.max(msg.time, this.lastMessageTime)
this.lastMessageTime = lastMessageTime
this.state.update({ lastMessageTime })
@ -471,9 +346,54 @@ export default class MessageManager {
}
}
setMessagesLoading(messagesLoading: boolean) {
this.screen.display(!messagesLoading);
this.state.update({ messagesLoading, ready: !messagesLoading && !this.state.get().cssLoading });
private _heeadChildrenID: number[] = []
private _handeleForSortHack(m: Message) {
// @ts-ignore
m.parentID === 1 && this._heeadChildrenID.push(m.id)
}
// Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
private _sortMessagesHack() {
const headChildrenIds = this._heeadChildrenID
this.pagesManager.sortPages((m1, m2) => {
if (m1.time === m2.time) {
if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
if (headChildrenIds.includes(m1.id)) {
return -1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
if (headChildrenIds.includes(m2.id)) {
return 1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
const m1FromHead = headChildrenIds.includes(m1.id);
const m2FromHead = headChildrenIds.includes(m2.id);
if (m1FromHead && !m2FromHead) {
return -1;
} else if (m2FromHead && !m1FromHead) {
return 1;
}
}
}
return 0;
})
}
onMessagesLoaded = () => {
const stateToUpdate : Partial<State>= {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvaliability: this.performanceTrackManager.avaliability,
...this.lists.getFullListsState(),
}
if (this.activityManager) {
this.activityManager.end()
stateToUpdate.skipIntervals = this.activityManager.list
}
this.state.update(stateToUpdate)
this._sortMessagesHack()
}
private setCSSLoading = (cssLoading: boolean) => {
this.state.update({ cssLoading })
}
private setSize({ height, width }: { height: number, width: number }) {

View file

@ -6,19 +6,12 @@ import type { Point, Dimensions } from './types';
export type State = Dimensions
export const INITIAL_STATE: State = {
width: 0,
height: 0,
}
export enum ScaleMode {
Embed,
//AdjustParentWidth
AdjustParentHeight,
}
function getElementsFromInternalPoint(doc: Document, { x, y }: Point): Element[] {
// @ts-ignore (IE, Edge)
if (typeof doc.msElementsFromRect === 'function') {
@ -57,6 +50,11 @@ function isIframe(el: Element): el is HTMLIFrameElement {
}
export default class Screen {
static INITIAL_STATE: State = {
width: 0,
height: 0,
}
readonly overlay: HTMLDivElement
readonly cursor: Cursor

View file

@ -0,0 +1 @@
// separate Player from File & WebLive player

View file

@ -15,6 +15,10 @@ export default class WebLivePlayer extends WebPlayer {
...WebPlayer.INITIAL_STATE,
...AssistManager.INITIAL_STATE,
liveTimeTravel: false,
assistLoading: false,
// get ready() { // TODO TODO TODO how to extend state here?
// return this.assistLoading && super.ready
// }
}
assistManager: AssistManager // public so far
@ -23,12 +27,12 @@ export default class WebLivePlayer extends WebPlayer {
private lastMessageInFileTime = 0
private lastMessageInFileIndex = 0
constructor(wpState: Store<typeof WebLivePlayer.INITIAL_STATE>, private session:any, config: RTCIceServer[]) {
super(wpState, session, true)
constructor(wpStore: Store<typeof WebLivePlayer.INITIAL_STATE>, private session:any, config: RTCIceServer[]) {
super(wpStore, session, true)
this.assistManager = new AssistManager(
session,
f => this.messageManager.setMessagesLoading(f),
assistLoading => wpStore.update({ assistLoading }),
(msg, idx) => {
this.incomingMessages.push(msg)
if (!this.historyFileIsLoading) {
@ -38,31 +42,27 @@ export default class WebLivePlayer extends WebPlayer {
},
this.screen,
config,
wpState,
wpStore,
)
this.assistManager.connect(session.agentToken)
}
toggleTimetravel = async () => {
if (this.wpState.get().liveTimeTravel) {
// TODO: implement via jump() API rewritten instead
if (this.wpStore.get().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()
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, msg._index)
}
this.wpState.update({
await this.messageLoader.requestFallbackDOM()
this.wpStore.update({
liveTimeTravel: true,
})
this.messageManager.onMessagesLoaded()
result = true
// here we need to update also lists state, if we gonna use them this.messageManager.onFileReadSuccess
} catch(e) {
toast.error('Error requesting a session file')
console.error("EFS file download error:", e)
@ -75,16 +75,14 @@ export default class WebLivePlayer extends WebPlayer {
this.incomingMessages.length = 0
this.historyFileIsLoading = false
this.messageManager.setMessagesLoading(false)
return result;
}
jumpToLive = () => {
this.wpState.update({
live: true,
this.wpStore.update({
livePlay: true,
})
this.jump(this.wpState.get().lastMessageTime)
this.jump(this.wpStore.get().lastMessageTime)
}
clean = () => {

View file

@ -1,31 +1,38 @@
import { Log, LogLevel } from './types/log'
import { toast } from 'react-toastify';
import logger from 'App/logger';
import type { Store } from 'App/player'
import type { Store } from '../common/types'
import StorSubscriber from '../common/StoreSubscriber'
import Player from '../player/Player'
import MessageManager from './MessageManager'
import InspectorController from './addons/InspectorController'
import TargetMarker from './addons/TargetMarker'
import Screen, { ScaleMode } from './Screen/Screen'
import MessageLoader from './MessageLoader'
// export type State = typeof WebPlayer.INITIAL_STATE
export default class WebPlayer extends Player {
static readonly INITIAL_STATE = {
...Player.INITIAL_STATE,
...TargetMarker.INITIAL_STATE,
...MessageManager.INITIAL_STATE,
...MessageLoader.INITIAL_STATE,
ready: true,
inspectorMode: false,
}
private readonly inspectorController: InspectorController
protected readonly screen: Screen
protected readonly messageLoader: MessageLoader
protected readonly messageManager: MessageManager
private targetMarker: TargetMarker
constructor(protected wpState: Store<typeof WebPlayer.INITIAL_STATE>, session: any, live: boolean, isClickMap = false) {
let initialLists = live ? {} : {
event: session.events || [],
@ -39,13 +46,32 @@ export default class WebPlayer extends Player {
) || [],
}
const store = new StorSubscriber(wpState)
const screen = new Screen(session.isMobile, isClickMap ? ScaleMode.AdjustParentHeight : ScaleMode.Embed)
const messageManager = new MessageManager(session, wpState, screen, initialLists)
super(wpState, messageManager)
const messageManager = new MessageManager(session, store, screen, initialLists)
//TODO: same for scaling
store.subscribe(state => state.cssLoading, cssLoading => this.screen.displayFrame(!cssLoading))
store.subscribe(state => state.domLoading, domLoading => this.screen.display(!domLoading))
store.subscribe(state => {
const notReady = state.cssLoading || (state.domLoading && state.time >= state.lastMessageTime)
return !notReady
}, ready => store.update({ ready }))
super(store, messageManager)
this.screen = screen
this.messageManager = messageManager
this.messageLoader = new MessageLoader(session, wpState, messageManager.distributeMessage)
if (!live) { // hack. TODO: split OfflinePlayer class
void messageManager.loadMessages(isClickMap)
this.messageLoader.loadDOM()
.then(() => messageManager.onMessagesLoaded())
.catch((e: any) => {
logger.error(e)
toast.error('Error requesting a session file') // TODO: outside of the player lib
})
this.messageLoader.loadDevtools()
.then(() => messageManager.onMessagesLoaded())
.catch(e => logger.error("Can not download the devtools file", e))
}
this.targetMarker = new TargetMarker(this.screen, wpState)

View file

@ -0,0 +1,3 @@
export interface MessageManager {
handleMessage(msg: Message): void
}

View file

@ -0,0 +1,163 @@
import { toast } from 'react-toastify';
import logger from 'App/logger';
import { decryptSessionBytes } from '../network/crypto';
import MFileReader from '../messages/MFileReader';
import { loadFiles, requestEFSDom, requestEFSDevtools } from './loadFiles';
import type {
Message,
} from '../messages';
export default class MessageLoader {
private lastMessageInFileTime: number = 0;
constructor(
private readonly session: any /*Session*/,
private setMessagesLoading: (flag: boolean) => void,
private distributeMessage: (msg: Message, index: number) => void,
) {}
requestEFSFile() {
this.setMessagesLoading(true)
this.waitingForFiles = true
const onData = (byteArray: Uint8Array) => {
const onMessage = (msg: Message) => { this.lastMessageInFileTime = msg.time }
this.parseAndDistributeMessages(new MFileReader(byteArray, this.session.startedAt), onMessage)
}
// assist will pause and skip messages to prevent timestamp related errors
// ----> this.reloadMessageManagers()
// ---> this.windowNodeCounter.reset()
return requestEFSDom(this.session.sessionId)
.then(onData)
// --->.then(this.onFileReadSuccess)
// --->.catch(this.onFileReadFailed)
.finally(this.onFileReadFinally)
}
private parseAndDistributeMessages(fileReader: MFileReader, onMessage?: (msg: Message) => void) {
const msgs: Array<Message> = []
let next: ReturnType<MFileReader['next']>
while (next = fileReader.next()) {
const [msg, index] = next
this.distributeMessage(msg, index)
msgs.push(msg)
onMessage?.(msg)
}
logger.info("Messages loaded: ", msgs.length, msgs)
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
//--->// const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
// this.pagesManager.sortPages((m1, m2) => {
// if (m1.time === m2.time) {
// if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
// if (headChildrenIds.includes(m1.id)) {
// return -1;
// }
// } else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
// if (headChildrenIds.includes(m2.id)) {
// return 1;
// }
// } else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
// const m1FromHead = headChildrenIds.includes(m1.id);
// const m2FromHead = headChildrenIds.includes(m2.id);
// if (m1FromHead && !m2FromHead) {
// return -1;
// } else if (m2FromHead && !m1FromHead) {
// return 1;
// }
// }
// }
// return 0;
// })
}
private waitingForFiles: boolean = false
//--->// private onFileReadSuccess = () => {
// const stateToUpdate : Partial<State>= {
// performanceChartData: this.performanceTrackManager.chartData,
// performanceAvaliability: this.performanceTrackManager.avaliability,
// ...this.lists.getFullListsState(),
// }
// if (this.activityManager) {
// this.activityManager.end()
// stateToUpdate.skipIntervals = this.activityManager.list
// }
// this.state.update(stateToUpdate)
// }
//---> private onFileReadFailed = (e: any) => {
// this.state.update({ error: true })
// logger.error(e)
// toast.error('Error requesting a session file')
// }
private onFileReadFinally = () => {
//--->// this.incomingMessages
// .filter(msg => msg.time >= this.lastMessageInFileTime)
// .forEach(msg => this.distributeMessage(msg, 0))
this.waitingForFiles = false
this.setMessagesLoading(false)
}
private createNewParser(shouldDecrypt=true) {
const decrypt = shouldDecrypt && this.session.fileKey
? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey)
: (b: Uint8Array) => Promise.resolve(b)
// Each time called - new fileReader created. TODO: reuseable decryptor instance
const fileReader = new MFileReader(new Uint8Array(), this.session.startedAt)
return (b: Uint8Array) => decrypt(b).then(b => {
fileReader.append(b)
this.parseAndDistributeMessages(fileReader)
this.setMessagesLoading(false)
})
}
loadDOM() {
this.setMessagesLoading(true)
this.waitingForFiles = true
let fileReadPromise = this.session.domURL && this.session.domURL.length > 0
? loadFiles(this.session.domURL, this.createNewParser())
: Promise.reject()
return fileReadPromise
// EFS fallback
.catch(() => requestEFSDom(this.session.sessionId).then(this.createNewParser(false)))
// old url fallback
.catch(e => {
logger.error('Can not get normal session replay file:', e)
// back compat fallback to an old mobsUrl
return loadFiles(this.session.mobsUrl, this.createNewParser(false))
})
// --->.then(this.onFileReadSuccess)
// --->.catch(this.onFileReadFailed)
.finally(this.onFileReadFinally)
}
loadDevtools() {
// load devtools
if (this.session.devtoolsURL.length) {
// ---> this.state.update({ devtoolsLoading: true })
return loadFiles(this.session.devtoolsURL, this.createNewParser())
.catch(() =>
requestEFSDevtools(this.session.sessionId)
.then(this.createNewParser(false))
)
//--->// .then(() => {
// this.state.update(this.lists.getFullListsState())
// })
// --->.catch(e => logger.error("Can not download the devtools file", e))
// --->//.finally(() => this.state.update({ devtoolsLoading: false }))
}
return Promise.resolve()
}
}

View file

@ -0,0 +1,60 @@
import APIClient from 'App/api_client';
const NO_NTH_FILE = "nnf"
const NO_UNPROCESSED_FILES = "nuf"
export async function* loadFiles(
urls: string[],
){
const firstFileURL = urls[0]
urls = urls.slice(1)
if (!firstFileURL) {
throw "No urls provided"
}
try {
yield await window.fetch(firstFileURL)
.then(r => processAPIStreamResponse(r, true))
for(const url in urls) {
yield await window.fetch(url)
.then(r => processAPIStreamResponse(r, false))
}
} catch(e) {
if (e === NO_NTH_FILE) {
return
}
throw e
}
}
export async function requestEFSDom(sessionId: string) {
return await requestEFSMobFile(sessionId + "/dom.mob")
}
export async function requestEFSDevtools(sessionId: string) {
return await requestEFSMobFile(sessionId + "/devtools.mob")
}
async function requestEFSMobFile(filename: string) {
const api = new APIClient()
const res = await api.fetch('/unprocessed/' + filename)
if (res.status >= 400) {
throw NO_UNPROCESSED_FILES
}
return await processAPIStreamResponse(res, false)
}
const processAPIStreamResponse = (response: Response, isFirstFile: boolean) => {
return new Promise<ArrayBuffer>((res, rej) => {
if (response.status === 404 && !isFirstFile) {
return rej(NO_NTH_FILE)
}
if (response.status >= 400) {
return rej(
isFirstFile ? `no start file. status code ${ response.status }`
: `Bad endfile status code ${response.status}`
)
}
res(response.arrayBuffer())
}).then(buffer => new Uint8Array(buffer))
}