Route guards, WS lifecycle, ticket stats. Refs EventHub/EventHubFrontAdmin#3
This commit is contained in:
+143
-100
@@ -1,100 +1,143 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
type WsMessage = {
|
||||
type: 'report_created' | 'ticket_created';
|
||||
data?: {
|
||||
report_id?: string;
|
||||
ticket_id?: string;
|
||||
target_type?: string;
|
||||
target_id?: string;
|
||||
reason?: string;
|
||||
};
|
||||
status?: string;
|
||||
channel?: string;
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
export const useAdminWebSocket = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const accessToken = useAuthStore((s) => s.accessToken);
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const lastMessageTimestamp = useRef<number>(0);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !accessToken) return;
|
||||
|
||||
let isMounted = true;
|
||||
const connect = () => {
|
||||
const wsUrl = import.meta.env.DEV
|
||||
? `ws://localhost:5173/admin/ws?token=${accessToken}`
|
||||
: `wss://${window.location.host}/admin/ws?token=${accessToken}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
if (!isMounted) return;
|
||||
console.log('[WS] Connected');
|
||||
ws.send(JSON.stringify({ action: 'subscribe', channel: 'reports' }));
|
||||
ws.send(JSON.stringify({ action: 'subscribe', channel: 'tickets' }));
|
||||
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action: 'ping' }));
|
||||
}
|
||||
}, 30_000);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (!isMounted) return;
|
||||
try {
|
||||
const msg: WsMessage = JSON.parse(event.data);
|
||||
console.log('[WS] Message:', msg);
|
||||
|
||||
if (msg.timestamp && msg.timestamp === lastMessageTimestamp.current) {
|
||||
return;
|
||||
}
|
||||
if (msg.timestamp) {
|
||||
lastMessageTimestamp.current = msg.timestamp;
|
||||
}
|
||||
|
||||
if (msg.type === 'report_created' || msg.type === 'ticket_created') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('admin-ws-message', { detail: msg })
|
||||
);
|
||||
if (msg.type === 'report_created') {
|
||||
queryClient.invalidateQueries({ queryKey: ['reports'] });
|
||||
} else if (msg.type === 'ticket_created') {
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['ticket-stats'] });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[WS] Failed to parse message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (!isMounted) return;
|
||||
console.log('[WS] Disconnected, will reconnect in 5s');
|
||||
if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
|
||||
reconnectTimeoutRef.current = setTimeout(connect, 5000);
|
||||
};
|
||||
|
||||
// Не вызываем ws.close() при ошибке, браузер закроет сокет сам
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
|
||||
if (pingIntervalRef.current) clearInterval(pingIntervalRef.current);
|
||||
// Никакого принудительного закрытия wsRef.current – браузер сам управится
|
||||
};
|
||||
}, [isAuthenticated, accessToken, queryClient]);
|
||||
};
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useMetricsStore } from '../store/metricsStore';
|
||||
|
||||
type WsMessage = {
|
||||
type: 'report_created' | 'ticket_created' | 'node_metric';
|
||||
data?: any;
|
||||
status?: string;
|
||||
channel?: string;
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
export const useAdminWebSocket = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const accessToken = useAuthStore((s) => s.accessToken);
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const setCurrentMetric = useMetricsStore((s) => s.setCurrent);
|
||||
const addToAllHistory = useMetricsStore((s) => s.addToAllHistory);
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const lastMessageTimestamp = useRef<number>(0);
|
||||
const shouldReconnectRef = useRef(true);
|
||||
|
||||
const clearTimers = () => {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const closeSocket = () => {
|
||||
clearTimers();
|
||||
if (wsRef.current) {
|
||||
wsRef.current.onclose = null;
|
||||
wsRef.current.onerror = null;
|
||||
wsRef.current.onmessage = null;
|
||||
wsRef.current.onopen = null;
|
||||
if (wsRef.current.readyState === WebSocket.OPEN ||
|
||||
wsRef.current.readyState === WebSocket.CONNECTING) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
wsRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !accessToken) {
|
||||
shouldReconnectRef.current = false;
|
||||
closeSocket();
|
||||
return;
|
||||
}
|
||||
|
||||
shouldReconnectRef.current = true;
|
||||
let isMounted = true;
|
||||
|
||||
const connect = () => {
|
||||
if (!isMounted || !shouldReconnectRef.current) return;
|
||||
|
||||
closeSocket();
|
||||
|
||||
const wsUrl = import.meta.env.DEV
|
||||
? `ws://localhost:5173/admin/ws?token=${accessToken}`
|
||||
: `wss://${window.location.host}/admin/ws?token=${accessToken}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onopen = () => {
|
||||
if (!isMounted || !shouldReconnectRef.current) return;
|
||||
console.log('[WS] Connected');
|
||||
ws.send(JSON.stringify({ action: 'subscribe', channel: 'reports' }));
|
||||
ws.send(JSON.stringify({ action: 'subscribe', channel: 'tickets' }));
|
||||
|
||||
pingIntervalRef.current = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action: 'ping' }));
|
||||
}
|
||||
}, 30_000);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (!isMounted) return;
|
||||
try {
|
||||
const msg: WsMessage = JSON.parse(event.data);
|
||||
console.log('[WS] Message:', msg);
|
||||
|
||||
if (msg.timestamp && msg.timestamp === lastMessageTimestamp.current) {
|
||||
return;
|
||||
}
|
||||
if (msg.timestamp) {
|
||||
lastMessageTimestamp.current = msg.timestamp;
|
||||
}
|
||||
|
||||
if (msg.type === 'report_created' || msg.type === 'ticket_created') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('admin-ws-message', { detail: msg })
|
||||
);
|
||||
if (msg.type === 'report_created') {
|
||||
queryClient.invalidateQueries({ queryKey: ['reports'] });
|
||||
} else if (msg.type === 'ticket_created') {
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['ticket-stats'] });
|
||||
}
|
||||
} else if (msg.type === 'node_metric') {
|
||||
setCurrentMetric(msg.data);
|
||||
addToAllHistory(msg.data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[WS] Failed to parse message:', e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
if (!isMounted || !shouldReconnectRef.current) return;
|
||||
console.log('[WS] Disconnected:', event.reason);
|
||||
clearTimers();
|
||||
wsRef.current = null;
|
||||
reconnectTimeoutRef.current = setTimeout(connect, 5000);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
if (!isMounted) return;
|
||||
console.error('[WS] Error:', error);
|
||||
ws.close();
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
shouldReconnectRef.current = false;
|
||||
closeSocket();
|
||||
};
|
||||
}, [isAuthenticated, accessToken, queryClient, setCurrentMetric, addToAllHistory]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user