// Socket.IO client instance for real-time communication import { io } from 'socket.io-client'; import { createContext, useContext, useEffect, useState } from 'react'; // Create a Socket.IO context for use throughout the app const SocketContext = createContext(null); // Game context for sharing game state const GameContext = createContext(null); export function SocketProvider({ children }) { const [socket, setSocket] = useState(null); const [isConnected, setIsConnected] = useState(false); const [isReconnecting, setIsReconnecting] = useState(false); const [reconnectionAttempts, setReconnectionAttempts] = useState(0); const [offlineActions, setOfflineActions] = useState([]); const [lastActivityTime, setLastActivityTime] = useState(Date.now()); useEffect(() => { // Create socket connection on component mount with improved reconnection settings const socketInstance = io(import.meta.env.DEV ? 'http://localhost:5237' : window.location.origin, { autoConnect: true, reconnection: true, reconnectionDelay: 1000, reconnectionDelayMax: 10000, reconnectionAttempts: Infinity, // Keep trying to reconnect indefinitely timeout: 20000, // Longer timeout for initial connection randomizationFactor: 0.5 // Add some randomization to reconnection attempts to prevent thundering herd }); // Socket event listeners socketInstance.on('connect', () => { setIsConnected(true); setIsReconnecting(false); setReconnectionAttempts(0); console.log('Connected to server'); // Process any actions that were queued while offline if (offlineActions.length > 0) { console.log(`Processing ${offlineActions.length} queued actions from offline mode`); // Wait a moment to ensure connection is stable setTimeout(() => { processOfflineActions(socketInstance); }, 1000); } // Try to reconnect to game if we have saved data const savedGameData = localStorage.getItem('songBattleGame'); if (savedGameData) { try { const { lobbyId, playerName, lastKnownState } = JSON.parse(savedGameData); if (lobbyId && playerName) { console.log(`Attempting to reconnect to lobby: ${lobbyId}`); socketInstance.emit('reconnect_to_lobby', { lobbyId, playerName, lastKnownState // Send last known game state for server reconciliation }, (response) => { if (response.error) { console.error('Reconnection failed:', response.error); // Don't remove data immediately, we might try again if (response.error === 'Lobby not found') { localStorage.removeItem('songBattleGame'); } } else { console.log('Successfully reconnected to lobby'); // Update the saved game data with latest state updateSavedGameData(response.lobby); } }); } } catch (e) { console.error('Error parsing saved game data:', e); localStorage.removeItem('songBattleGame'); } } }); socketInstance.on('disconnect', (reason) => { setIsConnected(false); console.log(`Disconnected from server: ${reason}`); // If the disconnection is expected, don't try to reconnect if (reason === 'io client disconnect') { console.log('Disconnection was initiated by the client'); } else { // Start reconnection process for unexpected disconnections setIsReconnecting(true); } }); socketInstance.on('connect_error', (error) => { console.error('Connection error:', error); setIsReconnecting(true); }); socketInstance.on('reconnect_attempt', (attemptNumber) => { console.log(`Reconnection attempt #${attemptNumber}`); setReconnectionAttempts(attemptNumber); }); socketInstance.on('reconnect', () => { console.log('Reconnected to server after interruption'); setIsReconnecting(false); setReconnectionAttempts(0); }); socketInstance.on('reconnect_error', (error) => { console.error('Reconnection error:', error); }); socketInstance.on('reconnect_failed', () => { console.error('Failed to reconnect to server after maximum attempts'); setIsReconnecting(false); }); // Setup a heartbeat to detect silent disconnections const heartbeatInterval = setInterval(() => { if (socketInstance && isConnected) { // Check if we've seen activity recently (within 15 seconds) const timeSinceLastActivity = Date.now() - lastActivityTime; if (timeSinceLastActivity > 15000) { console.log('No recent activity, sending ping to verify connection'); socketInstance.emit('ping', () => { // Update activity time when we get a response setLastActivityTime(Date.now()); }); // Set a timeout to check if the ping was successful setTimeout(() => { const newTimeSinceLastActivity = Date.now() - lastActivityTime; if (newTimeSinceLastActivity > 15000) { console.warn('No response to ping, connection may be dead'); // Force a reconnection attempt socketInstance.disconnect().connect(); } }, 5000); } } }, 30000); // Check every 30 seconds setSocket(socketInstance); // Function to process queued offline actions const processOfflineActions = (socket) => { if (!socket || !offlineActions.length) return; // Process each action in sequence const processAction = (index) => { if (index >= offlineActions.length) { // All actions processed, clear the queue setOfflineActions([]); return; } const { action, payload, callback } = offlineActions[index]; console.log(`Processing offline action: ${action}`, payload); // Emit the action to the server socket.emit(action, payload, (response) => { if (response.error) { console.error(`Error processing offline action ${action}:`, response.error); } else { console.log(`Successfully processed offline action: ${action}`); if (callback) callback(response); } // Process the next action processAction(index + 1); }); }; // Start processing from the first action processAction(0); }; // Update activity timestamp on any received message const originalOnEvent = socketInstance.onEvent; socketInstance.onEvent = (packet) => { setLastActivityTime(Date.now()); originalOnEvent.call(socketInstance, packet); }; // Function to update saved game data with latest state const updateSavedGameData = (lobby) => { if (!lobby) return; const currentData = localStorage.getItem('songBattleGame'); if (currentData) { try { const data = JSON.parse(currentData); data.lastKnownState = { gameState: lobby.state, currentBattle: lobby.currentBattle, timestamp: Date.now() }; localStorage.setItem('songBattleGame', JSON.stringify(data)); } catch (e) { console.error('Error updating saved game data:', e); } } }; // Clean up on unmount return () => { clearInterval(heartbeatInterval); socketInstance.disconnect(); }; }, []); // Offline mode detection const [isOffline, setIsOffline] = useState(!navigator.onLine); // Monitor browser's online/offline status useEffect(() => { const handleOnline = () => setIsOffline(false); const handleOffline = () => setIsOffline(true); window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); // Add an action to the offline queue const queueOfflineAction = (action, payload, callback) => { console.log(`Queuing offline action: ${action}`, payload); setOfflineActions(prev => [...prev, { action, payload, callback }]); // Store in localStorage as fallback try { const offlineQueue = JSON.parse(localStorage.getItem('offlineActionQueue') || '[]'); offlineQueue.push({ action, payload, timestamp: Date.now() }); localStorage.setItem('offlineActionQueue', JSON.stringify(offlineQueue)); } catch (e) { console.error('Error storing offline action in localStorage:', e); } }; // Emit an event, queue it if offline const safeEmit = (eventName, data, callback) => { // Update activity timestamp setLastActivityTime(Date.now()); if (socket && isConnected && !isOffline) { // Online - send immediately socket.emit(eventName, data, callback); } else { // Offline or disconnected - queue for later queueOfflineAction(eventName, data, callback); // For specific actions, provide optimistic UI updates if (eventName === 'submit_vote' && data.songId) { // Optimistically show the vote locally // Implementation depends on your UI update mechanism } } }; return ( {children} ); } /** * Game state provider - manages game state and provides methods for game actions */ export function GameProvider({ children }) { const { socket, isConnected, isOffline, isReconnecting, safeEmit } = useContext(SocketContext); const [lobby, setLobby] = useState(null); const [error, setError] = useState(null); const [currentPlayer, setCurrentPlayer] = useState(null); // Save game info when lobby is joined useEffect(() => { if (lobby && currentPlayer) { const savedData = { lobbyId: lobby.id, playerName: currentPlayer.name, lastKnownState: { gameState: lobby.state, currentBattle: lobby.currentBattle, timestamp: Date.now() } }; localStorage.setItem('songBattleGame', JSON.stringify(savedData)); } }, [lobby, currentPlayer]); // Helper function to update saved game data const updateSavedGameData = (updatedLobby) => { if (!updatedLobby || !currentPlayer) return; const savedData = { lobbyId: updatedLobby.id, playerName: currentPlayer.name, lastKnownState: { gameState: updatedLobby.state, currentBattle: updatedLobby.currentBattle, playerState: { isReady: updatedLobby.players?.find(p => p.id === currentPlayer.id)?.isReady, hasVoted: updatedLobby.currentBattle?.votes?.[currentPlayer.id] !== undefined }, timestamp: Date.now() } }; localStorage.setItem('songBattleGame', JSON.stringify(savedData)); }; // Clear error after 5 seconds useEffect(() => { if (error) { const timer = setTimeout(() => { setError(null); }, 5000); return () => clearTimeout(timer); } }, [error]); // Socket event handlers for game updates useEffect(() => { if (!socket) return; // Player joined the lobby const handlePlayerJoined = (data) => { setLobby(prevLobby => { if (!prevLobby) return prevLobby; // Update the lobby with the new player const updatedPlayers = [...prevLobby.players, { id: data.playerId, name: data.playerName, isConnected: true, isReady: false, songCount: 0 }]; return { ...prevLobby, players: updatedPlayers }; }); }; // Player reconnected to the lobby const handlePlayerReconnected = (data) => { setLobby(prevLobby => { if (!prevLobby) return prevLobby; // Update the lobby with the reconnected player const updatedPlayers = prevLobby.players.map(player => { if (player.name === data.playerName && !player.isConnected) { return { ...player, id: data.playerId, isConnected: true }; } return player; }); return { ...prevLobby, players: updatedPlayers }; }); }; // Player disconnected from the lobby const handlePlayerDisconnected = (data) => { setLobby(data.lobby); }; // Game settings were updated const handleSettingsUpdated = (data) => { setLobby(prevLobby => { if (!prevLobby) return prevLobby; return { ...prevLobby, settings: data.settings }; }); }; // Game started const handleGameStarted = (data) => { setLobby(prevLobby => { if (!prevLobby) return prevLobby; return { ...prevLobby, state: data.state }; }); }; // Songs were updated const handleSongsUpdated = (data) => { setLobby(data.lobby); }; // Player status changed (ready/not ready) const handlePlayerStatusChanged = (data) => { console.log('Player status changed, new lobby state:', data.lobby.state); setLobby(data.lobby); // If the state is VOTING and we have a current battle, explicitly log it if (data.lobby.state === 'VOTING' && data.lobby.currentBattle) { console.log('Battle ready in player_status_changed:', data.lobby.currentBattle); } }; // Vote was submitted const handleVoteSubmitted = (data) => { console.log('Vote submitted, updating lobby'); setLobby(data.lobby); }; // New battle started const handleNewBattle = (data) => { console.log('New battle received:', data.battle); setLobby(prevLobby => { if (!prevLobby) return prevLobby; // Ensure we update both the currentBattle and the state return { ...prevLobby, currentBattle: data.battle, state: 'VOTING' }; }); }; // Handle battle ended and show result screen const handleBattleEnded = (data) => { console.log('Battle ended, showing result screen:', data); setLobby(prevLobby => { if (!prevLobby) return prevLobby; return { ...prevLobby, state: 'BATTLE', previousBattle: data.previousBattle }; }); }; // Game finished const handleGameFinished = (data) => { setLobby(prevLobby => { if (!prevLobby) return prevLobby; return { ...prevLobby, state: 'FINISHED', finalWinner: data.winner, battles: data.battles }; }); }; // Register socket event listeners socket.on('player_joined', handlePlayerJoined); socket.on('player_reconnected', handlePlayerReconnected); socket.on('player_disconnected', handlePlayerDisconnected); socket.on('settings_updated', handleSettingsUpdated); socket.on('game_started', handleGameStarted); socket.on('songs_updated', handleSongsUpdated); socket.on('player_status_changed', handlePlayerStatusChanged); socket.on('vote_submitted', handleVoteSubmitted); socket.on('battle_ended', handleBattleEnded); socket.on('tournament_started', data => { console.log('Tournament started event received:', data); setLobby(prevLobby => prevLobby ? {...prevLobby, state: data.state} : prevLobby); }); socket.on('new_battle', handleNewBattle); socket.on('battle_ended', handleBattleEnded); socket.on('game_finished', handleGameFinished); // Clean up listeners on unmount return () => { socket.off('player_joined', handlePlayerJoined); socket.off('player_reconnected', handlePlayerReconnected); socket.off('player_disconnected', handlePlayerDisconnected); socket.off('settings_updated', handleSettingsUpdated); socket.off('game_started', handleGameStarted); socket.off('songs_updated', handleSongsUpdated); socket.off('player_status_changed', handlePlayerStatusChanged); socket.off('vote_submitted', handleVoteSubmitted); socket.off('new_battle', handleNewBattle); socket.off('battle_ended', handleBattleEnded); socket.off('game_finished', handleGameFinished); }; }, [socket]); // Create a lobby const createLobby = (playerName) => { if (!socket || !isConnected) { setError('Not connected to server'); return; } safeEmit('create_lobby', { playerName }, (response) => { if (response.error) { setError(response.error); } else { setLobby(response.lobby); setCurrentPlayer({ id: socket.id, name: playerName, isHost: true }); } }); }; // Join a lobby const joinLobby = (lobbyId, playerName) => { if (!socket || !isConnected) { setError('Not connected to server'); return; } safeEmit('join_lobby', { lobbyId, playerName }, (response) => { if (response.error) { setError(response.error); } else { setLobby(response.lobby); setCurrentPlayer({ id: socket.id, name: playerName, isHost: response.lobby.hostId === socket.id }); } }); }; // Update lobby settings const updateSettings = (settings) => { if (!socket || !isConnected || !lobby) { setError('Not connected to server or no active lobby'); return; } safeEmit('update_settings', { settings }, (response) => { if (response.error) { setError(response.error); } }); }; // Start the game const startGame = () => { if (!socket || !isConnected || !lobby) { setError('Not connected to server or no active lobby'); return; } safeEmit('start_game', {}, (response) => { if (response.error) { setError(response.error); } }); }; // Add a song const addSong = (song) => { if (!socket || !isConnected || !lobby) { setError('Not connected to server or no active lobby'); return Promise.reject('Not connected to server or no active lobby'); } console.log('Attempting to add song:', song); console.log('Current player state:', currentPlayer); console.log('Current lobby state before adding song:', lobby); return new Promise((resolve, reject) => { safeEmit('add_song', { song }, (response) => { console.log('Song addition response:', response); if (response.error) { console.error('Error adding song:', response.error); setError(response.error); reject(response.error); } else if (response.lobby) { // Log detailed lobby state for debugging console.log('Song added successfully, full lobby response:', response.lobby); console.log('Current player songs:', response.lobby.players.find(p => p.id === currentPlayer.id)?.songs); console.log('All players song data:', response.lobby.players.map(p => ({ name: p.name, id: p.id, songCount: p.songCount, songs: p.songs ? p.songs.map(s => ({id: s.id, title: s.title})) : 'No songs' })) ); // Force a deep clone of the lobby to ensure React detects the change const updatedLobby = JSON.parse(JSON.stringify(response.lobby)); setLobby(updatedLobby); // Verify the state was updated correctly setTimeout(() => { // This won't show the updated state immediately due to React's state update mechanism console.log('Lobby state after update (may not reflect immediate changes):', lobby); console.log('Updated lobby that was set:', updatedLobby); }, 0); // Store the latest state for offline reconciliation updateSavedGameData(updatedLobby); // Resolve with the updated lobby resolve(updatedLobby); } else { console.error('Song addition succeeded but no lobby data was returned'); setError('Failed to update song list'); reject('Failed to update song list'); } }); }); }; // Search for songs on YouTube const searchYouTube = (query) => { return new Promise((resolve, reject) => { if (!socket || !isConnected) { setError('Not connected to server'); reject('Not connected to server'); return; } safeEmit('search_youtube', { query }, (response) => { if (response.error) { setError(response.error); reject(response.error); } else { resolve(response.results); } }); }); }; // Get metadata for a YouTube video by ID const getYouTubeMetadata = (videoId) => { return new Promise((resolve, reject) => { if (!socket || !isConnected) { setError('Not connected to server'); reject('Not connected to server'); return; } safeEmit('get_video_metadata', { videoId }, (response) => { if (response.error) { setError(response.error); reject(response.error); } else { resolve(response.metadata); } }); }); }; // Remove a song const removeSong = (songId) => { if (!socket || !isConnected || !lobby) { setError('Not connected to server or no active lobby'); return; } safeEmit('remove_song', { songId }, (response) => { if (response.error) { setError(response.error); } else if (response.lobby) { setLobby(response.lobby); updateSavedGameData(response.lobby); } }); }; // Set player ready const setPlayerReady = () => { if (!socket || !isConnected || !lobby) { setError('Not connected to server or no active lobby'); return; } safeEmit('player_ready', {}, (response) => { if (response.error) { setError(response.error); } else if (response.lobby) { setLobby(response.lobby); updateSavedGameData(response.lobby); } }); }; // Submit a vote const submitVote = (songId) => { if (!socket || !isConnected || !lobby) { setError('Not connected to server or no active lobby'); return; } safeEmit('submit_vote', { songId }, (response) => { if (response.error) { setError(response.error); } else if (response.lobby) { setLobby(response.lobby); updateSavedGameData(response.lobby); } }); }; // Leave lobby and clear saved game state const leaveLobby = () => { localStorage.removeItem('songBattleGame'); setLobby(null); setCurrentPlayer(null); }; // Proceed to next battle after the battle result screen const proceedToNextBattle = () => { if (!socket || !isConnected) { setError('Not connected to server'); return; } safeEmit('proceed_to_next_battle', {}, (response) => { if (response.error) { setError(response.error); } else if (response.lobby) { setLobby(response.lobby); updateSavedGameData(response.lobby); } }); }; return ( {children} ); } // Custom hooks for using contexts export const useSocket = () => useContext(SocketContext); export const useGame = () => useContext(GameContext); // Re-export for better module compatibility export { SocketContext, GameContext };