mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
- Remove legacy .eslintrc.cjs and .eslintignore - Add eslint.config.js with flat config, security rules (no-eval, no-implied-eval, no-new-func, no-script-url) - Add .prettierrc and .prettierignore - Format entire codebase with prettier
160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
import { useAuthStore } from '../store/auth';
|
|
|
|
const isDev = import.meta.env.DEV;
|
|
|
|
export interface WSMessage {
|
|
type: string;
|
|
ticket_id?: number;
|
|
message?: string;
|
|
title?: string;
|
|
user_id?: number;
|
|
is_admin?: boolean;
|
|
}
|
|
|
|
interface UseWebSocketOptions {
|
|
onMessage?: (message: WSMessage) => void;
|
|
onConnect?: () => void;
|
|
onDisconnect?: () => void;
|
|
}
|
|
|
|
export function useWebSocket(options: UseWebSocketOptions = {}) {
|
|
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;
|
|
const optionsRef = useRef(options);
|
|
|
|
// Update options ref when they change
|
|
useEffect(() => {
|
|
optionsRef.current = options;
|
|
}, [options]);
|
|
|
|
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
|
|
}
|
|
}
|
|
// If apiUrl is relative (like /api), 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;
|
|
optionsRef.current.onConnect?.();
|
|
|
|
// 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;
|
|
}
|
|
|
|
optionsRef.current.onMessage?.(message);
|
|
} 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);
|
|
optionsRef.current.onDisconnect?.();
|
|
|
|
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]);
|
|
|
|
return { isConnected };
|
|
}
|