connection-context.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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 = ({ children }) => {
  37. const location = useLocation()
  38. const [connectionManager] = useState(() => new ConnectionManager())
  39. const [connectionState, setConnectionState] = useState(
  40. connectionManager.state
  41. )
  42. useEffect(() => {
  43. const handleStateChange = ((event: StateChangeEvent) => {
  44. setConnectionState(event.detail.state)
  45. }) as EventListener
  46. connectionManager.addEventListener('statechange', handleStateChange)
  47. return () => {
  48. connectionManager.removeEventListener('statechange', handleStateChange)
  49. }
  50. }, [connectionManager])
  51. const isConnected = connectionState.readyState === WebSocket.OPEN
  52. const isStillReconnecting =
  53. connectionState.readyState === WebSocket.CONNECTING &&
  54. performance.now() - connectionState.lastConnectionAttempt > 1000
  55. const secondsUntilReconnect = useCallback(
  56. () => secondsUntil(connectionState.reconnectAt),
  57. [connectionState.reconnectAt]
  58. )
  59. const tryReconnectNow = useCallback(
  60. () => connectionManager.tryReconnectNow(),
  61. [connectionManager]
  62. )
  63. const registerUserActivity = useCallback(
  64. () => connectionManager.registerUserActivity(),
  65. [connectionManager]
  66. )
  67. const closeConnection = useCallback(
  68. (err: ConnectionError) => connectionManager.close(err),
  69. [connectionManager]
  70. )
  71. const getSocketDebuggingInfo = useCallback(
  72. () => connectionManager.getSocketDebuggingInfo(),
  73. [connectionManager]
  74. )
  75. // Reload the page on force disconnect. Doing this in React-land means that we
  76. // can use useLocation(), which provides mockable location methods
  77. useEffect(() => {
  78. if (
  79. connectionState.forceDisconnected &&
  80. // keep editor open when out of sync
  81. connectionState.error !== 'out-of-sync'
  82. ) {
  83. const timer = window.setTimeout(
  84. () => location.reload(),
  85. connectionState.forcedDisconnectDelay * 1000
  86. )
  87. return () => {
  88. window.clearTimeout(timer)
  89. }
  90. }
  91. }, [
  92. connectionState.forceDisconnected,
  93. connectionState.forcedDisconnectDelay,
  94. connectionState.error,
  95. location,
  96. ])
  97. const value = useMemo<ConnectionContextValue>(
  98. () => ({
  99. socket: connectionManager.socket,
  100. connectionState,
  101. isConnected,
  102. isStillReconnecting,
  103. secondsUntilReconnect,
  104. tryReconnectNow,
  105. registerUserActivity,
  106. closeConnection,
  107. getSocketDebuggingInfo,
  108. }),
  109. [
  110. connectionManager.socket,
  111. connectionState,
  112. isConnected,
  113. isStillReconnecting,
  114. registerUserActivity,
  115. secondsUntilReconnect,
  116. tryReconnectNow,
  117. closeConnection,
  118. getSocketDebuggingInfo,
  119. ]
  120. )
  121. return (
  122. <ConnectionContext.Provider value={value}>
  123. {children}
  124. </ConnectionContext.Provider>
  125. )
  126. }
  127. export function useConnectionContext(): ConnectionContextValue {
  128. const context = useContext(ConnectionContext)
  129. if (!context) {
  130. throw new Error(
  131. 'useConnectionContext is only available inside ConnectionProvider'
  132. )
  133. }
  134. return context
  135. }