233 lines
6.3 KiB
TypeScript
233 lines
6.3 KiB
TypeScript
import React, {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
} from 'react'
|
|
import { Socket, io } from 'socket.io-client'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
AUTH_EVENTS,
|
|
EXTRACTION_EVENTS,
|
|
PLAYER_EVENTS,
|
|
SOUND_EVENTS,
|
|
USER_EVENTS,
|
|
authEvents,
|
|
extractionEvents,
|
|
playerEvents,
|
|
soundEvents,
|
|
userEvents,
|
|
} from '../lib/events'
|
|
import { extractionsService } from '../lib/api/services/extractions'
|
|
import { useAuth } from './AuthContext'
|
|
|
|
interface SocketContextType {
|
|
socket: Socket | null
|
|
isConnected: boolean
|
|
connectionError: string | null
|
|
isReconnecting: boolean
|
|
}
|
|
|
|
const SocketContext = createContext<SocketContextType | undefined>(undefined)
|
|
|
|
interface SocketProviderProps {
|
|
children: React.ReactNode
|
|
}
|
|
|
|
export function SocketProvider({ children }: SocketProviderProps) {
|
|
const { user, loading } = useAuth()
|
|
const [socket, setSocket] = useState<Socket | null>(null)
|
|
const [isConnected, setIsConnected] = useState(false)
|
|
const [connectionError, setConnectionError] = useState<string | null>(null)
|
|
const [isReconnecting, setIsReconnecting] = useState(false)
|
|
|
|
const fetchAndShowOngoingExtractions = useCallback(async () => {
|
|
try {
|
|
const processingExtractions = await extractionsService.getProcessingExtractions()
|
|
|
|
processingExtractions.forEach(extraction => {
|
|
const title = extraction.title || 'Processing extraction...'
|
|
toast.loading(`Extracting: ${title}`, {
|
|
id: `extraction-${extraction.id}`,
|
|
duration: Infinity, // Keep it open until status changes
|
|
})
|
|
})
|
|
} catch (error) {
|
|
console.error('Failed to fetch ongoing extractions:', error)
|
|
}
|
|
}, [])
|
|
|
|
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)
|
|
|
|
// Fetch and show any ongoing extractions
|
|
fetchAndShowOngoingExtractions()
|
|
})
|
|
|
|
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)
|
|
})
|
|
|
|
// Listen for extraction status updates
|
|
newSocket.on('extraction_status_update', data => {
|
|
const { extraction_id, status, title, error } = data
|
|
|
|
// Emit local event for other components to listen to
|
|
extractionEvents.emit(EXTRACTION_EVENTS.EXTRACTION_STATUS_UPDATED, data)
|
|
|
|
// Handle specific status events
|
|
switch (status) {
|
|
case 'processing':
|
|
toast.loading(`Extracting: ${title}`, {
|
|
id: `extraction-${extraction_id}`,
|
|
duration: Infinity, // Keep it open until status changes
|
|
})
|
|
break
|
|
case 'completed':
|
|
toast.dismiss(`extraction-${extraction_id}`)
|
|
toast.success(`Extraction complete: ${title}`, {
|
|
duration: 4000,
|
|
})
|
|
extractionEvents.emit(EXTRACTION_EVENTS.EXTRACTION_COMPLETED, data)
|
|
break
|
|
case 'failed':
|
|
toast.dismiss(`extraction-${extraction_id}`)
|
|
toast.error(`Extraction failed: ${title}`, {
|
|
description: error,
|
|
duration: 6000,
|
|
})
|
|
extractionEvents.emit(EXTRACTION_EVENTS.EXTRACTION_FAILED, data)
|
|
break
|
|
}
|
|
})
|
|
|
|
return newSocket
|
|
}, [user, fetchAndShowOngoingExtractions])
|
|
|
|
// 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 (
|
|
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useSocket() {
|
|
const context = useContext(SocketContext)
|
|
if (context === undefined) {
|
|
throw new Error('useSocket must be used within a SocketProvider')
|
|
}
|
|
return context
|
|
}
|