connection-context.tsx 3.7 KB

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