| | import {useContext, useEffect, useMemo, useRef, useState} from 'react'; |
| | import socketIOClient, {Socket} from 'socket.io-client'; |
| | import useStable from './useStable'; |
| | import {v4 as uuidv4} from 'uuid'; |
| | import {SocketContext} from './useSocket'; |
| | import {AppResetKeyContext} from './App'; |
| | import Backdrop from '@mui/material/Backdrop'; |
| | import CircularProgress from '@mui/material/CircularProgress'; |
| | import Typography from '@mui/material/Typography'; |
| | import {getURLParams} from './URLParams'; |
| |
|
| | |
| | const INITIAL_DISCONNECT_SCREEN_DELAY = 2000; |
| | const SERVER_URL_DEFAULT = `${window.location.protocol === "https:" ? "wss" : "ws" |
| | }://${window.location.host}`; |
| | |
| | export default function SocketWrapper({children}) { |
| | const [socket, setSocket] = useState<Socket | null>(null); |
| | const [connected, setConnected] = useState<boolean | null>(null); |
| | |
| | const [willAttemptReconnect] = useState<boolean>(true); |
| | const serverIDRef = useRef<string | null>(null); |
| |
|
| | const setAppResetKey = useContext(AppResetKeyContext); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const clientID = useStable<string>(() => { |
| | const newID = uuidv4(); |
| | |
| | |
| | return newID; |
| | }); |
| |
|
| | const socketObject = useMemo( |
| | () => ({socket, clientID, connected: connected ?? false}), |
| | [socket, clientID, connected], |
| | ); |
| |
|
| | useEffect(() => { |
| | const queryParams = { |
| | clientID: clientID, |
| | }; |
| |
|
| | const serverURLFromParams = getURLParams().serverURL; |
| | const serverURL = serverURLFromParams ?? SERVER_URL_DEFAULT; |
| |
|
| | console.log( |
| | `Opening socket connection to ${ |
| | serverURL?.length === 0 ? 'window.location.host' : serverURL |
| | } with query params:`, |
| | queryParams, |
| | ); |
| |
|
| | const newSocket: Socket = socketIOClient(serverURL, { |
| | query: queryParams, |
| | |
| | |
| | |
| | transports: ['websocket'], |
| | path: '/ws/socket.io' |
| | }); |
| |
|
| | const onServerID = (serverID: string) => { |
| | console.debug('Received server ID:', serverID); |
| | if (serverIDRef.current != null) { |
| | if (serverIDRef.current !== serverID) { |
| | console.error( |
| | 'Server ID changed. Resetting the app using the app key', |
| | ); |
| | setAppResetKey(serverID); |
| | } |
| | } |
| | serverIDRef.current = serverID; |
| | }; |
| |
|
| | newSocket.on('server_id', onServerID); |
| |
|
| | setSocket(newSocket); |
| |
|
| | return () => { |
| | newSocket.off('server_id', onServerID); |
| | console.log( |
| | 'Closing socket connection in the useEffect cleanup function...', |
| | ); |
| | newSocket.disconnect(); |
| | setSocket(null); |
| | }; |
| | }, [clientID, setAppResetKey]); |
| |
|
| | useEffect(() => { |
| | if (socket != null) { |
| | const onAny = (eventName: string, ...args) => { |
| | console.debug(`[event: ${eventName}] args:`, ...args); |
| | }; |
| |
|
| | socket.onAny(onAny); |
| |
|
| | return () => { |
| | socket.offAny(onAny); |
| | }; |
| | } |
| | return () => {}; |
| | }, [socket]); |
| |
|
| | useEffect(() => { |
| | if (socket != null) { |
| | const onConnect = (...args) => { |
| | console.debug('Connected to server with args:', ...args); |
| | setConnected(true); |
| | }; |
| |
|
| | const onConnectError = (err) => { |
| | console.error(`Connection error due to ${err.message}`); |
| | }; |
| |
|
| | const onDisconnect = (reason) => { |
| | setConnected(false); |
| | console.log(`Disconnected due to ${reason}`); |
| | }; |
| |
|
| | socket.on('connect', onConnect); |
| | socket.on('connect_error', onConnectError); |
| | socket.on('disconnect', onDisconnect); |
| |
|
| | return () => { |
| | socket.off('connect', onConnect); |
| | socket.off('connect_error', onConnectError); |
| | socket.off('disconnect', onDisconnect); |
| | }; |
| | } |
| | }, [socket]); |
| |
|
| | useEffect(() => { |
| | if (socket != null) { |
| | const onReconnectError = (err) => { |
| | console.log(`Reconnect error due to ${err.message}`); |
| | }; |
| |
|
| | socket.io.on('reconnect_error', onReconnectError); |
| |
|
| | const onError = (err) => { |
| | console.log(`General socket error with message ${err.message}`); |
| | }; |
| | socket.io.on('error', onError); |
| |
|
| | const onReconnect = (attempt) => { |
| | console.log(`Reconnected after ${attempt} attempt(s)`); |
| | }; |
| | socket.io.on('reconnect', onReconnect); |
| |
|
| | const disconnectOnBeforeUnload = () => { |
| | console.log('Disconnecting due to beforeunload event...'); |
| | socket.disconnect(); |
| | setSocket(null); |
| | }; |
| | window.addEventListener('beforeunload', disconnectOnBeforeUnload); |
| |
|
| | return () => { |
| | socket.io.off('reconnect_error', onReconnectError); |
| | socket.io.off('error', onError); |
| | socket.io.off('reconnect', onReconnect); |
| | window.removeEventListener('beforeunload', disconnectOnBeforeUnload); |
| | }; |
| | } |
| | }, [clientID, setAppResetKey, socket]); |
| |
|
| | |
| | |
| | |
| | useEffect(() => { |
| | window.setTimeout(() => { |
| | setConnected((prev) => { |
| | if (prev === null) { |
| | return false; |
| | } |
| | return prev; |
| | }); |
| | }, INITIAL_DISCONNECT_SCREEN_DELAY); |
| | }, []); |
| |
|
| | return ( |
| | <SocketContext.Provider value={socketObject}> |
| | {children} |
| | |
| | <Backdrop |
| | open={connected === false && willAttemptReconnect === true} |
| | sx={{ |
| | color: '#fff', |
| | zIndex: (theme) => theme.zIndex.drawer + 1, |
| | }}> |
| | <div |
| | style={{ |
| | alignItems: 'center', |
| | flexDirection: 'column', |
| | textAlign: 'center', |
| | }}> |
| | <CircularProgress color="inherit" /> |
| | <Typography |
| | align="center" |
| | fontSize={{sm: 18, xs: 16}} |
| | sx={{ |
| | fontFamily: |
| | 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', |
| | fontWeight: 'bold', |
| | }}> |
| | {'Disconnected. Attempting to reconnect...'} |
| | </Typography> |
| | </div> |
| | </Backdrop> |
| | </SocketContext.Provider> |
| | ); |
| | } |
| |
|