import React, { createContext, useCallback, useContext, useEffect, useState, } from 'react' import { Socket, io } from 'socket.io-client' import { toast } from 'sonner' import { AUTH_EVENTS, PLAYER_EVENTS, SOUND_EVENTS, USER_EVENTS, authEvents, playerEvents, soundEvents, userEvents, } from '../lib/events' import { useAuth } from './AuthContext' interface SocketContextType { socket: Socket | null isConnected: boolean connectionError: string | null isReconnecting: boolean } const SocketContext = createContext(undefined) interface SocketProviderProps { children: React.ReactNode } export function SocketProvider({ children }: SocketProviderProps) { const { user, loading } = useAuth() const [socket, setSocket] = useState(null) const [isConnected, setIsConnected] = useState(false) const [connectionError, setConnectionError] = useState(null) const [isReconnecting, setIsReconnecting] = useState(false) const createSocket = useCallback(() => { if (!user) return null // Get socket URL - use relative URL in production with reverse proxy const socketUrl = import.meta.env.PROD ? '' // Use relative URL in production (same origin as frontend) : import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000' const newSocket = io(socketUrl, { withCredentials: true, transports: ['polling', 'websocket'], timeout: 20000, forceNew: true, autoConnect: true, }) newSocket.on('connect', () => { setIsConnected(true) setConnectionError(null) setIsReconnecting(false) }) newSocket.on('disconnect', () => { setIsConnected(false) }) newSocket.on('connect_error', error => { setConnectionError(`Connection failed: ${error.message}`) setIsConnected(false) setIsReconnecting(false) }) // Listen for message events newSocket.on('user_message', data => { toast.info(`Message from ${data.from_user_name}`, { description: data.message, }) }) newSocket.on('broadcast_message', data => { toast.warning(`Broadcast from ${data.from_user_name}`, { description: data.message, }) }) // Listen for player events and emit them locally newSocket.on('player_state', data => { playerEvents.emit(PLAYER_EVENTS.PLAYER_STATE, data) }) // Listen for sound events and emit them locally newSocket.on('sound_played', data => { soundEvents.emit(SOUND_EVENTS.SOUND_PLAYED, data) }) newSocket.on('sound_favorited', data => { soundEvents.emit(SOUND_EVENTS.SOUND_FAVORITED, data) }) // Listen for user events and emit them locally newSocket.on('user_credits_changed', data => { userEvents.emit(USER_EVENTS.USER_CREDITS_CHANGED, data) }) return newSocket }, [user]) // Handle token refresh - reconnect socket with new token const handleTokenRefresh = useCallback(() => { if (!user || !socket) return setIsReconnecting(true) // Disconnect current socket socket.disconnect() // Create new socket with fresh token const newSocket = createSocket() if (newSocket) { setSocket(newSocket) } }, [user, socket, createSocket]) // Listen for token refresh events useEffect(() => { authEvents.on(AUTH_EVENTS.TOKEN_REFRESHED, handleTokenRefresh) return () => { authEvents.off(AUTH_EVENTS.TOKEN_REFRESHED, handleTokenRefresh) } }, [handleTokenRefresh]) // Initial socket connection useEffect(() => { if (loading) return if (!user) { if (socket) { socket.disconnect() setSocket(null) setIsConnected(false) } return } const newSocket = createSocket() if (newSocket) { setSocket(newSocket) } return () => { if (newSocket) { newSocket.disconnect() } } }, [loading, user, createSocket]) const value: SocketContextType = { socket, isConnected, connectionError, isReconnecting, } return ( {children} ) } export function useSocket() { const context = useContext(SocketContext) if (context === undefined) { throw new Error('useSocket must be used within a SocketProvider') } return context }