feat(realtime): add ping/pong heartbeat and watchdog reconnect
Some checks failed
CI / test (push) Failing after 20s
Some checks failed
CI / test (push) Failing after 20s
- support ping incoming event and pong outgoing response - add web heartbeat interval to keep ws alive - add stale-connection watchdog to force reconnect on missing pong
This commit is contained in:
@@ -16,6 +16,9 @@ export function useRealtime() {
|
||||
const typingByChat = useRef<Record<number, Set<number>>>({});
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<number | null>(null);
|
||||
const heartbeatIntervalRef = useRef<number | null>(null);
|
||||
const watchdogIntervalRef = useRef<number | null>(null);
|
||||
const lastPongAtRef = useRef<number>(Date.now());
|
||||
const reconnectAttemptsRef = useRef(0);
|
||||
const manualCloseRef = useRef(false);
|
||||
|
||||
@@ -42,6 +45,23 @@ export function useRealtime() {
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectAttemptsRef.current = 0;
|
||||
lastPongAtRef.current = Date.now();
|
||||
if (heartbeatIntervalRef.current !== null) {
|
||||
window.clearInterval(heartbeatIntervalRef.current);
|
||||
}
|
||||
if (watchdogIntervalRef.current !== null) {
|
||||
window.clearInterval(watchdogIntervalRef.current);
|
||||
}
|
||||
heartbeatIntervalRef.current = window.setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ event: "ping", payload: {} }));
|
||||
}
|
||||
}, 20000);
|
||||
watchdogIntervalRef.current = window.setInterval(() => {
|
||||
if (Date.now() - lastPongAtRef.current > 65000 && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
}, 15000);
|
||||
void useChatStore.getState().loadChats();
|
||||
};
|
||||
|
||||
@@ -80,6 +100,9 @@ export function useRealtime() {
|
||||
void chatStore.loadChats();
|
||||
}
|
||||
}
|
||||
if (event.event === "pong") {
|
||||
lastPongAtRef.current = Date.now();
|
||||
}
|
||||
if (event.event === "typing_start") {
|
||||
const chatId = Number(event.payload.chat_id);
|
||||
const userId = Number(event.payload.user_id);
|
||||
@@ -143,6 +166,14 @@ export function useRealtime() {
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (heartbeatIntervalRef.current !== null) {
|
||||
window.clearInterval(heartbeatIntervalRef.current);
|
||||
heartbeatIntervalRef.current = null;
|
||||
}
|
||||
if (watchdogIntervalRef.current !== null) {
|
||||
window.clearInterval(watchdogIntervalRef.current);
|
||||
watchdogIntervalRef.current = null;
|
||||
}
|
||||
if (manualCloseRef.current) {
|
||||
return;
|
||||
}
|
||||
@@ -160,6 +191,14 @@ export function useRealtime() {
|
||||
|
||||
return () => {
|
||||
manualCloseRef.current = true;
|
||||
if (heartbeatIntervalRef.current !== null) {
|
||||
window.clearInterval(heartbeatIntervalRef.current);
|
||||
heartbeatIntervalRef.current = null;
|
||||
}
|
||||
if (watchdogIntervalRef.current !== null) {
|
||||
window.clearInterval(watchdogIntervalRef.current);
|
||||
watchdogIntervalRef.current = null;
|
||||
}
|
||||
if (reconnectTimeoutRef.current !== null) {
|
||||
window.clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
|
||||
Reference in New Issue
Block a user