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(null); const [connected, setConnected] = useState(null); // Default to true: const [willAttemptReconnect] = useState(true); const serverIDRef = useRef(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(() => { 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 ( {children} theme.zIndex.drawer + 1, }}>
{'Disconnected. Attempting to reconnect...'}
); }