mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix: use single shared WebSocket connection and optimize build chunks
This commit is contained in:
217
src/providers/WebSocketProvider.tsx
Normal file
217
src/providers/WebSocketProvider.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useAuthStore } from '../store/auth';
|
||||
|
||||
const isDev = import.meta.env.DEV;
|
||||
|
||||
export interface WSMessage {
|
||||
type: string;
|
||||
// Ticket events
|
||||
ticket_id?: number;
|
||||
title?: string;
|
||||
// Common
|
||||
message?: string;
|
||||
user_id?: number;
|
||||
is_admin?: boolean;
|
||||
// Balance events
|
||||
amount_kopeks?: number;
|
||||
amount_rubles?: number;
|
||||
new_balance_kopeks?: number;
|
||||
new_balance_rubles?: number;
|
||||
description?: string;
|
||||
// Subscription events
|
||||
expires_at?: string;
|
||||
new_expires_at?: string;
|
||||
tariff_name?: string;
|
||||
days_left?: number;
|
||||
// Device purchase events
|
||||
devices_added?: number;
|
||||
new_device_limit?: number;
|
||||
// Traffic purchase events
|
||||
traffic_gb_added?: number;
|
||||
new_traffic_limit_gb?: number;
|
||||
// Autopay events
|
||||
required_kopeks?: number;
|
||||
required_rubles?: number;
|
||||
balance_kopeks?: number;
|
||||
balance_rubles?: number;
|
||||
reason?: string;
|
||||
// Account events (ban/warning)
|
||||
// Referral events
|
||||
bonus_kopeks?: number;
|
||||
bonus_rubles?: number;
|
||||
referral_name?: string;
|
||||
// Payment events
|
||||
payment_method?: string;
|
||||
}
|
||||
|
||||
type MessageHandler = (message: WSMessage) => void;
|
||||
|
||||
interface WebSocketContextValue {
|
||||
isConnected: boolean;
|
||||
subscribe: (handler: MessageHandler) => () => void;
|
||||
}
|
||||
|
||||
const WebSocketContext = createContext<WebSocketContextValue | null>(null);
|
||||
|
||||
export function WebSocketProvider({ children }: { children: React.ReactNode }) {
|
||||
const { accessToken, isAuthenticated } = useAuthStore();
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const reconnectAttemptsRef = useRef(0);
|
||||
const maxReconnectAttempts = 5;
|
||||
|
||||
// Store message handlers
|
||||
const handlersRef = useRef<Set<MessageHandler>>(new Set());
|
||||
|
||||
const cleanup = useCallback(() => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!accessToken || !isAuthenticated) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't reconnect if already connected
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
// Build WebSocket URL
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
let host = window.location.host;
|
||||
|
||||
// Handle VITE_API_URL - can be absolute URL or relative path
|
||||
const apiUrl = import.meta.env.VITE_API_URL;
|
||||
if (apiUrl && (apiUrl.startsWith('http://') || apiUrl.startsWith('https://'))) {
|
||||
try {
|
||||
host = new URL(apiUrl).host;
|
||||
} catch {
|
||||
// If URL parsing fails, use window.location.host
|
||||
}
|
||||
}
|
||||
|
||||
const wsUrl = `${protocol}//${host}/cabinet/ws?token=${accessToken}`;
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
if (isDev) console.log('[WS] Connected');
|
||||
setIsConnected(true);
|
||||
reconnectAttemptsRef.current = 0;
|
||||
|
||||
// Setup ping interval (every 25 seconds)
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'ping' }));
|
||||
}
|
||||
}, 25000);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data) as WSMessage;
|
||||
|
||||
// Ignore pong messages
|
||||
if (message.type === 'pong' || message.type === 'connected') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify all subscribers
|
||||
handlersRef.current.forEach((handler) => {
|
||||
try {
|
||||
handler(message);
|
||||
} catch (e) {
|
||||
if (isDev) console.error('[WS] Handler error:', e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (isDev) console.error('[WS] Failed to parse message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
if (isDev) console.log('[WS] Disconnected:', event.code, event.reason);
|
||||
setIsConnected(false);
|
||||
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
|
||||
// Attempt to reconnect if not closed intentionally
|
||||
if (event.code !== 1000 && reconnectAttemptsRef.current < maxReconnectAttempts) {
|
||||
const delay = Math.min(1000 * Math.pow(2, reconnectAttemptsRef.current), 30000);
|
||||
if (isDev)
|
||||
console.log(
|
||||
`[WS] Reconnecting in ${delay}ms (attempt ${reconnectAttemptsRef.current + 1})`,
|
||||
);
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
reconnectAttemptsRef.current++;
|
||||
connect();
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
if (isDev) console.error('[WS] Error:', error);
|
||||
};
|
||||
} catch (e) {
|
||||
if (isDev) console.error('[WS] Failed to connect:', e);
|
||||
}
|
||||
}, [accessToken, isAuthenticated, cleanup]);
|
||||
|
||||
// Connect when authenticated
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && accessToken) {
|
||||
connect();
|
||||
} else {
|
||||
cleanup();
|
||||
setIsConnected(false);
|
||||
}
|
||||
|
||||
return cleanup;
|
||||
}, [isAuthenticated, accessToken, connect, cleanup]);
|
||||
|
||||
// Subscribe function for components
|
||||
const subscribe = useCallback((handler: MessageHandler) => {
|
||||
handlersRef.current.add(handler);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
handlersRef.current.delete(handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={{ isConnected, subscribe }}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useWebSocketContext() {
|
||||
const context = useContext(WebSocketContext);
|
||||
if (!context) {
|
||||
throw new Error('useWebSocketContext must be used within a WebSocketProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user