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