Refactor to use Context API for network status
This commit is contained in:
@ -12,6 +12,8 @@ import SignIn from './routes/SignIn'
|
||||
import { Auth } from './Auth'
|
||||
import { isTauri } from './lib/isTauri'
|
||||
import Home from './routes/Home'
|
||||
import { NetworkContext } from './hooks/useNetworkContext'
|
||||
import { useNetworkStatus } from './hooks/useNetworkStatus'
|
||||
import makeUrlPathRelative from './lib/makeUrlPathRelative'
|
||||
import DownloadAppBanner from 'components/DownloadAppBanner'
|
||||
import { WasmErrBanner } from 'components/WasmErrBanner'
|
||||
@ -155,5 +157,9 @@ const router = createBrowserRouter([
|
||||
* @returns RouterProvider
|
||||
*/
|
||||
export const Router = () => {
|
||||
return <RouterProvider router={router} />
|
||||
const networkStatus = useNetworkStatus()
|
||||
|
||||
return <NetworkContext.Provider value={networkStatus}>
|
||||
<RouterProvider router={router} />
|
||||
</NetworkContext.Provider>
|
||||
}
|
||||
|
||||
@ -3,13 +3,11 @@ import { isCursorInSketchCommandRange } from 'lang/util'
|
||||
import { engineCommandManager, kclManager } from 'lib/singletons'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { ActionButton } from 'components/ActionButton'
|
||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import {
|
||||
NetworkHealthState,
|
||||
useNetworkStatus,
|
||||
} from 'components/NetworkHealthIndicator'
|
||||
import { useStore } from 'useStore'
|
||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
@ -38,8 +36,7 @@ export function Toolbar({
|
||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||
|
||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { overallState } = useNetworkContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useStore((s) => ({
|
||||
isStreamReady: s.isStreamReady,
|
||||
|
||||
@ -1,41 +1,52 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// import {
|
||||
// ConnectingType,
|
||||
// ConnectingTypeGroup,
|
||||
// DisconnectingType,
|
||||
// EngineCommandManagerEvents,
|
||||
// EngineConnectionEvents,
|
||||
// EngineConnectionStateType,
|
||||
// ErrorType,
|
||||
// initialConnectingTypeGroupState,
|
||||
// } from '../lang/std/engineConnection'
|
||||
// import { engineCommandManager } from '../lib/singletons'
|
||||
import {
|
||||
EngineCommandManagerEvents,
|
||||
EngineConnectionEvents,
|
||||
ConnectionError,
|
||||
CONNECTION_ERROR_TEXT,
|
||||
} from '../lang/std/engineConnection'
|
||||
|
||||
// Sorted by severity
|
||||
enum Error {
|
||||
Unset = 0,
|
||||
LongLoadingTime,
|
||||
BadAuthToken,
|
||||
TooManyConnections,
|
||||
}
|
||||
|
||||
const errorText: Record<Error, string> = {
|
||||
[Error.Unset]: "",
|
||||
[Error.LongLoadingTime]: "Loading is taking longer than expected...",
|
||||
[Error.BadAuthToken]: "Your authorization token is not valid; please login again.",
|
||||
[Error.TooManyConnections]: "There are too many connections.",
|
||||
}
|
||||
import { engineCommandManager } from '../lib/singletons'
|
||||
|
||||
const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
const [error, setError] = useState<Error>(Error.Unset)
|
||||
const [error, setError] = useState<ConnectionError>(ConnectionError.Unset)
|
||||
|
||||
useEffect(() => {
|
||||
const onConnectionStateChange = (state: EngineConnectionState) => {
|
||||
console.log("<Loading/>", state)
|
||||
}
|
||||
|
||||
const onEngineAvailable = ({ detail: engineConnection }: CustomEvent) => {
|
||||
engineConnection.addEventListener(
|
||||
EngineConnectionEvents.ConnectionStateChanged,
|
||||
onConnectionStateChange as EventListener
|
||||
)
|
||||
}
|
||||
|
||||
engineCommandManager.addEventListener(
|
||||
EngineCommandManagerEvents.EngineAvailable,
|
||||
onEngineAvailable as EventListener
|
||||
)
|
||||
|
||||
return () => {
|
||||
engineCommandManager.removeEventListener(
|
||||
EngineCommandManagerEvents.EngineAvailable,
|
||||
onEngineAvailable as EventListener
|
||||
)
|
||||
engineCommandManager.engineConnection?.removeEventListener(
|
||||
EngineConnectionEvents.ConnectionStateChanged,
|
||||
onConnectionStateChange as EventListener
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Don't set long loading time if there's a more severe error
|
||||
if (error > Error.LongLoadingTime) return
|
||||
if (error > ConnectionError.LongLoadingTime) return
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setError(Error.LongLoadingTime)
|
||||
setError(ConnectionError.LongLoadingTime)
|
||||
}, 4000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
@ -61,10 +72,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
||||
<p
|
||||
className={
|
||||
'text-sm mt-4 text-primary/60 transition-opacity duration-500' +
|
||||
(error !== Error.Unset ? ' opacity-100' : ' opacity-0')
|
||||
(error !== ConnectionError.Unset ? ' opacity-100' : ' opacity-0')
|
||||
}
|
||||
>
|
||||
{ errorText[error] }
|
||||
{ CONNECTION_ERROR_TEXT[error] }
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -33,6 +33,9 @@ import {
|
||||
syntaxHighlighting,
|
||||
defaultHighlightStyle,
|
||||
} from '@codemirror/language'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import interact from '@replit/codemirror-interact'
|
||||
import { kclManager, editorManager, codeManager } from 'lib/singletons'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
@ -63,6 +66,9 @@ export const KclEditorPane = () => {
|
||||
? getSystemTheme()
|
||||
: context.app.theme.current
|
||||
const { copilotLSP, kclLSP } = useLspContext()
|
||||
const { overallState } = useNetworkContext()
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
@ -1,24 +1,9 @@
|
||||
import { Popover } from '@headlessui/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
||||
import {
|
||||
ConnectingType,
|
||||
ConnectingTypeGroup,
|
||||
DisconnectingType,
|
||||
EngineCommandManagerEvents,
|
||||
EngineConnectionEvents,
|
||||
EngineConnectionStateType,
|
||||
ErrorType,
|
||||
initialConnectingTypeGroupState,
|
||||
} from '../lang/std/engineConnection'
|
||||
import { engineCommandManager } from '../lib/singletons'
|
||||
import Tooltip from './Tooltip'
|
||||
|
||||
export enum NetworkHealthState {
|
||||
Ok,
|
||||
Issue,
|
||||
Disconnected,
|
||||
}
|
||||
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
||||
import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||
|
||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||
[NetworkHealthState.Ok]: 'Connected',
|
||||
@ -81,198 +66,6 @@ const overallConnectionStateIcon: Record<
|
||||
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
|
||||
}
|
||||
|
||||
export function useNetworkStatus() {
|
||||
const [steps, setSteps] = useState(
|
||||
structuredClone(initialConnectingTypeGroupState)
|
||||
)
|
||||
const [internetConnected, setInternetConnected] = useState<boolean>(true)
|
||||
const [overallState, setOverallState] = useState<NetworkHealthState>(
|
||||
NetworkHealthState.Disconnected
|
||||
)
|
||||
const [pingPongHealth, setPingPongHealth] = useState<'OK' | 'BAD'>('BAD')
|
||||
const [hasCopied, setHasCopied] = useState<boolean>(false)
|
||||
|
||||
const [error, setError] = useState<ErrorType | undefined>(undefined)
|
||||
|
||||
const hasIssue = (i: [ConnectingType, boolean | undefined]) =>
|
||||
i[1] === undefined ? i[1] : !i[1]
|
||||
|
||||
const [issues, setIssues] = useState<
|
||||
Record<ConnectingTypeGroup, boolean | undefined>
|
||||
>({
|
||||
[ConnectingTypeGroup.WebSocket]: undefined,
|
||||
[ConnectingTypeGroup.ICE]: undefined,
|
||||
[ConnectingTypeGroup.WebRTC]: undefined,
|
||||
})
|
||||
|
||||
const [hasIssues, setHasIssues] = useState<boolean | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
setOverallState(
|
||||
!internetConnected
|
||||
? NetworkHealthState.Disconnected
|
||||
: hasIssues || hasIssues === undefined
|
||||
? NetworkHealthState.Issue
|
||||
: NetworkHealthState.Ok
|
||||
)
|
||||
}, [hasIssues, internetConnected])
|
||||
|
||||
useEffect(() => {
|
||||
const onlineCallback = () => {
|
||||
setSteps(initialConnectingTypeGroupState)
|
||||
setInternetConnected(true)
|
||||
}
|
||||
const offlineCallback = () => {
|
||||
setInternetConnected(false)
|
||||
}
|
||||
window.addEventListener('online', onlineCallback)
|
||||
window.addEventListener('offline', offlineCallback)
|
||||
return () => {
|
||||
window.removeEventListener('online', onlineCallback)
|
||||
window.removeEventListener('offline', offlineCallback)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log(pingPongHealth)
|
||||
}, [pingPongHealth])
|
||||
|
||||
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 (
|
||||
engineConnectionState.type === EngineConnectionStateType.Connecting
|
||||
) {
|
||||
const groups = Object.values(nextSteps)
|
||||
for (let group of groups) {
|
||||
for (let step of group) {
|
||||
if (step[0] !== engineConnectionState.value.type) continue
|
||||
step[1] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
engineConnectionState.type === EngineConnectionStateType.Disconnecting
|
||||
) {
|
||||
const groups = Object.values(nextSteps)
|
||||
for (let group of groups) {
|
||||
for (let step of group) {
|
||||
if (
|
||||
engineConnectionState.value.type === DisconnectingType.Error
|
||||
) {
|
||||
if (
|
||||
engineConnectionState.value.value.lastConnectingValue
|
||||
?.type === step[0]
|
||||
) {
|
||||
step[1] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (engineConnectionState.value.type === DisconnectingType.Error) {
|
||||
setError(engineConnectionState.value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the state of all steps if we have disconnected.
|
||||
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 {
|
||||
hasIssues,
|
||||
overallState,
|
||||
internetConnected,
|
||||
steps,
|
||||
issues,
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
pingPongHealth,
|
||||
}
|
||||
}
|
||||
|
||||
export const NetworkHealthIndicator = () => {
|
||||
const {
|
||||
hasIssues,
|
||||
@ -283,7 +76,7 @@ export const NetworkHealthIndicator = () => {
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
} = useNetworkStatus()
|
||||
} = useNetworkContext()
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
|
||||
@ -4,8 +4,9 @@ import { getNormalisedCoordinates } from '../lib/utils'
|
||||
import Loading from './Loading'
|
||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||
import { useModelingContext } from 'hooks/useModelingContext'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
||||
import { butName } from 'lib/cameraControls'
|
||||
import { sendSelectEventToEngine } from 'lib/selections'
|
||||
|
||||
@ -28,7 +29,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
||||
}))
|
||||
const { settings } = useSettingsAuthContext()
|
||||
const { state } = useModelingContext()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { overallState } = useNetworkContext()
|
||||
|
||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||
|
||||
|
||||
6
src/hooks/useNetworkContext.tsx
Normal file
6
src/hooks/useNetworkContext.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
|
||||
export const NetworkContext = createContext(null)
|
||||
export const useNetworkContext = () => {
|
||||
return useContext(NetworkContext)
|
||||
}
|
||||
214
src/hooks/useNetworkStatus.tsx
Normal file
214
src/hooks/useNetworkStatus.tsx
Normal file
@ -0,0 +1,214 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
ConnectingType,
|
||||
ConnectingTypeGroup,
|
||||
DisconnectingType,
|
||||
EngineCommandManagerEvents,
|
||||
EngineConnectionEvents,
|
||||
EngineConnectionStateType,
|
||||
ErrorType,
|
||||
initialConnectingTypeGroupState,
|
||||
} from '../lang/std/engineConnection'
|
||||
import { engineCommandManager } from '../lib/singletons'
|
||||
|
||||
export enum NetworkHealthState {
|
||||
Ok,
|
||||
Issue,
|
||||
Disconnected,
|
||||
}
|
||||
|
||||
// Must be called from one place in the application.
|
||||
// We've chosen the <Router /> component for this.
|
||||
export function useNetworkStatus() {
|
||||
const [steps, setSteps] = useState(
|
||||
structuredClone(initialConnectingTypeGroupState)
|
||||
)
|
||||
const [internetConnected, setInternetConnected] = useState<boolean>(true)
|
||||
const [overallState, setOverallState] = useState<NetworkHealthState>(
|
||||
NetworkHealthState.Disconnected
|
||||
)
|
||||
const [pingPongHealth, setPingPongHealth] = useState<undefined | 'OK' | 'TIMEOUT'>(undefined)
|
||||
const [hasCopied, setHasCopied] = useState<boolean>(false)
|
||||
|
||||
const [error, setError] = useState<ErrorType | undefined>(undefined)
|
||||
|
||||
const hasIssue = (i: [ConnectingType, boolean | undefined]) =>
|
||||
i[1] === undefined ? i[1] : !i[1]
|
||||
|
||||
const [issues, setIssues] = useState<
|
||||
Record<ConnectingTypeGroup, boolean | undefined>
|
||||
>({
|
||||
[ConnectingTypeGroup.WebSocket]: undefined,
|
||||
[ConnectingTypeGroup.ICE]: undefined,
|
||||
[ConnectingTypeGroup.WebRTC]: undefined,
|
||||
})
|
||||
|
||||
const [hasIssues, setHasIssues] = useState<boolean | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
setOverallState(
|
||||
!internetConnected
|
||||
? NetworkHealthState.Disconnected
|
||||
: hasIssues || hasIssues === undefined
|
||||
? NetworkHealthState.Issue
|
||||
: NetworkHealthState.Ok
|
||||
)
|
||||
}, [hasIssues, internetConnected])
|
||||
|
||||
useEffect(() => {
|
||||
const onlineCallback = () => {
|
||||
setSteps(initialConnectingTypeGroupState)
|
||||
setInternetConnected(true)
|
||||
}
|
||||
const offlineCallback = () => {
|
||||
setInternetConnected(false)
|
||||
}
|
||||
window.addEventListener('online', onlineCallback)
|
||||
window.addEventListener('offline', offlineCallback)
|
||||
console.log("useNetworkStatus initialized")
|
||||
return () => {
|
||||
window.removeEventListener('online', onlineCallback)
|
||||
window.removeEventListener('offline', offlineCallback)
|
||||
console.log("useNetworkStatus teardown")
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log("pingPongHealth", pingPongHealth)
|
||||
}, [pingPongHealth])
|
||||
|
||||
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 (
|
||||
engineConnectionState.type === EngineConnectionStateType.Connecting
|
||||
) {
|
||||
const groups = Object.values(nextSteps)
|
||||
for (let group of groups) {
|
||||
for (let step of group) {
|
||||
if (step[0] !== engineConnectionState.value.type) continue
|
||||
step[1] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
engineConnectionState.type === EngineConnectionStateType.Disconnecting
|
||||
) {
|
||||
const groups = Object.values(nextSteps)
|
||||
for (let group of groups) {
|
||||
for (let step of group) {
|
||||
if (
|
||||
engineConnectionState.value.type === DisconnectingType.Error
|
||||
) {
|
||||
if (
|
||||
engineConnectionState.value.value.lastConnectingValue
|
||||
?.type === step[0]
|
||||
) {
|
||||
step[1] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (engineConnectionState.value.type === DisconnectingType.Error) {
|
||||
setError(engineConnectionState.value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the state of all steps if we have disconnected.
|
||||
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 {
|
||||
hasIssues,
|
||||
overallState,
|
||||
internetConnected,
|
||||
steps,
|
||||
issues,
|
||||
error,
|
||||
setHasCopied,
|
||||
hasCopied,
|
||||
pingPongHealth,
|
||||
}
|
||||
}
|
||||
@ -7,12 +7,10 @@ import { authMachine } from 'machines/authMachine'
|
||||
import { settingsMachine } from 'machines/settingsMachine'
|
||||
import { homeMachine } from 'machines/homeMachine'
|
||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||
import {
|
||||
NetworkHealthState,
|
||||
useNetworkStatus,
|
||||
} from 'components/NetworkHealthIndicator'
|
||||
import { useKclContext } from 'lang/KclProvider'
|
||||
import { useStore } from 'useStore'
|
||||
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||
|
||||
// This might not be necessary, AnyStateMachine from xstate is working
|
||||
export type AllMachines =
|
||||
@ -47,12 +45,16 @@ export default function useStateMachineCommands<
|
||||
onCancel,
|
||||
}: UseStateMachineCommandsArgs<T, S>) {
|
||||
const { commandBarSend } = useCommandsContext()
|
||||
const { overallState } = useNetworkStatus()
|
||||
const { overallState } = useNetworkContext()
|
||||
const { isExecuting } = useKclContext()
|
||||
const { isStreamReady } = useStore((s) => ({
|
||||
isStreamReady: s.isStreamReady,
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
console.log("useStateMachineCommands initialized")
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const disableAllButtons =
|
||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||
|
||||
@ -113,9 +113,44 @@ export enum DisconnectingType {
|
||||
Quit = 'quit',
|
||||
}
|
||||
|
||||
// Sorted by severity
|
||||
export enum ConnectionError {
|
||||
Unset = 0,
|
||||
LongLoadingTime,
|
||||
|
||||
ICENegotiate,
|
||||
DataChannelError,
|
||||
WebSocketError,
|
||||
LocalDescriptionInvalid,
|
||||
|
||||
// These are more severe than protocol errors because they don't even allow
|
||||
// the program to do any protocol messages in the first place if they occur.
|
||||
BadAuthToken,
|
||||
TooManyConnections,
|
||||
|
||||
// An unknown error is the most severe because it has not been classified
|
||||
// or encountered before.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
export const CONNECTION_ERROR_TEXT: Record<ConnectionError, string> = {
|
||||
[ConnectionError.Unset]: "",
|
||||
[ConnectionError.LongLoadingTime]: "Loading is taking longer than expected...",
|
||||
[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.BadAuthToken]: "Your authorization token is not valid; please login again.",
|
||||
[ConnectionError.TooManyConnections]: "There are too many connections.",
|
||||
[ConnectionError.Unknown]: "An unexpected error occurred. Please report this to us.",
|
||||
}
|
||||
|
||||
export interface ErrorType {
|
||||
// We may not necessary have an error to assign.
|
||||
error?: Error
|
||||
// The error we've encountered.
|
||||
error: ConnectionError,
|
||||
|
||||
// Additional context.
|
||||
context?: string,
|
||||
|
||||
// We assign this in the state setter because we may have not failed at
|
||||
// a Connecting state, which we check for there.
|
||||
@ -130,7 +165,7 @@ export type DisconnectingValue =
|
||||
// These are ordered by the expected sequence.
|
||||
export enum ConnectingType {
|
||||
WebSocketConnecting = 'websocket-connecting',
|
||||
WebSocketEstablished = 'websocket-established',
|
||||
WebSocketOpen = 'websocket-open',
|
||||
PeerConnectionCreated = 'peer-connection-created',
|
||||
ICEServersSet = 'ice-servers-set',
|
||||
SetLocalDescription = 'set-local-description',
|
||||
@ -157,7 +192,7 @@ export const initialConnectingTypeGroupState: Record<
|
||||
> = {
|
||||
[ConnectingTypeGroup.WebSocket]: [
|
||||
[ConnectingType.WebSocketConnecting, undefined],
|
||||
[ConnectingType.WebSocketEstablished, undefined],
|
||||
[ConnectingType.WebSocketOpen, undefined],
|
||||
],
|
||||
[ConnectingTypeGroup.ICE]: [
|
||||
[ConnectingType.PeerConnectionCreated, undefined],
|
||||
@ -179,7 +214,7 @@ export const initialConnectingTypeGroupState: Record<
|
||||
|
||||
export type ConnectingValue =
|
||||
| State<ConnectingType.WebSocketConnecting, void>
|
||||
| State<ConnectingType.WebSocketEstablished, void>
|
||||
| State<ConnectingType.WebSocketOpen, void>
|
||||
| State<ConnectingType.PeerConnectionCreated, void>
|
||||
| State<ConnectingType.ICEServersSet, void>
|
||||
| State<ConnectingType.SetLocalDescription, void>
|
||||
@ -200,7 +235,7 @@ export type EngineConnectionState =
|
||||
| State<EngineConnectionStateType.Disconnecting, DisconnectingValue>
|
||||
| State<EngineConnectionStateType.Disconnected, void>
|
||||
|
||||
export type PingPongState = 'OK' | 'BAD'
|
||||
export type PingPongState = 'OK' | 'TIMEOUT'
|
||||
|
||||
export enum EngineConnectionEvents {
|
||||
// Fires for each ping-pong success or failure.
|
||||
@ -404,9 +439,8 @@ class EngineConnection extends EventTarget {
|
||||
value: {
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error: new Error(
|
||||
'failed to negotiate ice connection; restarting'
|
||||
),
|
||||
error: ConnectionError.ICENegotiate,
|
||||
context: event.toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -532,6 +566,8 @@ class EngineConnection extends EventTarget {
|
||||
})
|
||||
|
||||
this.unreliableDataChannel.addEventListener('close', (event) => {
|
||||
console.log("data channel close")
|
||||
|
||||
this.disconnectAll()
|
||||
this.finalizeIfAllConnectionsClosed()
|
||||
})
|
||||
@ -544,7 +580,8 @@ class EngineConnection extends EventTarget {
|
||||
value: {
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error: new Error(event.toString()),
|
||||
error: ConnectionError.DataChannelError,
|
||||
context: event.toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -589,7 +626,7 @@ class EngineConnection extends EventTarget {
|
||||
this.state = {
|
||||
type: EngineConnectionStateType.Connecting,
|
||||
value: {
|
||||
type: ConnectingType.WebSocketEstablished,
|
||||
type: ConnectingType.WebSocketOpen,
|
||||
},
|
||||
}
|
||||
|
||||
@ -609,6 +646,8 @@ class EngineConnection extends EventTarget {
|
||||
})
|
||||
|
||||
this.websocket.addEventListener('close', (event) => {
|
||||
console.log("websocket close")
|
||||
|
||||
this.disconnectAll()
|
||||
this.finalizeIfAllConnectionsClosed()
|
||||
})
|
||||
@ -621,7 +660,8 @@ class EngineConnection extends EventTarget {
|
||||
value: {
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error: new Error(event.toString()),
|
||||
error: ConnectionError.WebSocketError,
|
||||
context: event.toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -635,6 +675,8 @@ class EngineConnection extends EventTarget {
|
||||
// when assuming we're the only consumer or that all messages will
|
||||
// be carefully formatted here.
|
||||
|
||||
console.log("websocket message", event)
|
||||
|
||||
if (typeof event.data !== 'string') {
|
||||
return
|
||||
}
|
||||
@ -680,7 +722,7 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||
detail: 'BAD',
|
||||
detail: 'TIMEOUT',
|
||||
})
|
||||
)
|
||||
} else {
|
||||
@ -773,8 +815,7 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error(error)
|
||||
.catch((err: Error) => {
|
||||
// The local description is invalid, so there's no point continuing.
|
||||
this.disconnectAll()
|
||||
this.state = {
|
||||
@ -782,7 +823,8 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
||||
value: {
|
||||
type: DisconnectingType.Error,
|
||||
value: {
|
||||
error,
|
||||
error: ConnectionError.LocalDescriptionInvalid,
|
||||
context: err.toString(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -318,7 +318,6 @@ function resetAndSetEngineEntitySelectionCmds(
|
||||
selections: SelectionToEngine[]
|
||||
): Models['WebSocketRequest_type'][] {
|
||||
if (!engineCommandManager.engineConnection?.isReady()) {
|
||||
console.log('engine connection is not ready')
|
||||
return []
|
||||
}
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user