connection-context.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import {
  2. createContext,
  3. useContext,
  4. useEffect,
  5. useState,
  6. FC,
  7. useCallback,
  8. useMemo,
  9. } from 'react'
  10. import {
  11. ConnectionError,
  12. ConnectionState,
  13. SocketDebuggingInfo,
  14. } from '../connection/types/connection-state'
  15. import {
  16. ConnectionManager,
  17. StateChangeEvent,
  18. } from '@/features/ide-react/connection/connection-manager'
  19. import { Socket } from '@/features/ide-react/connection/types/socket'
  20. import { secondsUntil } from '@/features/ide-react/connection/utils'
  21. import { useLocation } from '@/shared/hooks/use-location'
  22. type ConnectionContextValue = {
  23. socket: Socket
  24. connectionState: ConnectionState
  25. isConnected: boolean
  26. isStillReconnecting: boolean
  27. secondsUntilReconnect: () => number
  28. tryReconnectNow: () => void
  29. registerUserActivity: () => void
  30. closeConnection: (err: ConnectionError) => void
  31. getSocketDebuggingInfo: () => SocketDebuggingInfo
  32. }
  33. export const ConnectionContext = createContext<
  34. ConnectionContextValue | undefined
  35. >(undefined)
  36. export const ConnectionProvider: FC<React.PropsWithChildren> = ({
  37. children,
  38. }) => {
  39. const location = useLocation()
  40. const [connectionManager] = useState(() => new ConnectionManager())
  41. const [connectionState, setConnectionState] = useState(
  42. connectionManager.state
  43. )
  44. useEffect(() => {
  45. const handleStateChange = ((event: StateChangeEvent) => {
  46. setConnectionState(event.detail.state)
  47. }) as EventListener
  48. connectionManager.addEventListener('statechange', handleStateChange)
  49. return () => {
  50. connectionManager.removeEventListener('statechange', handleStateChange)
  51. }
  52. }, [connectionManager])
  53. const isConnected = connectionState.readyState === WebSocket.OPEN
  54. const isStillReconnecting =
  55. connectionState.readyState === WebSocket.CONNECTING &&
  56. performance.now() - connectionState.lastConnectionAttempt > 1000
  57. const secondsUntilReconnect = useCallback(
  58. () => secondsUntil(connectionState.reconnectAt),
  59. [connectionState.reconnectAt]
  60. )
  61. const tryReconnectNow = useCallback(
  62. () => connectionManager.tryReconnectNow(),
  63. [connectionManager]
  64. )
  65. const registerUserActivity = useCallback(
  66. () => connectionManager.registerUserActivity(),
  67. [connectionManager]
  68. )
  69. const closeConnection = useCallback(
  70. (err: ConnectionError) => connectionManager.close(err),
  71. [connectionManager]
  72. )
  73. const getSocketDebuggingInfo = useCallback(
  74. () => connectionManager.getSocketDebuggingInfo(),
  75. [connectionManager]
  76. )
  77. // Reload the page on force disconnect. Doing this in React-land means that we
  78. // can use useLocation(), which provides mockable location methods
  79. useEffect(() => {
  80. if (
  81. connectionState.forceDisconnected &&
  82. // keep editor open when out of sync
  83. connectionState.error !== 'out-of-sync'
  84. ) {
  85. const timer = window.setTimeout(
  86. () => location.reload(),
  87. connectionState.forcedDisconnectDelay * 1000
  88. )
  89. return () => {
  90. window.clearTimeout(timer)
  91. }
  92. }
  93. }, [
  94. connectionState.forceDisconnected,
  95. connectionState.forcedDisconnectDelay,
  96. connectionState.error,
  97. location,
  98. ])
  99. const value = useMemo<ConnectionContextValue>(
  100. () => ({
  101. socket: connectionManager.socket,
  102. connectionState,
  103. isConnected,
  104. isStillReconnecting,
  105. secondsUntilReconnect,
  106. tryReconnectNow,
  107. registerUserActivity,
  108. closeConnection,
  109. getSocketDebuggingInfo,
  110. }),
  111. [
  112. connectionManager.socket,
  113. connectionState,
  114. isConnected,
  115. isStillReconnecting,
  116. registerUserActivity,
  117. secondsUntilReconnect,
  118. tryReconnectNow,
  119. closeConnection,
  120. getSocketDebuggingInfo,
  121. ]
  122. )
  123. return (
  124. <ConnectionContext.Provider value={value}>
  125. {children}
  126. </ConnectionContext.Provider>
  127. )
  128. }
  129. export function useConnectionContext(): ConnectionContextValue {
  130. const context = useContext(ConnectionContext)
  131. if (!context) {
  132. throw new Error(
  133. 'useConnectionContext is only available inside ConnectionProvider'
  134. )
  135. }
  136. return context
  137. }