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 { Auth } from './Auth'
|
||||||
import { isTauri } from './lib/isTauri'
|
import { isTauri } from './lib/isTauri'
|
||||||
import Home from './routes/Home'
|
import Home from './routes/Home'
|
||||||
|
import { NetworkContext } from './hooks/useNetworkContext'
|
||||||
|
import { useNetworkStatus } from './hooks/useNetworkStatus'
|
||||||
import makeUrlPathRelative from './lib/makeUrlPathRelative'
|
import makeUrlPathRelative from './lib/makeUrlPathRelative'
|
||||||
import DownloadAppBanner from 'components/DownloadAppBanner'
|
import DownloadAppBanner from 'components/DownloadAppBanner'
|
||||||
import { WasmErrBanner } from 'components/WasmErrBanner'
|
import { WasmErrBanner } from 'components/WasmErrBanner'
|
||||||
@ -155,5 +157,9 @@ const router = createBrowserRouter([
|
|||||||
* @returns RouterProvider
|
* @returns RouterProvider
|
||||||
*/
|
*/
|
||||||
export const Router = () => {
|
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 { engineCommandManager, kclManager } from 'lib/singletons'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { ActionButton } from 'components/ActionButton'
|
import { ActionButton } from 'components/ActionButton'
|
||||||
import { isSingleCursorInPipe } from 'lang/queryAst'
|
import { isSingleCursorInPipe } from 'lang/queryAst'
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import {
|
|
||||||
NetworkHealthState,
|
|
||||||
useNetworkStatus,
|
|
||||||
} from 'components/NetworkHealthIndicator'
|
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
import { ActionButtonDropdown } from 'components/ActionButtonDropdown'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
@ -38,8 +36,7 @@ export function Toolbar({
|
|||||||
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
}, [engineCommandManager.artifactMap, context.selectionRanges])
|
||||||
|
|
||||||
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
const toolbarButtonsRef = useRef<HTMLUListElement>(null)
|
||||||
|
const { overallState } = useNetworkContext()
|
||||||
const { overallState } = useNetworkStatus()
|
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useStore((s) => ({
|
const { isStreamReady } = useStore((s) => ({
|
||||||
isStreamReady: s.isStreamReady,
|
isStreamReady: s.isStreamReady,
|
||||||
|
|||||||
@ -1,41 +1,52 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
// import {
|
import {
|
||||||
// ConnectingType,
|
EngineCommandManagerEvents,
|
||||||
// ConnectingTypeGroup,
|
EngineConnectionEvents,
|
||||||
// DisconnectingType,
|
ConnectionError,
|
||||||
// EngineCommandManagerEvents,
|
CONNECTION_ERROR_TEXT,
|
||||||
// EngineConnectionEvents,
|
} from '../lang/std/engineConnection'
|
||||||
// EngineConnectionStateType,
|
|
||||||
// ErrorType,
|
|
||||||
// initialConnectingTypeGroupState,
|
|
||||||
// } from '../lang/std/engineConnection'
|
|
||||||
// import { engineCommandManager } from '../lib/singletons'
|
|
||||||
|
|
||||||
// Sorted by severity
|
import { engineCommandManager } from '../lib/singletons'
|
||||||
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.",
|
|
||||||
}
|
|
||||||
|
|
||||||
const Loading = ({ children }: React.PropsWithChildren) => {
|
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(() => {
|
useEffect(() => {
|
||||||
// Don't set long loading time if there's a more severe error
|
// 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(() => {
|
const timer = setTimeout(() => {
|
||||||
setError(Error.LongLoadingTime)
|
setError(ConnectionError.LongLoadingTime)
|
||||||
}, 4000)
|
}, 4000)
|
||||||
|
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
@ -61,10 +72,10 @@ const Loading = ({ children }: React.PropsWithChildren) => {
|
|||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
'text-sm mt-4 text-primary/60 transition-opacity duration-500' +
|
'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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -33,6 +33,9 @@ import {
|
|||||||
syntaxHighlighting,
|
syntaxHighlighting,
|
||||||
defaultHighlightStyle,
|
defaultHighlightStyle,
|
||||||
} from '@codemirror/language'
|
} 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 interact from '@replit/codemirror-interact'
|
||||||
import { kclManager, editorManager, codeManager } from 'lib/singletons'
|
import { kclManager, editorManager, codeManager } from 'lib/singletons'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
@ -63,6 +66,9 @@ export const KclEditorPane = () => {
|
|||||||
? getSystemTheme()
|
? getSystemTheme()
|
||||||
: context.app.theme.current
|
: context.app.theme.current
|
||||||
const { copilotLSP, kclLSP } = useLspContext()
|
const { copilotLSP, kclLSP } = useLspContext()
|
||||||
|
const { overallState } = useNetworkContext()
|
||||||
|
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
|
|||||||
@ -1,24 +1,9 @@
|
|||||||
import { Popover } from '@headlessui/react'
|
import { Popover } from '@headlessui/react'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { ActionIcon, ActionIconProps } from './ActionIcon'
|
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'
|
import Tooltip from './Tooltip'
|
||||||
|
import { ConnectingTypeGroup } from '../lang/std/engineConnection'
|
||||||
export enum NetworkHealthState {
|
import { useNetworkContext } from '../hooks/useNetworkContext'
|
||||||
Ok,
|
import { NetworkHealthState } from '../hooks/useNetworkStatus'
|
||||||
Issue,
|
|
||||||
Disconnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
|
||||||
[NetworkHealthState.Ok]: 'Connected',
|
[NetworkHealthState.Ok]: 'Connected',
|
||||||
@ -81,198 +66,6 @@ const overallConnectionStateIcon: Record<
|
|||||||
[NetworkHealthState.Disconnected]: 'networkCrossedOut',
|
[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 = () => {
|
export const NetworkHealthIndicator = () => {
|
||||||
const {
|
const {
|
||||||
hasIssues,
|
hasIssues,
|
||||||
@ -283,7 +76,7 @@ export const NetworkHealthIndicator = () => {
|
|||||||
error,
|
error,
|
||||||
setHasCopied,
|
setHasCopied,
|
||||||
hasCopied,
|
hasCopied,
|
||||||
} = useNetworkStatus()
|
} = useNetworkContext()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import { getNormalisedCoordinates } from '../lib/utils'
|
|||||||
import Loading from './Loading'
|
import Loading from './Loading'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useModelingContext } from 'hooks/useModelingContext'
|
import { useModelingContext } from 'hooks/useModelingContext'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
|
||||||
import { NetworkHealthState, useNetworkStatus } from './NetworkHealthIndicator'
|
|
||||||
import { butName } from 'lib/cameraControls'
|
import { butName } from 'lib/cameraControls'
|
||||||
import { sendSelectEventToEngine } from 'lib/selections'
|
import { sendSelectEventToEngine } from 'lib/selections'
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
|
|||||||
}))
|
}))
|
||||||
const { settings } = useSettingsAuthContext()
|
const { settings } = useSettingsAuthContext()
|
||||||
const { state } = useModelingContext()
|
const { state } = useModelingContext()
|
||||||
const { overallState } = useNetworkStatus()
|
const { overallState } = useNetworkContext()
|
||||||
|
|
||||||
const isNetworkOkay = overallState === NetworkHealthState.Ok
|
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 { settingsMachine } from 'machines/settingsMachine'
|
||||||
import { homeMachine } from 'machines/homeMachine'
|
import { homeMachine } from 'machines/homeMachine'
|
||||||
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
import { Command, CommandSetConfig, CommandSetSchema } from 'lib/commandTypes'
|
||||||
import {
|
|
||||||
NetworkHealthState,
|
|
||||||
useNetworkStatus,
|
|
||||||
} from 'components/NetworkHealthIndicator'
|
|
||||||
import { useKclContext } from 'lang/KclProvider'
|
import { useKclContext } from 'lang/KclProvider'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
|
import { useNetworkContext } from 'hooks/useNetworkContext'
|
||||||
|
import { NetworkHealthState } from 'hooks/useNetworkStatus'
|
||||||
|
|
||||||
// This might not be necessary, AnyStateMachine from xstate is working
|
// This might not be necessary, AnyStateMachine from xstate is working
|
||||||
export type AllMachines =
|
export type AllMachines =
|
||||||
@ -47,12 +45,16 @@ export default function useStateMachineCommands<
|
|||||||
onCancel,
|
onCancel,
|
||||||
}: UseStateMachineCommandsArgs<T, S>) {
|
}: UseStateMachineCommandsArgs<T, S>) {
|
||||||
const { commandBarSend } = useCommandsContext()
|
const { commandBarSend } = useCommandsContext()
|
||||||
const { overallState } = useNetworkStatus()
|
const { overallState } = useNetworkContext()
|
||||||
const { isExecuting } = useKclContext()
|
const { isExecuting } = useKclContext()
|
||||||
const { isStreamReady } = useStore((s) => ({
|
const { isStreamReady } = useStore((s) => ({
|
||||||
isStreamReady: s.isStreamReady,
|
isStreamReady: s.isStreamReady,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("useStateMachineCommands initialized")
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const disableAllButtons =
|
const disableAllButtons =
|
||||||
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
overallState !== NetworkHealthState.Ok || isExecuting || !isStreamReady
|
||||||
|
|||||||
@ -113,9 +113,44 @@ export enum DisconnectingType {
|
|||||||
Quit = 'quit',
|
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 {
|
export interface ErrorType {
|
||||||
// We may not necessary have an error to assign.
|
// The error we've encountered.
|
||||||
error?: Error
|
error: ConnectionError,
|
||||||
|
|
||||||
|
// Additional context.
|
||||||
|
context?: string,
|
||||||
|
|
||||||
// We assign this in the state setter because we may have not failed at
|
// We assign this in the state setter because we may have not failed at
|
||||||
// a Connecting state, which we check for there.
|
// a Connecting state, which we check for there.
|
||||||
@ -130,7 +165,7 @@ export type DisconnectingValue =
|
|||||||
// These are ordered by the expected sequence.
|
// These are ordered by the expected sequence.
|
||||||
export enum ConnectingType {
|
export enum ConnectingType {
|
||||||
WebSocketConnecting = 'websocket-connecting',
|
WebSocketConnecting = 'websocket-connecting',
|
||||||
WebSocketEstablished = 'websocket-established',
|
WebSocketOpen = 'websocket-open',
|
||||||
PeerConnectionCreated = 'peer-connection-created',
|
PeerConnectionCreated = 'peer-connection-created',
|
||||||
ICEServersSet = 'ice-servers-set',
|
ICEServersSet = 'ice-servers-set',
|
||||||
SetLocalDescription = 'set-local-description',
|
SetLocalDescription = 'set-local-description',
|
||||||
@ -157,7 +192,7 @@ export const initialConnectingTypeGroupState: Record<
|
|||||||
> = {
|
> = {
|
||||||
[ConnectingTypeGroup.WebSocket]: [
|
[ConnectingTypeGroup.WebSocket]: [
|
||||||
[ConnectingType.WebSocketConnecting, undefined],
|
[ConnectingType.WebSocketConnecting, undefined],
|
||||||
[ConnectingType.WebSocketEstablished, undefined],
|
[ConnectingType.WebSocketOpen, undefined],
|
||||||
],
|
],
|
||||||
[ConnectingTypeGroup.ICE]: [
|
[ConnectingTypeGroup.ICE]: [
|
||||||
[ConnectingType.PeerConnectionCreated, undefined],
|
[ConnectingType.PeerConnectionCreated, undefined],
|
||||||
@ -179,7 +214,7 @@ export const initialConnectingTypeGroupState: Record<
|
|||||||
|
|
||||||
export type ConnectingValue =
|
export type ConnectingValue =
|
||||||
| State<ConnectingType.WebSocketConnecting, void>
|
| State<ConnectingType.WebSocketConnecting, void>
|
||||||
| State<ConnectingType.WebSocketEstablished, void>
|
| State<ConnectingType.WebSocketOpen, void>
|
||||||
| State<ConnectingType.PeerConnectionCreated, void>
|
| State<ConnectingType.PeerConnectionCreated, void>
|
||||||
| State<ConnectingType.ICEServersSet, void>
|
| State<ConnectingType.ICEServersSet, void>
|
||||||
| State<ConnectingType.SetLocalDescription, void>
|
| State<ConnectingType.SetLocalDescription, void>
|
||||||
@ -200,7 +235,7 @@ 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 type PingPongState = 'OK' | 'TIMEOUT'
|
||||||
|
|
||||||
export enum EngineConnectionEvents {
|
export enum EngineConnectionEvents {
|
||||||
// Fires for each ping-pong success or failure.
|
// Fires for each ping-pong success or failure.
|
||||||
@ -404,9 +439,8 @@ class EngineConnection extends EventTarget {
|
|||||||
value: {
|
value: {
|
||||||
type: DisconnectingType.Error,
|
type: DisconnectingType.Error,
|
||||||
value: {
|
value: {
|
||||||
error: new Error(
|
error: ConnectionError.ICENegotiate,
|
||||||
'failed to negotiate ice connection; restarting'
|
context: event.toString(),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -532,6 +566,8 @@ class EngineConnection extends EventTarget {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.unreliableDataChannel.addEventListener('close', (event) => {
|
this.unreliableDataChannel.addEventListener('close', (event) => {
|
||||||
|
console.log("data channel close")
|
||||||
|
|
||||||
this.disconnectAll()
|
this.disconnectAll()
|
||||||
this.finalizeIfAllConnectionsClosed()
|
this.finalizeIfAllConnectionsClosed()
|
||||||
})
|
})
|
||||||
@ -544,7 +580,8 @@ class EngineConnection extends EventTarget {
|
|||||||
value: {
|
value: {
|
||||||
type: DisconnectingType.Error,
|
type: DisconnectingType.Error,
|
||||||
value: {
|
value: {
|
||||||
error: new Error(event.toString()),
|
error: ConnectionError.DataChannelError,
|
||||||
|
context: event.toString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -589,7 +626,7 @@ class EngineConnection extends EventTarget {
|
|||||||
this.state = {
|
this.state = {
|
||||||
type: EngineConnectionStateType.Connecting,
|
type: EngineConnectionStateType.Connecting,
|
||||||
value: {
|
value: {
|
||||||
type: ConnectingType.WebSocketEstablished,
|
type: ConnectingType.WebSocketOpen,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -609,6 +646,8 @@ class EngineConnection extends EventTarget {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.websocket.addEventListener('close', (event) => {
|
this.websocket.addEventListener('close', (event) => {
|
||||||
|
console.log("websocket close")
|
||||||
|
|
||||||
this.disconnectAll()
|
this.disconnectAll()
|
||||||
this.finalizeIfAllConnectionsClosed()
|
this.finalizeIfAllConnectionsClosed()
|
||||||
})
|
})
|
||||||
@ -621,7 +660,8 @@ class EngineConnection extends EventTarget {
|
|||||||
value: {
|
value: {
|
||||||
type: DisconnectingType.Error,
|
type: DisconnectingType.Error,
|
||||||
value: {
|
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
|
// when assuming we're the only consumer or that all messages will
|
||||||
// be carefully formatted here.
|
// be carefully formatted here.
|
||||||
|
|
||||||
|
console.log("websocket message", event)
|
||||||
|
|
||||||
if (typeof event.data !== 'string') {
|
if (typeof event.data !== 'string') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -680,7 +722,7 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
) {
|
) {
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
new CustomEvent(EngineConnectionEvents.PingPongChanged, {
|
||||||
detail: 'BAD',
|
detail: 'TIMEOUT',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -773,8 +815,7 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((error: Error) => {
|
.catch((err: Error) => {
|
||||||
console.error(error)
|
|
||||||
// The local description is invalid, so there's no point continuing.
|
// The local description is invalid, so there's no point continuing.
|
||||||
this.disconnectAll()
|
this.disconnectAll()
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -782,7 +823,8 @@ failed cmd type was ${artifactThatFailed?.commandType}`
|
|||||||
value: {
|
value: {
|
||||||
type: DisconnectingType.Error,
|
type: DisconnectingType.Error,
|
||||||
value: {
|
value: {
|
||||||
error,
|
error: ConnectionError.LocalDescriptionInvalid,
|
||||||
|
context: err.toString(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -318,7 +318,6 @@ function resetAndSetEngineEntitySelectionCmds(
|
|||||||
selections: SelectionToEngine[]
|
selections: SelectionToEngine[]
|
||||||
): Models['WebSocketRequest_type'][] {
|
): Models['WebSocketRequest_type'][] {
|
||||||
if (!engineCommandManager.engineConnection?.isReady()) {
|
if (!engineCommandManager.engineConnection?.isReady()) {
|
||||||
console.log('engine connection is not ready')
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
|||||||
Reference in New Issue
Block a user