Try to avoid the black screen again & improve error messages (#7327)

* Fix the black screen of death

* fmt

* make check

* Clean up

* Fix up zoom to fit

* Change how emulateNetworkConditions work

* Do NOT use browser's offline/online mechanisms

* Fix test

* Improve network error messages

* Signal offline when failed event comes in

* Don't use logic on components that only want a loader

* Remove unnecessary pause state transition

---------

Co-authored-by: jacebrowning <jacebrowning@gmail.com>
This commit is contained in:
Zookeeper Lee
2025-06-04 13:59:22 -04:00
committed by GitHub
parent ff92c73ac4
commit 5ceb92d117
12 changed files with 330 additions and 183 deletions

View File

@ -1,4 +1,3 @@
import { TEST } from '@src/env'
import type { Models } from '@kittycad/lib'
import { VITE_KC_API_WS_MODELING_URL, VITE_KC_DEV_TOKEN } from '@src/env'
import { jsAppSettings } from '@src/lib/settings/settingsUtils'
@ -83,6 +82,9 @@ export enum ConnectionError {
TooManyConnections,
Outage,
// Observed to happen on a local network outage.
PeerConnectionRemoteDisconnected,
// An unknown error is the most severe because it has not been classified
// or encountered before.
Unknown,
@ -93,22 +95,18 @@ export const CONNECTION_ERROR_TEXT: Record<ConnectionError, string> = {
[ConnectionError.LongLoadingTime]:
'Loading is taking longer than expected...',
[ConnectionError.VeryLongLoadingTime]:
'Loading seems stuck. Do you have a firewall turned on?',
"It's possible there's a connection issue.",
[ConnectionError.ICENegotiate]: 'ICE negotiation failed.',
[ConnectionError.DataChannelError]: 'The data channel signaled an error.',
[ConnectionError.WebSocketError]: 'The websocket signaled an error.',
[ConnectionError.LocalDescriptionInvalid]:
'The local description is invalid.',
[ConnectionError.MissingAuthToken]:
'Your authorization token is missing; please login again.',
[ConnectionError.BadAuthToken]:
'Your authorization token is invalid; please login again.',
[ConnectionError.TooManyConnections]:
'There are too many open engine connections associated with your account.',
[ConnectionError.Outage]:
'We seem to be experiencing an outage. Please visit [status.zoo.dev](https://status.zoo.dev) for updates.',
[ConnectionError.Unknown]:
'An unexpected error occurred. Please report this to us.',
[ConnectionError.DataChannelError]: 'Data channel error.',
[ConnectionError.WebSocketError]: 'Websocket error.',
[ConnectionError.LocalDescriptionInvalid]: 'Local description invalid',
[ConnectionError.MissingAuthToken]: 'Missing authorization token',
[ConnectionError.BadAuthToken]: 'Bad authorization token',
[ConnectionError.TooManyConnections]: 'Too many connections',
[ConnectionError.Outage]: 'Outage',
[ConnectionError.PeerConnectionRemoteDisconnected]:
'Peer connection disconnected',
[ConnectionError.Unknown]: 'Unknown',
}
export const WEBSOCKET_READYSTATE_TEXT: Record<number, string> = {
@ -226,6 +224,9 @@ export enum EngineConnectionEvents {
Opened = 'opened', // (engineConnection: EngineConnection) => void
Closed = 'closed', // (engineConnection: EngineConnection) => void
NewTrack = 'new-track', // (track: NewTrackArgs) => void
// A general offline state.
Offline = 'offline',
}
function toRTCSessionDescriptionInit(
@ -669,14 +670,28 @@ class EngineConnection extends EventTarget {
},
},
}
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Offline, {})
)
this.disconnectAll()
break
// The remote end broke up with us! :(
case 'disconnected':
this.state = {
type: EngineConnectionStateType.Disconnecting,
value: {
type: DisconnectingType.Error,
value: {
error: ConnectionError.PeerConnectionRemoteDisconnected,
context: event,
},
},
}
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.RestartRequest, {})
new CustomEvent(EngineConnectionEvents.Offline, {})
)
this.disconnectAll()
break
case 'closed':
this.pc?.removeEventListener('icecandidate', this.onIceCandidate)
@ -847,7 +862,6 @@ class EngineConnection extends EventTarget {
'message',
this.onDataChannelMessage
)
this.disconnectAll()
}
this.unreliableDataChannel?.addEventListener(
@ -866,7 +880,6 @@ class EngineConnection extends EventTarget {
},
},
}
this.disconnectAll()
}
this.unreliableDataChannel?.addEventListener(
'error',
@ -956,6 +969,9 @@ class EngineConnection extends EventTarget {
this.onNetworkStatusReady
)
this.dispatchEvent(
new CustomEvent(EngineConnectionEvents.Offline, {})
)
this.disconnectAll()
}
this.websocket.addEventListener('close', this.onWebSocketClose)
@ -974,8 +990,6 @@ class EngineConnection extends EventTarget {
},
}
}
this.disconnectAll()
}
this.websocket.addEventListener('error', this.onWebSocketError)
@ -1331,6 +1345,9 @@ export enum EngineCommandManagerEvents {
// the whole scene is ready (settings loaded)
SceneReady = 'scene-ready',
// we're offline
Offline = 'offline',
}
/**
@ -1380,6 +1397,7 @@ export class EngineCommandManager extends EventTarget {
* This is compared to the {@link outSequence} number to determine if we should ignore
* any out-of-order late responses in the unreliable channel.
*/
keepForcefulOffline = false
inSequence = 1
engineConnection?: EngineConnection
commandLogs: CommandLog[] = []
@ -1453,13 +1471,8 @@ export class EngineCommandManager extends EventTarget {
)
}
private onOffline = () => {
console.log('Browser reported network is offline')
if (TEST) {
console.warn('DURING TESTS ENGINECONNECTION.ONOFFLINE WILL DO NOTHING.')
return
}
this.onEngineConnectionRestartRequest()
private onEngineOffline = () => {
this.dispatchEvent(new CustomEvent(EngineCommandManagerEvents.Offline, {}))
}
idleMode: boolean = false
@ -1494,6 +1507,11 @@ export class EngineCommandManager extends EventTarget {
if (settings) {
this.settings = settings
}
if (this.keepForcefulOffline) {
return
}
if (width === 0 || height === 0) {
return
}
@ -1509,8 +1527,6 @@ export class EngineCommandManager extends EventTarget {
return
}
window.addEventListener('offline', this.onOffline)
let additionalSettings = this.settings.enableSSAO ? '&post_effect=ssao' : ''
additionalSettings +=
'&show_grid=' + (this.settings.showScaleGrid ? 'true' : 'false')
@ -1537,6 +1553,11 @@ export class EngineCommandManager extends EventTarget {
this.onEngineConnectionRestartRequest as EventListener
)
this.engineConnection.addEventListener(
EngineConnectionEvents.Offline,
this.onEngineOffline as EventListener
)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.onEngineConnectionOpened = async () => {
console.log('onEngineConnectionOpened')
@ -1548,13 +1569,8 @@ export class EngineCommandManager extends EventTarget {
this.codeManager?.currentFilePath || undefined
)
} catch (e) {
// If this happens shit's actually gone south aka the websocket closed.
// Let's restart.
console.warn("shit's gone south")
console.warn(e)
this.engineConnection?.dispatchEvent(
new CustomEvent(EngineConnectionEvents.RestartRequest, {})
)
// If this happens, the websocket may have closed and we need to restart
console.warn('unknown error:', e)
return
}
@ -1597,23 +1613,7 @@ export class EngineCommandManager extends EventTarget {
console.log('camControlsCameraChange')
this._camControlsCameraChange()
// We should eventually only have 1 restoral call.
if (this.idleMode) {
await this.sceneInfra?.camControls.restoreRemoteCameraStateAndTriggerSync()
} else {
// NOTE: This code is old. It uses the old hack to restore camera.
console.log('call default_camera_get_settings')
// eslint-disable-next-line @typescript-eslint/no-floating-promises
await this.sendSceneCommand({
// CameraControls subscribes to default_camera_get_settings response events
// firing this at connection ensure the camera's are synced initially
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'default_camera_get_settings',
},
})
}
await this.sceneInfra?.camControls.restoreRemoteCameraStateAndTriggerSync()
setIsStreamReady(true)
@ -1877,8 +1877,6 @@ export class EngineCommandManager extends EventTarget {
tearDown(opts?: { idleMode: boolean }) {
this.idleMode = opts?.idleMode ?? false
window.removeEventListener('offline', this.onOffline)
if (this.engineConnection) {
for (const [cmdId, pending] of Object.entries(this.pendingCommands)) {
pending.reject([
@ -1928,7 +1926,26 @@ export class EngineCommandManager extends EventTarget {
this.engineCommandManager.engineConnection = null
}
this.engineConnection = undefined
// It is possible all connections never even started, but we still want
// to signal to the whole application we are "offline".
this.dispatchEvent(new CustomEvent(EngineCommandManagerEvents.Offline, {}))
}
offline() {
this.keepForcefulOffline = true
this.tearDown()
console.log('offline')
}
online() {
this.keepForcefulOffline = false
this.dispatchEvent(
new CustomEvent(EngineCommandManagerEvents.EngineRestartRequest, {})
)
console.log('online')
}
async startNewSession() {
this.responseMap = {}
}