feat ui: prefetch mobfiles on hover, use prefetched if exist (#2098)

This commit is contained in:
Delirium 2024-04-17 09:03:50 +02:00 committed by GitHub
parent 156e1f94d6
commit ceb714617e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 440 additions and 204 deletions

View file

@ -1,17 +1,18 @@
import withPermissions from 'HOCs/withPermissions';
import React from 'react';
import { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { connect } from 'react-redux';
import usePageTitle from 'App/hooks/usePageTitle';
import { fetchV2, clearCurrentSession } from "Duck/sessions";
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
import { Link, NoContent, Loader } from 'UI';
import { sessions as sessionsRoute } from 'App/routes';
import withPermissions from 'HOCs/withPermissions'
import WebPlayer from './WebPlayer';
import { useStore } from 'App/mstore';
import { clearLogs } from 'App/dev/console';
import MobilePlayer from "Components/Session/MobilePlayer";
import { clearLogs } from 'App/dev/console';
import usePageTitle from 'App/hooks/usePageTitle';
import { useStore } from 'App/mstore';
import { sessions as sessionsRoute } from 'App/routes';
import MobilePlayer from 'Components/Session/MobilePlayer';
import { fetchList as fetchSlackList } from 'Duck/integrations/slack';
import { clearCurrentSession, fetchV2 } from 'Duck/sessions';
import { Link, Loader, NoContent } from 'UI';
import WebPlayer from './WebPlayer';
const SESSIONS_ROUTE = sessionsRoute();
@ -26,33 +27,30 @@ interface Props {
function Session({
sessionId,
loading,
hasErrors,
fetchV2,
clearCurrentSession,
session,
}: Props) {
usePageTitle("OpenReplay Session Player");
const [ initializing, setInitializing ] = useState(true)
usePageTitle('OpenReplay Session Player');
const { sessionStore } = useStore();
useEffect(() => {
if (sessionId != null) {
fetchV2(sessionId)
fetchV2(sessionId);
} else {
console.error("No sessionID in route.")
console.error('No sessionID in route.');
}
setInitializing(false)
return () => {
clearCurrentSession();
}
};
}, [sessionId]);
useEffect(() => {
clearLogs()
clearLogs();
sessionStore.resetUserFilter();
} ,[])
}, []);
const player = session.isMobileNative ? <MobilePlayer /> : <WebPlayer />
const player = session.isMobileNative ? <MobilePlayer /> : <WebPlayer />;
return (
<NoContent
show={hasErrors}
@ -60,27 +58,42 @@ function Session({
subtext={
<span>
{'Please check your data retention plan, or try '}
<Link to={ SESSIONS_ROUTE } className="link">{'another one'}</Link>
<Link to={SESSIONS_ROUTE} className="link">
{'another one'}
</Link>
</span>
}
>
<Loader className="flex-1" loading={ loading || initializing }>
<Loader className="flex-1" loading={!session.sessionId}>
{player}
</Loader>
</NoContent>
);
}
export default withPermissions(['SESSION_REPLAY'], '', true)(connect((state: any, props: any) => {
const { match: { params: { sessionId } } } = props;
export default withPermissions(
['SESSION_REPLAY'],
'',
true
)(
connect(
(state: any, props: any) => {
const {
match: {
params: { sessionId },
},
} = props;
return {
sessionId,
loading: state.getIn(['sessions', 'loading']),
hasErrors: !!state.getIn(['sessions', 'errors']),
session: state.getIn(['sessions', 'current']),
};
}, {
},
{
fetchSlackList,
fetchV2,
clearCurrentSession,
})(Session));
}
)(Session)
);

View file

@ -1,21 +1,28 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Modal, Loader } from 'UI';
import { toggleFullscreen, closeBottomBlock } from 'Duck/components/player';
import { fetchList } from 'Duck/integrations';
import withLocationHandlers from 'HOCs/withLocationHandlers';
import { createWebPlayer } from 'Player';
import { makeAutoObservable } from 'mobx';
import withLocationHandlers from 'HOCs/withLocationHandlers';
import { useStore } from 'App/mstore';
import PlayerBlockHeader from './Player/ReplayPlayer/PlayerBlockHeader';
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
import { observer } from 'mobx-react-lite';
import { Note } from 'App/services/NotesService';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useParams } from 'react-router-dom';
import { toast } from 'react-toastify';
import { useStore } from 'App/mstore';
import { Note } from 'App/services/NotesService';
import { closeBottomBlock, toggleFullscreen } from 'Duck/components/player';
import { fetchList } from 'Duck/integrations';
import { Loader, Modal } from 'UI';
import ReadNote from '../Session_/Player/Controls/components/ReadNote';
import PlayerBlockHeader from './Player/ReplayPlayer/PlayerBlockHeader';
import PlayerContent from './Player/ReplayPlayer/PlayerContent';
import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext';
const TABS = {
EVENTS: 'Activity',
CLICKMAP: 'Click Map',
@ -54,13 +61,21 @@ function WebPlayer(props: any) {
useEffect(() => {
playerInst = undefined;
if (!session.sessionId || contextValue.player !== undefined) return;
const mobData = sessionStore.prefetchedMobUrls[session.sessionId] as Record<string, any> | undefined;
const usePrefetched = props.prefetched && mobData?.data;
fetchList('issues');
sessionStore.setUserTimezone(session.timezone);
const [WebPlayerInst, PlayerStore] = createWebPlayer(
session,
(state) => makeAutoObservable(state),
toast
toast,
props.prefetched,
);
if (usePrefetched) {
if (mobData?.data) {
WebPlayerInst.preloadFirstFile(mobData?.data)
}
}
setContextValue({ player: WebPlayerInst, store: PlayerStore });
playerInst = WebPlayerInst;
@ -78,6 +93,12 @@ function WebPlayer(props: any) {
}
}, [session.sessionId]);
useEffect(() => {
if (!props.prefetched && session.domURL.length > 0) {
playerInst?.reinit(session)
}
}, [session.domURL.length, props.prefetched])
const { firstVisualEvent: visualOffset, messagesProcessed, tabStates, ready } = contextValue.store?.get() || {};
const cssLoading = ready && tabStates ? Object.values(tabStates).some(
({ cssLoading }) => cssLoading
@ -205,6 +226,7 @@ export default connect(
(state: any) => ({
session: state.getIn(['sessions', 'current']),
insights: state.getIn(['sessions', 'insights']),
prefetched: state.getIn(['sessions', 'prefetched']),
visitedEvents: state.getIn(['sessions', 'visitedEvents']),
jwt: state.getIn(['user', 'jwt']),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),

View file

@ -1,6 +1,13 @@
import React, { useState, useEffect } from 'react';
import { Link, Icon } from 'UI';
import { session as sessionRoute, liveSession as liveSessionRoute } from 'App/routes';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import {
liveSession as liveSessionRoute,
session as sessionRoute,
withSiteId,
} from 'App/routes';
import { Icon, Link } from 'UI';
import { connect } from 'react-redux';
const PLAY_ICON_NAMES = {
notPlayed: 'play-fill',
@ -8,7 +15,8 @@ const PLAY_ICON_NAMES = {
hovered: 'play-hover',
};
const getDefaultIconName = (isViewed: any) => (!isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played);
const getDefaultIconName = (isViewed: any) =>
!isViewed ? PLAY_ICON_NAMES.notPlayed : PLAY_ICON_NAMES.played;
interface Props {
isAssist: boolean;
@ -17,10 +25,13 @@ interface Props {
onClick?: () => void;
queryParams?: any;
newTab?: boolean;
query?: string
query?: string;
beforeOpen?: () => void;
siteId?: string;
}
export default function PlayLink(props: Props) {
function PlayLink(props: Props) {
const { isAssist, viewed, sessionId, onClick = null, queryParams } = props;
const history = useHistory();
const defaultIconName = getDefaultIconName(viewed);
const [isHovered, toggleHover] = useState(false);
@ -31,16 +42,33 @@ export default function PlayLink(props: Props) {
else setIconName(getDefaultIconName(viewed));
}, [isHovered, viewed]);
const link = isAssist ? liveSessionRoute(sessionId, queryParams) : sessionRoute(sessionId);
const link = isAssist
? liveSessionRoute(sessionId, queryParams)
: sessionRoute(sessionId);
const handleBeforeOpen = () => {
if (props.beforeOpen) {
props.beforeOpen();
history.push(withSiteId(link + (props.query ? props.query : ''), props.siteId));
}
};
const onLinkClick = props.beforeOpen ? handleBeforeOpen : onClick;
return (
<Link
onClick={onClick ? onClick : () => {}}
onClick={onLinkClick}
to={link + (props.query ? props.query : '')}
onMouseEnter={() => toggleHover(true)}
onMouseLeave={() => toggleHover(false)}
target={props.newTab ? "_blank" : undefined} rel={props.newTab ? "noopener noreferrer" : undefined}
target={props.newTab ? '_blank' : undefined}
rel={props.newTab ? 'noopener noreferrer' : undefined}
>
<Icon name={iconName} size={38} color={isAssist ? 'tealx' : 'teal'} />
</Link>
);
}
export default connect((state: any, props: Props) => ({
siteId: props.siteId || state.getIn([ 'site', 'siteId' ])
}))(PlayLink);

View file

@ -1,26 +1,38 @@
import React, { useMemo } from 'react';
import cn from 'classnames';
import { CountryFlag, Avatar, TextEllipsis, Label, Icon, Tooltip, ItemMenu } from 'UI';
import { useStore } from 'App/mstore';
import copy from 'copy-to-clipboard';
import { Duration } from 'luxon';
import { observer } from 'mobx-react-lite';
import React, { useMemo } from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { toast } from 'react-toastify';
import { durationFormatted, formatTimeOrDate } from 'App/date';
import stl from './sessionItem.module.css';
import Counter from './Counter';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import SessionMetaList from './SessionMetaList';
import PlayLink from './PlayLink';
import ErrorBars from './ErrorBars';
import { presetSession } from 'App/duck/sessions';
import { useStore } from 'App/mstore';
import {
assist as assistRoute,
isRoute,
liveSession,
sessions as sessionsRoute,
session as sessionRoute,
isRoute
sessions as sessionsRoute,
} from 'App/routes';
import { capitalize } from 'App/utils';
import { Duration } from 'luxon';
import copy from 'copy-to-clipboard';
import { toast } from 'react-toastify';
import {
Avatar,
CountryFlag,
Icon,
ItemMenu,
Label,
TextEllipsis,
Tooltip,
} from 'UI';
import Counter from './Counter';
import ErrorBars from './ErrorBars';
import PlayLink from './PlayLink';
import SessionMetaList from './SessionMetaList';
import stl from './sessionItem.module.css';
const ASSIST_ROUTE = assistRoute();
const ASSIST_LIVE_SESSION = liveSession();
@ -69,12 +81,20 @@ interface Props {
ignoreAssist?: boolean;
bookmarked?: boolean;
toggleFavorite?: (sessionId: string) => void;
query?: string
query?: string;
presetSession?: typeof presetSession;
}
const PREFETCH_STATE = {
none: 0,
loading: 1,
fetched: 2,
}
function SessionItem(props: RouteComponentProps & Props) {
const { settingsStore } = useStore();
const { settingsStore, sessionStore } = useStore();
const { timezone, shownTimezone } = settingsStore.sessionSettings;
const [prefetchState, setPrefetched] = React.useState(PREFETCH_STATE.none);
const {
session,
@ -88,6 +108,7 @@ function SessionItem(props: RouteComponentProps & Props) {
ignoreAssist = false,
bookmarked = false,
query,
presetSession,
} = props;
const {
@ -110,7 +131,7 @@ function SessionItem(props: RouteComponentProps & Props) {
metadata,
issueTypes,
active,
timezone: userTimezone
timezone: userTimezone,
} = session;
const location = props.location;
@ -120,11 +141,11 @@ function SessionItem(props: RouteComponentProps & Props) {
const hasUserId = userId || userAnonymousId;
const isSessions = isRoute(SESSIONS_ROUTE, location.pathname);
const isAssist =
!ignoreAssist &&
(!ignoreAssist &&
(isRoute(ASSIST_ROUTE, location.pathname) ||
isRoute(ASSIST_LIVE_SESSION, location.pathname) ||
location.pathname.includes('multiview')) ||
props.live
location.pathname.includes('multiview'))) ||
props.live;
const isLastPlayed = lastPlayedSessionId === sessionId;
@ -146,16 +167,32 @@ function SessionItem(props: RouteComponentProps & Props) {
}${sessionRoute(sessionId)}`;
copy(sessionPath);
toast.success('Session URL copied to clipboard');
}
},
},
{
icon: 'trash',
text: 'Remove',
onClick: () => (props.toggleFavorite ? props.toggleFavorite(sessionId) : null)
}
onClick: () =>
props.toggleFavorite ? props.toggleFavorite(sessionId) : null,
},
];
}, []);
const handleHover = async () => {
if (prefetchState !== PREFETCH_STATE.none || props.live || isAssist) return;
setPrefetched(PREFETCH_STATE.loading);
try {
await sessionStore.getFirstMob(sessionId);
setPrefetched(PREFETCH_STATE.fetched);
} catch (e) {
console.error('Error while prefetching first mob', e);
}
};
const openSession = () => {
if (props.live || isAssist || prefetchState === PREFETCH_STATE.none) return
presetSession?.(session);
};
return (
<Tooltip
delay={0}
@ -164,23 +201,31 @@ function SessionItem(props: RouteComponentProps & Props) {
>
<div
className={cn(stl.sessionItem, 'flex flex-col py-2 px-4')}
id='session-item'
id="session-item"
onClick={(e) => e.stopPropagation()}
>
<div className='flex items-start'>
<div className="flex items-start">
<div className={cn('flex items-center w-full')}>
{!compact && (
<div className='flex items-center pr-2 shrink-0' style={{ width: '40%' }}>
<div>
<Avatar isActive={active} seed={userNumericHash} isAssist={isAssist} />
</div>
<div
className='flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0'>
className="flex items-center pr-2 shrink-0"
style={{ width: '40%' }}
>
<div>
<Avatar
isActive={active}
seed={userNumericHash}
isAssist={isAssist}
/>
</div>
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center shrink-0">
<div
className={cn('text-lg', {
'color-teal cursor-pointer': !disableUser && hasUserId && !props.isDisabled,
[stl.userName]: !disableUser && hasUserId && !props.isDisabled,
'color-gray-medium': disableUser || !hasUserId
'color-teal cursor-pointer':
!disableUser && hasUserId && !props.isDisabled,
[stl.userName]:
!disableUser && hasUserId && !props.isDisabled,
'color-gray-medium': disableUser || !hasUserId,
})}
onClick={() =>
!disableUser && !hasUserFilter && hasUserId
@ -190,7 +235,7 @@ function SessionItem(props: RouteComponentProps & Props) {
>
<TextEllipsis
text={userDisplayName}
maxWidth='200px'
maxWidth="200px"
popupProps={{ inverted: true, size: 'tiny' }}
/>
</div>
@ -199,7 +244,7 @@ function SessionItem(props: RouteComponentProps & Props) {
)}
<div
style={{ width: compact ? '40%' : '20%' }}
className='px-2 flex flex-col justify-between'
className="px-2 flex flex-col justify-between"
>
<div>
<Tooltip
@ -208,7 +253,9 @@ function SessionItem(props: RouteComponentProps & Props) {
title={
<div className={'flex flex-col gap-1'}>
<span>
Local Time: {formatTimeOrDate(startedAt, timezone, true)} {timezone.label}
Local Time:{' '}
{formatTimeOrDate(startedAt, timezone, true)}{' '}
{timezone.label}
</span>
{userTimezone ? (
<span>
@ -217,7 +264,7 @@ function SessionItem(props: RouteComponentProps & Props) {
startedAt,
{
label: userTimezone.split('+').join(' +'),
value: userTimezone.split(':')[0]
value: userTimezone.split(':')[0],
},
true
)}{' '}
@ -226,7 +273,7 @@ function SessionItem(props: RouteComponentProps & Props) {
) : null}
</div>
}
className='w-fit !block'
className="w-fit !block"
>
<TextEllipsis
text={formatTimeOrDate(
@ -234,7 +281,7 @@ function SessionItem(props: RouteComponentProps & Props) {
shownTimezone === 'user' && userTimezone
? {
label: userTimezone.split('+').join(' +'),
value: userTimezone.split(':')[0]
value: userTimezone.split(':')[0],
}
: timezone
)}
@ -242,20 +289,33 @@ function SessionItem(props: RouteComponentProps & Props) {
/>
</Tooltip>
</div>
<div className='flex items-center color-gray-medium py-1'>
<div className="flex items-center color-gray-medium py-1">
{!isAssist && (
<>
<div className='color-gray-medium'>
<span className='mr-1'>{eventsCount}</span>
<span>{eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event'}</span>
<div className="color-gray-medium">
<span className="mr-1">{eventsCount}</span>
<span>
{eventsCount === 0 || eventsCount > 1
? 'Events'
: 'Event'}
</span>
</div>
<Icon name='circle-fill' size={3} className='mx-4' />
<Icon name="circle-fill" size={3} className="mx-4" />
</>
)}
<div>{live || props.live ? <Counter startTime={startedAt} /> : formattedDuration}</div>
<div>
{live || props.live ? (
<Counter startTime={startedAt} />
) : (
formattedDuration
)}
</div>
</div>
<div style={{ width: '30%' }} className='px-2 flex flex-col justify-between'>
</div>
<div
style={{ width: '30%' }}
className="px-2 flex flex-col justify-between"
>
<div style={{ height: '21px' }}>
<CountryFlag
userCity={userCity}
@ -276,14 +336,17 @@ function SessionItem(props: RouteComponentProps & Props) {
{userOs && userBrowser ? (
<Icon name="circle-fill" size={3} className="mx-4" />
) : null}
<span className={/ios/i.test(userOs) ? '' : 'capitalize'} style={{ maxWidth: '70px' }}>
<span
className={/ios/i.test(userOs) ? '' : 'capitalize'}
style={{ maxWidth: '70px' }}
>
<TextEllipsis
text={/ios/i.test(userOs) ? 'iOS' : capitalize(userOs)}
popupProps={{ inverted: true, size: 'tiny' }}
/>
</span>
<Icon name='circle-fill' size={3} className='mx-4' />
<span className='capitalize' style={{ maxWidth: '70px' }}>
<Icon name="circle-fill" size={3} className="mx-4" />
<span className="capitalize" style={{ maxWidth: '70px' }}>
<TextEllipsis
text={capitalize(userDeviceType)}
popupProps={{ inverted: true, size: 'tiny' }}
@ -292,35 +355,44 @@ function SessionItem(props: RouteComponentProps & Props) {
</div>
</div>
{isSessions && (
<div style={{ width: '10%' }} className='self-center px-2 flex items-center'>
<div
style={{ width: '10%' }}
className="self-center px-2 flex items-center"
>
<ErrorBars count={issueTypes.length} />
</div>
)}
</div>
<div className='flex items-center'>
<div className="flex items-center">
<div
className={cn(
stl.playLink,
props.isDisabled ? 'cursor-not-allowed' : 'cursor-pointer'
)}
id='play-button'
id="play-button"
data-viewed={viewed}
>
{live && session.isCallActive && session.agentIds!.length > 0 ? (
<div className='mr-4'>
<Label className='bg-gray-lightest p-1 px-2 rounded-lg'>
<span className='color-gray-medium text-xs' style={{ whiteSpace: 'nowrap' }}>
<div className="mr-4">
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
<span
className="color-gray-medium text-xs"
style={{ whiteSpace: 'nowrap' }}
>
CALL IN PROGRESS
</span>
</Label>
</div>
) : null}
{isSessions && (
<div className='mr-4 flex-shrink-0 w-24'>
<div className="mr-4 flex-shrink-0 w-24">
{isLastPlayed && (
<Label className='bg-gray-lightest p-1 px-2 rounded-lg'>
<span className='color-gray-medium text-xs' style={{ whiteSpace: 'nowrap' }}>
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
<span
className="color-gray-medium text-xs"
style={{ whiteSpace: 'nowrap' }}
>
LAST PLAYED
</span>
</Label>
@ -329,15 +401,15 @@ function SessionItem(props: RouteComponentProps & Props) {
)}
{props.isAdd ? (
<div
className='rounded-full border-tealx p-2 border'
className="rounded-full border-tealx p-2 border"
onClick={() => (props.isDisabled ? null : props.onClick())}
>
<div className='bg-tealx rounded-full p-2'>
<Icon name='plus' size={16} color='white' />
<div className="bg-tealx rounded-full p-2">
<Icon name="plus" size={16} color="white" />
</div>
</div>
) : (
<>
<div onMouseEnter={handleHover}>
<PlayLink
isAssist={isAssist}
sessionId={sessionId}
@ -345,21 +417,24 @@ function SessionItem(props: RouteComponentProps & Props) {
onClick={onClick}
queryParams={queryParams}
query={query}
beforeOpen={props.live || isAssist ? undefined : openSession}
/>
{bookmarked && (
<div className='ml-2 cursor-pointer'>
<div className="ml-2 cursor-pointer">
<ItemMenu bold items={menuItems} />
</div>
)}
</>
</div>
)}
</div>
</div>
</div>
{_metaList.length > 0 && <SessionMetaList className='mt-4' metaList={_metaList} />}
{_metaList.length > 0 && (
<SessionMetaList className="mt-4" metaList={_metaList} />
)}
</div>
</Tooltip>
);
}
export default withRouter(observer(SessionItem));
export default withRouter(connect(null, { presetSession })(observer(SessionItem)));

View file

@ -48,6 +48,8 @@ const SET_ACTIVE_TAB = 'sessions/SET_ACTIVE_TAB';
const CLEAR_CURRENT_SESSION = 'sessions/CLEAR_CURRENT_SESSION'
const PREFETCH_SESSION = 'sessions/PREFETCH_SESSION'
const range = getDateRangeFromValue(LAST_7_DAYS);
const defaultDateFilters = {
url: '',
@ -60,6 +62,7 @@ const initObj = {
list: [],
sessionIds: [],
current: new Session(),
prefetched: false,
eventsAsked: false,
total: 0,
keyMap: Map(),
@ -185,6 +188,13 @@ const reducer = (state = initialState, action: IAction) => {
return state
.set('current', session)
.set('prefetched', false)
}
case PREFETCH_SESSION: {
const { data } = action;
return state
.set('current', data)
.set('prefetched', true);
}
case FETCH_EVENTS.SUCCESS: {
const {
@ -454,6 +464,13 @@ export const fetchV2 = (sessionId: string) =>
}
export function presetSession(sessionData) {
return {
type: PREFETCH_SESSION,
data: sessionData
}
}
export function clearCurrentSession() {
return {
type: CLEAR_CURRENT_SESSION

View file

@ -10,6 +10,7 @@ import { getDateRangeFromValue } from "App/dateRange";
import { getRE, setSessionFilter, getSessionFilter, compareJsonObjects, cleanSessionFilters } from 'App/utils';
import store from 'App/store'
import { Note } from "App/services/NotesService";
import { loadFile } from "../player/web/network/loadFiles";
class UserFilter {
endDate: number = new Date().getTime();
@ -125,6 +126,7 @@ export default class SessionStore {
previousId = ''
nextId = ''
userTimezone = ''
prefetchedMobUrls: Record<string, { data: Uint8Array, entryNum: number }> = {}
constructor() {
makeAutoObservable(this, {
@ -141,6 +143,28 @@ export default class SessionStore {
this.userFilter = new UserFilter();
}
async getFirstMob(sessionId: string) {
const { domURL } = await sessionService.getFirstMobUrl(sessionId)
await loadFile(
domURL[0],
(data) => this.setPrefetchedMobUrl(sessionId, data)
)
}
setPrefetchedMobUrl(sessionId: string, fileData: Uint8Array) {
const keys = Object.keys(this.prefetchedMobUrls)
const toLimit = 10 - keys.length
if (toLimit < 0) {
const oldest = keys.sort(
(a, b) => this.prefetchedMobUrls[a].entryNum - this.prefetchedMobUrls[b].entryNum
)[0]
delete this.prefetchedMobUrls[oldest]
}
const nextEntryNum = keys.length > 0
? Math.max(...keys.map(key => this.prefetchedMobUrls[key].entryNum)) + 1 : 0
this.prefetchedMobUrls[sessionId] = { data: fileData, entryNum: nextEntryNum }
}
getSessions(filter: any): Promise<any> {
return new Promise((resolve, reject) => {
sessionService

View file

@ -40,7 +40,8 @@ export function createIOSPlayer(
export function createWebPlayer(
session: SessionFilesInfo,
wrapStore?: (s: IWebPlayerStore) => IWebPlayerStore,
uiErrorHandler?: { error: (msg: string) => void }
uiErrorHandler?: { error: (msg: string) => void },
prefetched?: boolean,
): [IWebPlayer, IWebPlayerStore] {
let store: WebPlayerStore = new SimpleStore<WebState>({
...WebPlayer.INITIAL_STATE,
@ -49,7 +50,7 @@ export function createWebPlayer(
store = wrapStore(store);
}
const player = new WebPlayer(store, session, false, false, uiErrorHandler);
const player = new WebPlayer(store, session, false, false, uiErrorHandler, prefetched);
return [player, store];
}

View file

@ -32,13 +32,17 @@ export default class MessageLoader {
};
constructor(
private readonly session: SessionFilesInfo,
private session: SessionFilesInfo,
private store: Store<State>,
private messageManager: MessageManager | IOSMessageManager,
private isClickmap: boolean,
private uiErrorHandler?: { error: (msg: string) => void }
) {}
setSession(session: SessionFilesInfo) {
this.session = session
}
createNewParser(
shouldDecrypt = true,
onMessagesDone: (msgs: PlayerMsg[], file?: string) => void,
@ -145,6 +149,18 @@ export default class MessageLoader {
}
}
preloaded = false;
async preloadFirstFile(data: Uint8Array) {
this.mobParser = this.createNewParser(true, this.processMessages, 'p:dom');
try {
await this.mobParser(data)
this.preloaded = true;
} catch (e) {
console.error('error parsing msgs', e)
}
}
async loadDomFiles(urls: string[], parser: (b: Uint8Array) => Promise<void>) {
if (urls.length > 0) {
this.store.update({ domLoading: true });
@ -197,21 +213,25 @@ export default class MessageLoader {
}
}
mobParser: (b: Uint8Array) => Promise<void>
loadMobs = async () => {
const loadMethod =
this.session.domURL && this.session.domURL.length > 0
? {
mobUrls: this.session.domURL,
parser: () =>
this.createNewParser(true, this.processMessages, 'dom'),
this.createNewParser(true, this.processMessages, 'd:dom'),
}
: {
mobUrls: this.session.mobsUrl,
parser: () =>
this.createNewParser(false, this.processMessages, 'dom'),
this.createNewParser(false, this.processMessages, 'm:dom'),
};
const parser = loadMethod.parser();
if (!this.mobParser) {
this.mobParser = loadMethod.parser();
}
const parser = this.mobParser
const devtoolsParser = this.createNewParser(
true,
this.processMessages,
@ -225,7 +245,7 @@ export default class MessageLoader {
* as a tradeoff we have some copy-paste code
* for the devtools file
* */
await loadFiles([loadMethod.mobUrls[0]], parser);
if (!this.preloaded) await loadFiles([loadMethod.mobUrls[0]], parser);
this.messageManager.onFileReadFinally();
const restDomFilesPromise = this.loadDomFiles(
[...loadMethod.mobUrls.slice(1)],

View file

@ -237,9 +237,9 @@ export default class MessageManager {
});
if (
this.waitingForFiles &&
this.lastMessageTime <= t &&
t !== this.session.duration.milliseconds
this.waitingForFiles ||
(this.lastMessageTime <= t &&
t !== this.session.duration.milliseconds)
) {
this.setMessagesLoading(true);
}

View file

@ -20,9 +20,10 @@ export default class WebPlayer extends Player {
liveTimeTravel: false,
inspectorMode: false,
mobsFetched: false,
}
private readonly inspectorController: InspectorController
private inspectorController: InspectorController
protected screen: Screen
protected readonly messageManager: MessageManager
protected readonly messageLoader: MessageLoader
@ -34,7 +35,8 @@ export default class WebPlayer extends Player {
session: SessionFilesInfo,
live: boolean,
isClickMap = false,
public readonly uiErrorHandler?: { error: (msg: string) => void }
public readonly uiErrorHandler?: { error: (msg: string) => void },
private readonly prefetched?: boolean,
) {
let initialLists = live ? {} : {
event: session.events || [],
@ -62,8 +64,10 @@ export default class WebPlayer extends Player {
this.screen = screen
this.messageManager = messageManager
this.messageLoader = messageLoader
if (!live) { // hack. TODO: split OfflinePlayer class
if (!live && !prefetched) { // hack. TODO: split OfflinePlayer class
void messageLoader.loadFiles()
wpState.update({ mobsFetched: true })
}
this.targetMarker = new TargetMarker(this.screen, wpState)
@ -84,6 +88,30 @@ export default class WebPlayer extends Player {
window.playerJumpToTime = this.jump.bind(this)
}
preloadFirstFile(data: Uint8Array) {
void this.messageLoader.preloadFirstFile(data)
}
reinit(session: SessionFilesInfo) {
if (this.wpState.get().mobsFetched) return; // already initialized
this.messageLoader.setSession(session)
void this.messageLoader.loadFiles();
this.targetMarker = new TargetMarker(this.screen, this.wpState)
this.inspectorController = new InspectorController(this.screen, this.wpState)
const endTime = session.duration?.valueOf() || 0
this.wpState.update({
//@ts-ignore
session,
endTime, // : 0,
})
// @ts-ignore
window.playerJumpToTime = this.jump.bind(this)
}
updateLists = (session: any) => {
const lists = {
event: session.events || [],

View file

@ -46,6 +46,14 @@ export default class SettingsService {
.catch((e) => Promise.reject(e));
}
getFirstMobUrl(sessionId: string): Promise<{ domURL: string[] }> {
return this.client
.get(`/sessions/${sessionId}/first-mob`)
.then((r) => r.json())
.then((j) => j.data || {})
.catch(console.error);
}
getSessionInfo(sessionId: string, isLive?: boolean): Promise<ISession> {
return this.client
.get(isLive ? `/assist/sessions/${sessionId}` : `/sessions/${sessionId}`)