210 lines
6.0 KiB
TypeScript
210 lines
6.0 KiB
TypeScript
import { useCallback, useEffect, useRef, useState, useContext } from 'react'
|
|
import { io, type Socket } from 'socket.io-client'
|
|
import type { GameMove } from '@/lib/arcade/validation'
|
|
import { ArcadeErrorContext } from '@/contexts/ArcadeErrorContext'
|
|
|
|
export interface ArcadeSocketEvents {
|
|
onSessionState?: (data: {
|
|
gameState: unknown
|
|
currentGame: string
|
|
gameUrl: string
|
|
activePlayers: number[]
|
|
version: number
|
|
}) => void
|
|
onMoveAccepted?: (data: { gameState: unknown; version: number; move: GameMove }) => void
|
|
onMoveRejected?: (data: { error: string; move: GameMove; versionConflict?: boolean }) => void
|
|
onSessionEnded?: () => void
|
|
onNoActiveSession?: () => void
|
|
onError?: (error: { error: string }) => void
|
|
/** If true, errors will NOT show toasts (for cases where game handles errors directly) */
|
|
suppressErrorToasts?: boolean
|
|
}
|
|
|
|
export interface UseArcadeSocketReturn {
|
|
socket: Socket | null
|
|
connected: boolean
|
|
joinSession: (userId: string, roomId?: string) => void
|
|
sendMove: (userId: string, move: GameMove, roomId?: string) => void
|
|
exitSession: (userId: string) => void
|
|
pingSession: (userId: string) => void
|
|
}
|
|
|
|
/**
|
|
* Hook for managing WebSocket connection to arcade sessions
|
|
*
|
|
* @param events - Event handlers for socket events
|
|
* @returns Socket instance and helper methods
|
|
*/
|
|
export function useArcadeSocket(events: ArcadeSocketEvents = {}): UseArcadeSocketReturn {
|
|
const [socket, setSocket] = useState<Socket | null>(null)
|
|
const [connected, setConnected] = useState(false)
|
|
const eventsRef = useRef(events)
|
|
|
|
// Get error context if available, but don't throw if it's not
|
|
const errorContext = useContext(ArcadeErrorContext)
|
|
const addError = errorContext?.addError || (() => {})
|
|
|
|
// Update events ref when they change
|
|
useEffect(() => {
|
|
eventsRef.current = events
|
|
}, [events])
|
|
|
|
// Initialize socket connection
|
|
useEffect(() => {
|
|
const socketInstance = io({
|
|
path: '/api/socket',
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionAttempts: 5,
|
|
})
|
|
|
|
socketInstance.on('connect', () => {
|
|
console.log('[ArcadeSocket] Connected')
|
|
setConnected(true)
|
|
})
|
|
|
|
socketInstance.on('disconnect', () => {
|
|
console.log('[ArcadeSocket] Disconnected')
|
|
setConnected(false)
|
|
|
|
// Show error toast unless suppressed
|
|
if (!eventsRef.current.suppressErrorToasts) {
|
|
addError(
|
|
'Connection lost',
|
|
'The connection to the game server was lost. Attempting to reconnect...'
|
|
)
|
|
}
|
|
})
|
|
|
|
socketInstance.on('connect_error', (error) => {
|
|
console.error('[ArcadeSocket] Connection error', error)
|
|
|
|
if (!eventsRef.current.suppressErrorToasts) {
|
|
addError(
|
|
'Connection error',
|
|
`Failed to connect to the game server: ${error.message}\n\nPlease check your internet connection and try refreshing the page.`
|
|
)
|
|
}
|
|
})
|
|
|
|
socketInstance.on('session-state', (data) => {
|
|
eventsRef.current.onSessionState?.(data)
|
|
})
|
|
|
|
socketInstance.on('no-active-session', () => {
|
|
// Show error toast unless suppressed
|
|
if (!eventsRef.current.suppressErrorToasts) {
|
|
addError(
|
|
'No active session',
|
|
'No game session was found. Please start a new game or join an existing room.'
|
|
)
|
|
}
|
|
|
|
eventsRef.current.onNoActiveSession?.()
|
|
})
|
|
|
|
socketInstance.on('move-accepted', (data) => {
|
|
eventsRef.current.onMoveAccepted?.(data)
|
|
})
|
|
|
|
socketInstance.on('move-rejected', (data) => {
|
|
console.log(`[ArcadeSocket] Move rejected: ${data.error}`)
|
|
|
|
// Show error toast for move rejections unless suppressed or it's a version conflict
|
|
if (!eventsRef.current.suppressErrorToasts && !data.versionConflict) {
|
|
addError(
|
|
'Move rejected',
|
|
`Your move was not accepted: ${data.error}\n\nMove type: ${data.move.type}`
|
|
)
|
|
}
|
|
|
|
eventsRef.current.onMoveRejected?.(data)
|
|
})
|
|
|
|
socketInstance.on('session-ended', () => {
|
|
console.log('[ArcadeSocket] Session ended')
|
|
eventsRef.current.onSessionEnded?.()
|
|
})
|
|
|
|
socketInstance.on('session-error', (data) => {
|
|
console.error('[ArcadeSocket] Session error', data)
|
|
|
|
// Show error toast unless suppressed
|
|
if (!eventsRef.current.suppressErrorToasts) {
|
|
addError(
|
|
'Game session error',
|
|
`Error: ${data.error}\n\nThis usually means there was a problem loading or updating the game session. Please try refreshing the page or returning to the lobby.`
|
|
)
|
|
}
|
|
|
|
eventsRef.current.onError?.(data)
|
|
})
|
|
|
|
socketInstance.on('pong-session', () => {
|
|
console.log('[ArcadeSocket] Pong received')
|
|
})
|
|
|
|
setSocket(socketInstance)
|
|
|
|
return () => {
|
|
socketInstance.disconnect()
|
|
}
|
|
}, [])
|
|
|
|
const joinSession = useCallback(
|
|
(userId: string, roomId?: string) => {
|
|
if (!socket) {
|
|
console.warn('[ArcadeSocket] Cannot join session - socket not connected')
|
|
return
|
|
}
|
|
console.log(
|
|
'[ArcadeSocket] Joining session for user:',
|
|
userId,
|
|
roomId ? `in room ${roomId}` : '(solo)'
|
|
)
|
|
socket.emit('join-arcade-session', { userId, roomId })
|
|
},
|
|
[socket]
|
|
)
|
|
|
|
const sendMove = useCallback(
|
|
(userId: string, move: GameMove, roomId?: string) => {
|
|
if (!socket) {
|
|
console.warn('[ArcadeSocket] Cannot send move - socket not connected')
|
|
return
|
|
}
|
|
socket.emit('game-move', { userId, move, roomId })
|
|
},
|
|
[socket]
|
|
)
|
|
|
|
const exitSession = useCallback(
|
|
(userId: string) => {
|
|
if (!socket) {
|
|
console.warn('[ArcadeSocket] Cannot exit session - socket not connected')
|
|
return
|
|
}
|
|
console.log('[ArcadeSocket] Exiting session for user:', userId)
|
|
socket.emit('exit-arcade-session', { userId })
|
|
},
|
|
[socket]
|
|
)
|
|
|
|
const pingSession = useCallback(
|
|
(userId: string) => {
|
|
if (!socket) return
|
|
socket.emit('ping-session', { userId })
|
|
},
|
|
[socket]
|
|
)
|
|
|
|
return {
|
|
socket,
|
|
connected,
|
|
joinSession,
|
|
sendMove,
|
|
exitSession,
|
|
pingSession,
|
|
}
|
|
}
|