Commit
This commit is contained in:
190
pwa/src/hooks/useGameWebSocket.ts
Normal file
190
pwa/src/hooks/useGameWebSocket.ts
Normal 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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user