Add ping pong health, remove a timeout interval, fix up network events (#1555)

* Add ping pong health, fix up network events

* Change the default connection state for test

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
This commit is contained in:
49fl
2024-03-14 12:18:06 -04:00
committed by GitHub
parent 0579ccd53b
commit 61d7950ca3
4 changed files with 296 additions and 188 deletions

View File

@ -30,7 +30,7 @@ describe('NetworkHealthIndicator tests', () => {
fireEvent.click(screen.getByTestId('network-toggle')) fireEvent.click(screen.getByTestId('network-toggle'))
expect(screen.getByTestId('network')).toHaveTextContent( expect(screen.getByTestId('network')).toHaveTextContent(
NETWORK_HEALTH_TEXT[NetworkHealthState.Ok] NETWORK_HEALTH_TEXT[NetworkHealthState.Issue]
) )
}) })

View File

@ -6,7 +6,8 @@ import {
ConnectingTypeGroup, ConnectingTypeGroup,
DisconnectingType, DisconnectingType,
engineCommandManager, engineCommandManager,
EngineConnectionState, EngineCommandManagerEvents,
EngineConnectionEvents,
EngineConnectionStateType, EngineConnectionStateType,
ErrorType, ErrorType,
initialConnectingTypeGroupState, initialConnectingTypeGroupState,
@ -81,37 +82,35 @@ const overallConnectionStateIcon: Record<
} }
export function useNetworkStatus() { export function useNetworkStatus() {
const [steps, setSteps] = useState(initialConnectingTypeGroupState) const [steps, setSteps] = useState(
structuredClone(initialConnectingTypeGroupState)
)
const [internetConnected, setInternetConnected] = useState<boolean>(true) const [internetConnected, setInternetConnected] = useState<boolean>(true)
const [overallState, setOverallState] = useState<NetworkHealthState>( const [overallState, setOverallState] = useState<NetworkHealthState>(
NetworkHealthState.Ok NetworkHealthState.Disconnected
) )
const [pingPongHealth, setPingPongHealth] = useState<'OK' | 'BAD'>('BAD')
const [hasCopied, setHasCopied] = useState<boolean>(false) const [hasCopied, setHasCopied] = useState<boolean>(false)
const [error, setError] = useState<ErrorType | undefined>(undefined) const [error, setError] = useState<ErrorType | undefined>(undefined)
const issues: Record<ConnectingTypeGroup, boolean> = { const hasIssue = (i: [ConnectingType, boolean | undefined]) =>
[ConnectingTypeGroup.WebSocket]: steps[ConnectingTypeGroup.WebSocket].some( i[1] === undefined ? i[1] : !i[1]
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
[ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].some(
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
[ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].some(
(a: [ConnectingType, boolean | undefined]) => a[1] === false
),
}
const hasIssues: boolean = const [issues, setIssues] = useState<
issues[ConnectingTypeGroup.WebSocket] || Record<ConnectingTypeGroup, boolean | undefined>
issues[ConnectingTypeGroup.ICE] || >({
issues[ConnectingTypeGroup.WebRTC] [ConnectingTypeGroup.WebSocket]: undefined,
[ConnectingTypeGroup.ICE]: undefined,
[ConnectingTypeGroup.WebRTC]: undefined,
})
const [hasIssues, setHasIssues] = useState<boolean | undefined>(undefined)
useEffect(() => { useEffect(() => {
setOverallState( setOverallState(
!internetConnected !internetConnected
? NetworkHealthState.Disconnected ? NetworkHealthState.Disconnected
: hasIssues : hasIssues || hasIssues === undefined
? NetworkHealthState.Issue ? NetworkHealthState.Issue
: NetworkHealthState.Ok : NetworkHealthState.Ok
) )
@ -134,19 +133,59 @@ export function useNetworkStatus() {
}, []) }, [])
useEffect(() => { useEffect(() => {
engineCommandManager.onConnectionStateChange( console.log(pingPongHealth)
(engineConnectionState: EngineConnectionState) => { }, [pingPongHealth])
let hasSetAStep = false
useEffect(() => {
const issues = {
[ConnectingTypeGroup.WebSocket]: steps[
ConnectingTypeGroup.WebSocket
].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
[ConnectingTypeGroup.ICE]: steps[ConnectingTypeGroup.ICE].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
[ConnectingTypeGroup.WebRTC]: steps[ConnectingTypeGroup.WebRTC].reduce(
(acc: boolean | undefined, a) =>
acc === true || acc === undefined ? acc : hasIssue(a),
false
),
}
setIssues(issues)
}, [steps])
useEffect(() => {
setHasIssues(
issues[ConnectingTypeGroup.WebSocket] ||
issues[ConnectingTypeGroup.ICE] ||
issues[ConnectingTypeGroup.WebRTC]
)
}, [issues])
useEffect(() => {
const onPingPongChange = ({ detail: state }: CustomEvent) => {
setPingPongHealth(state)
}
const onConnectionStateChange = ({
detail: engineConnectionState,
}: CustomEvent) => {
setSteps((steps) => {
let nextSteps = structuredClone(steps)
if ( if (
engineConnectionState.type === EngineConnectionStateType.Connecting engineConnectionState.type === EngineConnectionStateType.Connecting
) { ) {
const groups = Object.values(steps) const groups = Object.values(nextSteps)
for (let group of groups) { for (let group of groups) {
for (let step of group) { for (let step of group) {
if (step[0] !== engineConnectionState.value.type) continue if (step[0] !== engineConnectionState.value.type) continue
step[1] = true step[1] = true
hasSetAStep = true
} }
} }
} }
@ -154,7 +193,7 @@ export function useNetworkStatus() {
if ( if (
engineConnectionState.type === EngineConnectionStateType.Disconnecting engineConnectionState.type === EngineConnectionStateType.Disconnecting
) { ) {
const groups = Object.values(steps) const groups = Object.values(nextSteps)
for (let group of groups) { for (let group of groups) {
for (let step of group) { for (let step of group) {
if ( if (
@ -165,7 +204,6 @@ export function useNetworkStatus() {
?.type === step[0] ?.type === step[0]
) { ) {
step[1] = false step[1] = false
hasSetAStep = true
} }
} }
} }
@ -176,11 +214,50 @@ export function useNetworkStatus() {
} }
} }
if (hasSetAStep) { // Reset the state of all steps if we have disconnected.
setSteps(steps) if (
engineConnectionState.type === EngineConnectionStateType.Disconnected
) {
return structuredClone(initialConnectingTypeGroupState)
} }
}
return nextSteps
})
}
const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
engineConnection.addEventListener(
EngineConnectionEvents.PingPongChanged,
onPingPongChange as EventListener
)
engineConnection.addEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChange as EventListener
)
}
engineCommandManager.addEventListener(
EngineCommandManagerEvents.EngineAvailable,
onEngineAvailable as EventListener
) )
return () => {
engineCommandManager.removeEventListener(
EngineCommandManagerEvents.EngineAvailable,
onEngineAvailable as EventListener
)
// When the component is unmounted these should be assigned, but it's possible
// the component mounts and unmounts before engine is available.
engineCommandManager.engineConnection?.addEventListener(
EngineConnectionEvents.PingPongChanged,
onPingPongChange as EventListener
)
engineCommandManager.engineConnection?.addEventListener(
EngineConnectionEvents.ConnectionStateChanged,
onConnectionStateChange as EventListener
)
}
}, []) }, [])
return { return {
@ -192,6 +269,7 @@ export function useNetworkStatus() {
error, error,
setHasCopied, setHasCopied,
hasCopied, hasCopied,
pingPongHealth,
} }
} }
@ -256,18 +334,18 @@ export const NetworkHealthIndicator = () => {
size="lg" size="lg"
icon={ icon={
hasIssueToIcon[ hasIssueToIcon[
issues[name as ConnectingTypeGroup].toString() String(issues[name as ConnectingTypeGroup])
] ]
} }
iconClassName={ iconClassName={
hasIssueToIconColors[ hasIssueToIconColors[
issues[name as ConnectingTypeGroup].toString() String(issues[name as ConnectingTypeGroup])
].icon ].icon
} }
bgClassName={ bgClassName={
'rounded-sm ' + 'rounded-sm ' +
hasIssueToIconColors[ hasIssueToIconColors[
issues[name as ConnectingTypeGroup].toString() String(issues[name as ConnectingTypeGroup])
].bg ].bg
} }
/> />

View File

@ -32,6 +32,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
const { state } = useModelingContext() const { state } = useModelingContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()
const { overallState } = useNetworkStatus() const { overallState } = useNetworkStatus()
const isNetworkOkay = overallState === NetworkHealthState.Ok const isNetworkOkay = overallState === NetworkHealthState.Ok
useEffect(() => { useEffect(() => {

View File

@ -1,5 +1,5 @@
import { PathToNode, Program, SourceRange } from 'lang/wasm' import { PathToNode, Program, SourceRange } from 'lang/wasm'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_CONNECTION_TIMEOUT_MS } from 'env' import { VITE_KC_API_WS_MODELING_URL } from 'env'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { exportSave } from 'lib/exportSave' import { exportSave } from 'lib/exportSave'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@ -8,6 +8,9 @@ import { sceneInfra } from 'clientSideScene/sceneInfra'
let lastMessage = '' let lastMessage = ''
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000
interface CommandInfo { interface CommandInfo {
commandType: CommandTypes commandType: CommandTypes
range: SourceRange range: SourceRange
@ -37,11 +40,6 @@ export interface ArtifactMap {
[key: string]: ResultCommand | PendingCommand | FailedCommand [key: string]: ResultCommand | PendingCommand | FailedCommand
} }
interface NewTrackArgs {
conn: EngineConnection
mediaStream: MediaStream
}
// This looks funny, I know. This is needed because node and the browser // This looks funny, I know. This is needed because node and the browser
// disagree as to the type. In a browser it's a number, but in node it's a // disagree as to the type. In a browser it's a number, but in node it's a
// "Timeout". // "Timeout".
@ -158,10 +156,28 @@ export type EngineConnectionState =
| State<EngineConnectionStateType.Disconnecting, DisconnectingValue> | State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
| State<EngineConnectionStateType.Disconnected, void> | State<EngineConnectionStateType.Disconnected, void>
export type PingPongState = 'OK' | 'BAD'
export enum EngineConnectionEvents {
// Fires for each ping-pong success or failure.
PingPongChanged = 'ping-pong-changed', // (state: PingPongState) => void
// For now, this is only used by the NetworkHealthIndicator.
// We can eventually use it for more, but one step at a time.
ConnectionStateChanged = 'connection-state-changed', // (state: EngineConnectionState) => void
// These are used for the EngineCommandManager and were created
// before onConnectionStateChange existed.
ConnectionStarted = 'connection-started', // (engineConnection: EngineConnection) => void
Opened = 'opened', // (engineConnection: EngineConnection) => void
Closed = 'closed', // (engineConnection: EngineConnection) => void
NewTrack = 'new-track', // (track: NewTrackArgs) => void
}
// EngineConnection encapsulates the connection(s) to the Engine // EngineConnection encapsulates the connection(s) to the Engine
// for the EngineCommandManager; namely, the underlying WebSocket // for the EngineCommandManager; namely, the underlying WebSocket
// and WebRTC connections. // and WebRTC connections.
class EngineConnection { class EngineConnection extends EventTarget {
websocket?: WebSocket websocket?: WebSocket
pc?: RTCPeerConnection pc?: RTCPeerConnection
unreliableDataChannel?: RTCDataChannel unreliableDataChannel?: RTCDataChannel
@ -195,7 +211,12 @@ class EngineConnection {
} }
} }
this._state = next this._state = next
this.onConnectionStateChange(this._state)
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.ConnectionStateChanged, {
detail: this._state,
})
)
} }
private failedConnTimeout: Timeout | null private failedConnTimeout: Timeout | null
@ -203,74 +224,39 @@ class EngineConnection {
readonly url: string readonly url: string
private readonly token?: string private readonly token?: string
// For now, this is only used by the NetworkHealthIndicator.
// We can eventually use it for more, but one step at a time.
private onConnectionStateChange: (state: EngineConnectionState) => void
// These are used for the EngineCommandManager and were created
// before onConnectionStateChange existed.
private onEngineConnectionOpen: (engineConnection: EngineConnection) => void
private onConnectionStarted: (engineConnection: EngineConnection) => void
private onClose: (engineConnection: EngineConnection) => void
private onNewTrack: (track: NewTrackArgs) => void
// TODO: actual type is ClientMetrics // TODO: actual type is ClientMetrics
private webrtcStatsCollector?: () => Promise<ClientMetrics> private webrtcStatsCollector?: () => Promise<ClientMetrics>
constructor({ private pingPongSpan: { ping?: Date; pong?: Date }
url,
token, constructor({ url, token }: { url: string; token?: string }) {
onConnectionStateChange = () => {}, super()
onNewTrack = () => {},
onEngineConnectionOpen = () => {},
onConnectionStarted = () => {},
onClose = () => {},
}: {
url: string
token?: string
onConnectionStateChange?: (state: EngineConnectionState) => void
onEngineConnectionOpen?: (engineConnection: EngineConnection) => void
onConnectionStarted?: (engineConnection: EngineConnection) => void
onClose?: (engineConnection: EngineConnection) => void
onNewTrack?: (track: NewTrackArgs) => void
}) {
this.url = url this.url = url
this.token = token this.token = token
this.failedConnTimeout = null this.failedConnTimeout = null
this.onConnectionStateChange = onConnectionStateChange
this.onEngineConnectionOpen = onEngineConnectionOpen
this.onConnectionStarted = onConnectionStarted
this.onClose = onClose this.pingPongSpan = { ping: undefined, pong: undefined }
this.onNewTrack = onNewTrack
// TODO(paultag): This ought to be tweakable.
const pingIntervalMs = 10000
// Without an interval ping, our connection will timeout. // Without an interval ping, our connection will timeout.
let pingInterval = setInterval(() => { setInterval(() => {
switch (this.state.type as EngineConnectionStateType) { switch (this.state.type as EngineConnectionStateType) {
case EngineConnectionStateType.ConnectionEstablished: case EngineConnectionStateType.ConnectionEstablished:
this.send({ type: 'ping' }) this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date()
break break
case EngineConnectionStateType.Disconnecting: case EngineConnectionStateType.Disconnecting:
case EngineConnectionStateType.Disconnected: case EngineConnectionStateType.Disconnected:
clearInterval(pingInterval) // Reconnect if we have disconnected.
if (!this.isConnecting()) this.connect()
break break
default: default:
if (this.isConnecting()) break
// Means we never could do an initial connection. Reconnect everything.
if (!this.pingPongSpan.ping) this.connect()
break break
} }
}, pingIntervalMs) }, pingIntervalMs)
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS
let connectRetryInterval = setInterval(() => {
if (this.state.type !== EngineConnectionStateType.Disconnected) return
// Only try reconnecting when completely disconnected.
clearInterval(connectRetryInterval)
console.log('Trying to reconnect')
this.connect()
}, connectionTimeoutMs)
} }
isConnecting() { isConnecting() {
@ -352,7 +338,11 @@ class EngineConnection {
// dance is it safest to connect the video tracks / stream // dance is it safest to connect the video tracks / stream
case 'connected': case 'connected':
// Let the browser attach to the video stream now // Let the browser attach to the video stream now
this.onNewTrack({ conn: this, mediaStream: this.mediaStream! }) this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.NewTrack, {
detail: { conn: this, mediaStream: this.mediaStream! },
})
)
break break
case 'failed': case 'failed':
this.disconnectAll() this.disconnectAll()
@ -468,7 +458,9 @@ class EngineConnection {
// Everything is now connected. // Everything is now connected.
this.state = { type: EngineConnectionStateType.ConnectionEstablished } this.state = { type: EngineConnectionStateType.ConnectionEstablished }
this.onEngineConnectionOpen(this) this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Opened, { detail: this })
)
}) })
this.unreliableDataChannel.addEventListener('close', (event) => { this.unreliableDataChannel.addEventListener('close', (event) => {
@ -510,6 +502,10 @@ class EngineConnection {
}, },
} }
// Send an initial ping
this.send({ type: 'ping' })
this.pingPongSpan.ping = new Date()
// This is required for when KCMA is running stand-alone / within Tauri. // This is required for when KCMA is running stand-alone / within Tauri.
// Otherwise when run in a browser, the token is sent implicitly via // Otherwise when run in a browser, the token is sent implicitly via
// the Cookie header. // the Cookie header.
@ -575,12 +571,34 @@ failed cmd type was ${artifactThatFailed?.commandType}`
let resp = message.resp let resp = message.resp
// If there's no body to the response, we can bail here. // If there's no body to the response, we can bail here.
// !resp.type is usually "pong" response for our "ping"
if (!resp || !resp.type) { if (!resp || !resp.type) {
return return
} }
switch (resp.type) { switch (resp.type) {
case 'pong':
this.pingPongSpan.pong = new Date()
if (this.pingPongSpan.ping && this.pingPongSpan.pong) {
if (
Math.abs(
this.pingPongSpan.pong.valueOf() -
this.pingPongSpan.ping.valueOf()
) >= pingIntervalMs
) {
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
detail: 'BAD',
})
)
} else {
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
detail: 'OK',
})
)
}
}
break
case 'ice_server_info': case 'ice_server_info':
let ice_servers = resp.data?.ice_servers let ice_servers = resp.data?.ice_servers
@ -727,27 +745,11 @@ failed cmd type was ${artifactThatFailed?.commandType}`
} }
}) })
const connectionTimeoutMs = VITE_KC_CONNECTION_TIMEOUT_MS this.dispatchEvent(
if (this.failedConnTimeout) { new CustomEvent(EngineConnectionEvents.ConnectionStarted, {
clearTimeout(this.failedConnTimeout) detail: this,
this.failedConnTimeout = null })
} )
this.failedConnTimeout = setTimeout(() => {
if (this.isReady()) {
return
}
this.failedConnTimeout = null
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Timeout,
},
}
this.disconnectAll()
this.finalizeIfAllConnectionsClosed()
}, connectionTimeoutMs)
this.onConnectionStarted(this)
} }
unreliableSend(message: object | string) { unreliableSend(message: object | string) {
// TODO(paultag): Add in logic to determine the connection state and // TODO(paultag): Add in logic to determine the connection state and
@ -796,6 +798,8 @@ interface UnreliableSubscription<T extends UnreliableResponses['type']> {
callback: (data: Extract<UnreliableResponses, { type: T }>) => void callback: (data: Extract<UnreliableResponses, { type: T }>) => void
} }
// TODO: Should eventually be replaced with native EventTarget event system,
// as it manages events in a more familiar way to other developers.
export interface Subscription<T extends ModelTypes> { export interface Subscription<T extends ModelTypes> {
event: T event: T
callback: ( callback: (
@ -823,7 +827,11 @@ export type CommandLog =
data: null data: null
} }
export class EngineCommandManager { export enum EngineCommandManagerEvents {
EngineAvailable = 'engine-available',
}
export class EngineCommandManager extends EventTarget {
artifactMap: ArtifactMap = {} artifactMap: ArtifactMap = {}
lastArtifactMap: ArtifactMap = {} lastArtifactMap: ArtifactMap = {}
sceneCommandArtifacts: ArtifactMap = {} sceneCommandArtifacts: ArtifactMap = {}
@ -857,10 +865,9 @@ export class EngineCommandManager {
} }
} = {} as any } = {} as any
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
[]
constructor() { constructor() {
super()
this.engineConnection = undefined this.engineConnection = undefined
;(async () => { ;(async () => {
// circular dependency needs one to be lazy loaded // circular dependency needs one to be lazy loaded
@ -901,12 +908,17 @@ export class EngineCommandManager {
this.engineConnection = new EngineConnection({ this.engineConnection = new EngineConnection({
url, url,
token, token,
onConnectionStateChange: (state: EngineConnectionState) => { })
for (let cb of this.callbacksEngineStateConnection) {
cb(state) this.dispatchEvent(
} new CustomEvent(EngineCommandManagerEvents.EngineAvailable, {
}, detail: this.engineConnection,
onEngineConnectionOpen: () => { })
)
this.engineConnection.addEventListener(
EngineConnectionEvents.Opened,
() => {
// Make the axis gizmo. // Make the axis gizmo.
// We do this after the connection opened to avoid a race condition. // We do this after the connection opened to avoid a race condition.
// Connected opened is the last thing that happens when the stream // Connected opened is the last thing that happens when the stream
@ -941,78 +953,98 @@ export class EngineCommandManager {
setIsStreamReady(true) setIsStreamReady(true)
executeCode(undefined, true) executeCode(undefined, true)
}) })
}, }
onClose: () => { )
setIsStreamReady(false)
},
onConnectionStarted: (engineConnection) => {
engineConnection?.pc?.addEventListener('datachannel', (event) => {
let unreliableDataChannel = event.channel
unreliableDataChannel.addEventListener('message', (event) => { this.engineConnection.addEventListener(
const result: UnreliableResponses = JSON.parse(event.data) EngineConnectionEvents.Closed,
Object.values( () => {
this.unreliableSubscriptions[result.type] || {} setIsStreamReady(false)
).forEach( }
// TODO: There is only one response that uses the unreliable channel atm, )
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence this.engineConnection.addEventListener(
// per unreliable subscription. EngineConnectionEvents.ConnectionStarted,
(callback) => { (({ detail: engineConnection }: CustomEvent) => {
if ( engineConnection?.pc?.addEventListener(
result?.data?.sequence && 'datachannel',
result?.data.sequence > this.inSequence && (event: RTCDataChannelEvent) => {
result.type === 'highlight_set_entity' let unreliableDataChannel = event.channel
) {
this.inSequence = result.data.sequence unreliableDataChannel.addEventListener(
callback(result) 'message',
} (event: MessageEvent) => {
const result: UnreliableResponses = JSON.parse(event.data)
Object.values(
this.unreliableSubscriptions[result.type] || {}
).forEach(
// TODO: There is only one response that uses the unreliable channel atm,
// highlight_set_entity, if there are more it's likely they will all have the same
// sequence logic, but I'm not sure if we use a single global sequence or a sequence
// per unreliable subscription.
(callback) => {
if (
result?.data?.sequence &&
result?.data.sequence > this.inSequence &&
result.type === 'highlight_set_entity'
) {
this.inSequence = result.data.sequence
callback(result)
}
}
)
} }
) )
}) }
}) )
// When the EngineConnection starts a connection, we want to register // When the EngineConnection starts a connection, we want to register
// callbacks into the WebSocket/PeerConnection. // callbacks into the WebSocket/PeerConnection.
engineConnection.websocket?.addEventListener('message', (event) => { engineConnection.websocket?.addEventListener(
if (event.data instanceof ArrayBuffer) { 'message',
// If the data is an ArrayBuffer, it's the result of an export command, (event: MessageEvent) => {
// because in all other cases we send JSON strings. But in the case of if (event.data instanceof ArrayBuffer) {
// export we send a binary blob. // If the data is an ArrayBuffer, it's the result of an export command,
// Pass this to our export function. // because in all other cases we send JSON strings. But in the case of
void exportSave(event.data) // export we send a binary blob.
} else { // Pass this to our export function.
const message: Models['WebSocketResponse_type'] = JSON.parse( void exportSave(event.data)
event.data } else {
) const message: Models['WebSocketResponse_type'] = JSON.parse(
if ( event.data
message.success && )
message.resp.type === 'modeling' && if (
message.request_id message.success &&
) { message.resp.type === 'modeling' &&
this.handleModelingCommand(message.resp, message.request_id) message.request_id
} else if ( ) {
!message.success && this.handleModelingCommand(message.resp, message.request_id)
message.request_id && } else if (
this.artifactMap[message.request_id] !message.success &&
) { message.request_id &&
this.handleFailedModelingCommand(message) this.artifactMap[message.request_id]
) {
this.handleFailedModelingCommand(message)
}
} }
} }
}) )
}, }) as EventListener
onNewTrack: ({ mediaStream }) => { )
console.log('received track', mediaStream)
mediaStream.getVideoTracks()[0].addEventListener('mute', () => { this.engineConnection.addEventListener(EngineConnectionEvents.NewTrack, (({
console.log('peer is not sending video to us') detail: { mediaStream },
// this.engineConnection?.close() }: CustomEvent) => {
// this.engineConnection?.connect() console.log('received track', mediaStream)
})
setMediaStream(mediaStream) mediaStream.getVideoTracks()[0].addEventListener('mute', () => {
}, console.log('peer is not sending video to us')
}) // this.engineConnection?.close()
// this.engineConnection?.connect()
})
setMediaStream(mediaStream)
}) as EventListener)
this.engineConnection?.connect() this.engineConnection?.connect()
} }
@ -1202,9 +1234,6 @@ export class EngineCommandManager {
) { ) {
delete this.unreliableSubscriptions[event][id] delete this.unreliableSubscriptions[event][id]
} }
onConnectionStateChange(callback: (state: EngineConnectionState) => void) {
this.callbacksEngineStateConnection.push(callback)
}
endSession() { endSession() {
// TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)` // TODO: instead of sending a single command with `object_ids: Object.keys(this.artifactMap)`
// we need to loop over them each individually because if the engine doesn't recognise a single // we need to loop over them each individually because if the engine doesn't recognise a single