Spaces:
Runtime error
Runtime error
| 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'; | |
| // The time to wait before showing a "disconnected" screen upon initial app load | |
| 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); | |
| // Default to true: | |
| const [willAttemptReconnect] = useState<boolean>(true); | |
| const serverIDRef = useRef<string | null>(null); | |
| const setAppResetKey = useContext(AppResetKeyContext); | |
| /** | |
| * Previously we had stored the clientID in local storage, but in that case | |
| * if a user refreshes their page they'll still have the same clientID, and | |
| * will be put back into the same room, which may be confusing if they're trying | |
| * to join a new room or reset the app interface. So now clientIDs persist only as | |
| * long as the react app full lifecycle | |
| */ | |
| const clientID = useStable<string>(() => { | |
| const newID = uuidv4(); | |
| // Set the clientID in session storage so if the page reloads the person | |
| // still retains their member/room config | |
| 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, | |
| // Normally socket.io will fallback to http polling, but we basically never | |
| // want that because that'd mean awful performance. It'd be better for the app | |
| // to simply break in that case and not connect. | |
| 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]); | |
| /** | |
| * Wait to show the disconnected screen on initial app load | |
| */ | |
| 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> | |
| ); | |
| } | |