Files
overleaf-cep/services/web/frontend/js/features/ide-react/context/connection-context.tsx
Jakob Ackermann 45a5d090d9 [web] add external 15s heartbeat to socket.io connection (#22853)
* [web] add external 15s heartbeat to socket.io connection

* [web] extend debugging context for client errors

- include performance.now() timestamp for correlating other timings
- include connectionState, especially for new externalHeartbeat detail
- include spellCheckLanguage to check on client-side spelling impact

* [web] remove unnecessary hook dependency

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>

* Refactor externalHeartbeat

* Add connectionManager to context

* Clear the interval earlier, and on connect

* [web] refactor handling of socket debugging info

* [web] add split-test for external socket heartbeat

* [web] fully remove connectionManager from connection context

---------

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
GitOrigin-RevId: fbebe64f8aa207eb4fd4a8f27d522d1cac35f9d4
2025-01-28 09:05:12 +00:00

148 lines
3.7 KiB
TypeScript

import {
createContext,
useContext,
useEffect,
useState,
FC,
useCallback,
useMemo,
} from 'react'
import {
ConnectionState,
SocketDebuggingInfo,
} from '../connection/types/connection-state'
import {
ConnectionManager,
StateChangeEvent,
} from '@/features/ide-react/connection/connection-manager'
import { Socket } from '@/features/ide-react/connection/types/socket'
import { secondsUntil } from '@/features/ide-react/connection/utils'
import { useLocation } from '@/shared/hooks/use-location'
type ConnectionContextValue = {
socket: Socket
connectionState: ConnectionState
isConnected: boolean
isStillReconnecting: boolean
secondsUntilReconnect: () => number
tryReconnectNow: () => void
registerUserActivity: () => void
disconnect: () => void
getSocketDebuggingInfo: () => SocketDebuggingInfo
}
export const ConnectionContext = createContext<
ConnectionContextValue | undefined
>(undefined)
export const ConnectionProvider: FC = ({ children }) => {
const location = useLocation()
const [connectionManager] = useState(() => new ConnectionManager())
const [connectionState, setConnectionState] = useState(
connectionManager.state
)
useEffect(() => {
const handleStateChange = ((event: StateChangeEvent) => {
setConnectionState(event.detail.state)
}) as EventListener
connectionManager.addEventListener('statechange', handleStateChange)
return () => {
connectionManager.removeEventListener('statechange', handleStateChange)
}
}, [connectionManager])
const isConnected = connectionState.readyState === WebSocket.OPEN
const isStillReconnecting =
connectionState.readyState === WebSocket.CONNECTING &&
performance.now() - connectionState.lastConnectionAttempt > 1000
const secondsUntilReconnect = useCallback(
() => secondsUntil(connectionState.reconnectAt),
[connectionState.reconnectAt]
)
const tryReconnectNow = useCallback(
() => connectionManager.tryReconnectNow(),
[connectionManager]
)
const registerUserActivity = useCallback(
() => connectionManager.registerUserActivity(),
[connectionManager]
)
const disconnect = useCallback(() => {
connectionManager.disconnect()
}, [connectionManager])
const getSocketDebuggingInfo = useCallback(
() => connectionManager.getSocketDebuggingInfo(),
[connectionManager]
)
// Reload the page on force disconnect. Doing this in React-land means that we
// can use useLocation(), which provides mockable location methods
useEffect(() => {
if (connectionState.forceDisconnected) {
const timer = window.setTimeout(
() => location.reload(),
connectionState.forcedDisconnectDelay * 1000
)
return () => {
window.clearTimeout(timer)
}
}
}, [
connectionState.forceDisconnected,
connectionState.forcedDisconnectDelay,
location,
])
const value = useMemo<ConnectionContextValue>(
() => ({
socket: connectionManager.socket,
connectionState,
isConnected,
isStillReconnecting,
secondsUntilReconnect,
tryReconnectNow,
registerUserActivity,
disconnect,
getSocketDebuggingInfo,
}),
[
connectionManager.socket,
connectionState,
isConnected,
isStillReconnecting,
registerUserActivity,
secondsUntilReconnect,
tryReconnectNow,
disconnect,
getSocketDebuggingInfo,
]
)
return (
<ConnectionContext.Provider value={value}>
{children}
</ConnectionContext.Provider>
)
}
export function useConnectionContext(): ConnectionContextValue {
const context = useContext(ConnectionContext)
if (!context) {
throw new Error(
'useConnectionContext is only available inside ConnectionProvider'
)
}
return context
}