This commit is contained in:
Joan
2025-11-27 16:27:01 +01:00
parent 33cc9586c2
commit 81f8912059
304 changed files with 56149 additions and 10122 deletions

View File

@@ -0,0 +1,190 @@
import { useEffect, useRef, useState, useCallback } from 'react';
interface WebSocketMessage {
type: string;
data?: any;
message?: string;
timestamp?: string;
[key: string]: any;
}
interface UseGameWebSocketProps {
token: string | null;
onMessage: (message: WebSocketMessage) => void;
enabled?: boolean;
}
interface UseGameWebSocketReturn {
isConnected: boolean;
sendMessage: (message: any) => void;
reconnect: () => void;
}
/**
* Custom hook for managing WebSocket connection to the game server.
* Provides automatic reconnection, heartbeat, and message handling.
*/
export const useGameWebSocket = ({
token,
onMessage,
enabled = true
}: UseGameWebSocketProps): UseGameWebSocketReturn => {
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<number | null>(null);
const heartbeatIntervalRef = useRef<number | null>(null);
const reconnectAttemptsRef = useRef(0);
const maxReconnectAttempts = 5;
const reconnectDelay = 3000; // 3 seconds
// Get WebSocket URL based on current environment
const getWebSocketUrl = useCallback(() => {
const API_BASE = import.meta.env.VITE_API_URL || (
import.meta.env.PROD
? 'https://api-staging.echoesoftheash.com'
: 'http://localhost:8000'
);
// Remove /api suffix if present and convert http(s) to ws(s)
const wsBase = API_BASE.replace(/\/api$/, '').replace(/^http/, 'ws');
return `${wsBase}/ws/game/${token}`;
}, [token]);
// Send heartbeat to keep connection alive
const sendHeartbeat = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: 'heartbeat' }));
}
}, []);
// Connect to WebSocket
const connect = useCallback(() => {
if (!token || !enabled) return;
try {
const wsUrl = getWebSocketUrl();
console.log('🔌 Connecting to WebSocket:', wsUrl);
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => {
console.log('✅ WebSocket connected');
setIsConnected(true);
reconnectAttemptsRef.current = 0;
// Start heartbeat interval (every 30 seconds)
heartbeatIntervalRef.current = setInterval(sendHeartbeat, 30000);
};
ws.onmessage = (event) => {
try {
const message: WebSocketMessage = JSON.parse(event.data);
// Handle heartbeat acks silently
if (message.type === 'heartbeat_ack' || message.type === 'pong') {
return;
}
// Pass message to handler
onMessage(message);
} catch (error) {
console.error('❌ Error parsing WebSocket message:', error);
}
};
ws.onerror = (error) => {
console.error('❌ WebSocket error:', error);
};
ws.onclose = () => {
console.log('🔌 WebSocket disconnected');
setIsConnected(false);
// Clear heartbeat interval
if (heartbeatIntervalRef.current) {
clearInterval(heartbeatIntervalRef.current);
heartbeatIntervalRef.current = null;
}
// Attempt reconnection if enabled and under max attempts
if (enabled && reconnectAttemptsRef.current < maxReconnectAttempts) {
reconnectAttemptsRef.current += 1;
console.log(
`🔄 Reconnecting... (attempt ${reconnectAttemptsRef.current}/${maxReconnectAttempts})`
);
reconnectTimeoutRef.current = setTimeout(() => {
connect();
}, reconnectDelay);
} else if (reconnectAttemptsRef.current >= maxReconnectAttempts) {
console.error('❌ Max reconnection attempts reached');
}
};
} catch (error) {
console.error('❌ Error creating WebSocket:', error);
}
}, [token, enabled, getWebSocketUrl, onMessage, sendHeartbeat]);
// Disconnect from WebSocket
const disconnect = useCallback(() => {
// Clear reconnection timeout
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
// Clear heartbeat interval
if (heartbeatIntervalRef.current) {
clearInterval(heartbeatIntervalRef.current);
heartbeatIntervalRef.current = null;
}
// Close WebSocket
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
setIsConnected(false);
}, []);
// Send message through WebSocket
const sendMessage = useCallback((message: any) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify(message));
} else {
console.warn('⚠️ WebSocket not connected, cannot send message');
}
}, []);
// Manual reconnect function
const reconnect = useCallback(() => {
disconnect();
reconnectAttemptsRef.current = 0;
setTimeout(connect, 500);
}, [connect, disconnect]);
// Effect: Connect/disconnect based on token and enabled status
useEffect(() => {
if (!token || !enabled) {
return;
}
// Connect on mount
connect();
// Cleanup on unmount or when dependencies change
return () => {
disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token, enabled]); // Only reconnect when token or enabled changes
return {
isConnected,
sendMessage,
reconnect
};
};