fixed bug causing websockets to reconnect after leaving a page
This commit is contained in:
parent
72afd06309
commit
d6e3fe7a16
2 changed files with 84 additions and 38 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { useEffect, useRef, useMemo } from "react";
|
||||
import { getWsBaseUrl } from "utils/getWsBaseUrl";
|
||||
|
||||
/** Off by default to reduce server load; set VITE_ENABLE_WEBSOCKETS=true to enable live streams. */
|
||||
|
|
@ -36,10 +36,47 @@ export function useDeviceStatusStreams(options: UseDeviceStatusStreamsOptions) {
|
|||
const wsMapRef = useRef<Map<string, WebSocket>>(new Map());
|
||||
const retriesRef = useRef<Map<string, number>>(new Map());
|
||||
const reconnectTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
||||
/** Bumps only in effect cleanup so late onclose / reconnect timers never run after unmount. */
|
||||
const generationRef = useRef(0);
|
||||
/** Latest ID set so a device dropped from the list does not reconnect on abnormal close. */
|
||||
const allowedIdsRef = useRef<Set<string>>(new Set());
|
||||
allowedIdsRef.current = new Set(deviceIds);
|
||||
|
||||
const connect = useCallback(
|
||||
(deviceId: string) => {
|
||||
// Normalize for stable comparison: same set of IDs = same string regardless of order
|
||||
const deviceIdsKey = useMemo(
|
||||
() => [...new Set(deviceIds)].sort().join(","),
|
||||
[deviceIds.join(",")]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const clearReconnect = (deviceId: string) => {
|
||||
const t = reconnectTimeoutsRef.current.get(deviceId);
|
||||
if (t) {
|
||||
clearTimeout(t);
|
||||
reconnectTimeoutsRef.current.delete(deviceId);
|
||||
}
|
||||
};
|
||||
|
||||
const shutdownAll = () => {
|
||||
reconnectTimeoutsRef.current.forEach((t) => clearTimeout(t));
|
||||
reconnectTimeoutsRef.current.clear();
|
||||
wsMapRef.current.forEach((ws) => ws.close(1000, "cleanup"));
|
||||
wsMapRef.current.clear();
|
||||
retriesRef.current.clear();
|
||||
};
|
||||
|
||||
if (!WEBSOCKETS_ENABLED || !enabled || !token || deviceIds.length === 0) {
|
||||
shutdownAll();
|
||||
return () => {
|
||||
generationRef.current += 1;
|
||||
};
|
||||
}
|
||||
|
||||
const gen = generationRef.current;
|
||||
|
||||
const connect = (deviceId: string) => {
|
||||
if (!token || !deviceId) return;
|
||||
if (gen !== generationRef.current) return;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set("token", token);
|
||||
|
|
@ -47,18 +84,19 @@ export function useDeviceStatusStreams(options: UseDeviceStatusStreamsOptions) {
|
|||
if (types?.length) params.set("types", types.toString());
|
||||
if (as) params.set("as", as);
|
||||
|
||||
// Match useWebSocket / pond routes: /v1/live/devices/:id/status
|
||||
const path = `/v1/live/devices/${deviceId}/status`;
|
||||
const url = `${getWsBaseUrl()}${path}?${params.toString()}`;
|
||||
const ws = new WebSocket(url);
|
||||
wsMapRef.current.set(deviceId, ws);
|
||||
|
||||
ws.onopen = () => {
|
||||
if (gen !== generationRef.current) return;
|
||||
console.debug(`[ws] connected: ${path}`);
|
||||
retriesRef.current.set(deviceId, 0);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (gen !== generationRef.current) return;
|
||||
try {
|
||||
const raw = JSON.parse(event.data);
|
||||
const status = pond.DeviceStatus.fromObject(raw ?? {});
|
||||
|
|
@ -75,7 +113,15 @@ export function useDeviceStatusStreams(options: UseDeviceStatusStreamsOptions) {
|
|||
ws.onclose = (event) => {
|
||||
wsMapRef.current.delete(deviceId);
|
||||
|
||||
// Don't reconnect on clean close or auth rejection
|
||||
if (gen !== generationRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowedIdsRef.current.has(deviceId)) {
|
||||
retriesRef.current.delete(deviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.code === 1000 || event.code === 4001 || event.code === 4003) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -85,55 +131,36 @@ export function useDeviceStatusStreams(options: UseDeviceStatusStreamsOptions) {
|
|||
retriesRef.current.set(deviceId, retries + 1);
|
||||
console.debug(`[ws] reconnecting ${path} in ${delay}ms...`);
|
||||
|
||||
const timeout = setTimeout(() => connect(deviceId), delay);
|
||||
const timeout = setTimeout(() => {
|
||||
if (gen !== generationRef.current) return;
|
||||
if (!allowedIdsRef.current.has(deviceId)) return;
|
||||
reconnectTimeoutsRef.current.delete(deviceId);
|
||||
connect(deviceId);
|
||||
}, delay);
|
||||
reconnectTimeoutsRef.current.set(deviceId, timeout);
|
||||
};
|
||||
},
|
||||
[token, keys?.join(), types?.join(), as]
|
||||
);
|
||||
|
||||
// Normalize for stable comparison: same set of IDs = same string regardless of order
|
||||
const deviceIdsKey = useMemo(
|
||||
() => [...new Set(deviceIds)].sort().join(","),
|
||||
[deviceIds.join(",")]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!WEBSOCKETS_ENABLED || !enabled || !token || deviceIds.length === 0) {
|
||||
wsMapRef.current.forEach((ws) => ws.close(1000, "disabled"));
|
||||
wsMapRef.current.clear();
|
||||
reconnectTimeoutsRef.current.forEach((t) => clearTimeout(t));
|
||||
reconnectTimeoutsRef.current.clear();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const currentIds = new Set(deviceIds);
|
||||
|
||||
// Connect to new devices
|
||||
deviceIds.forEach((id) => {
|
||||
if (!wsMapRef.current.has(id)) {
|
||||
connect(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Disconnect from devices no longer in the list
|
||||
wsMapRef.current.forEach((ws, id) => {
|
||||
if (!currentIds.has(id)) {
|
||||
clearReconnect(id);
|
||||
ws.close(1000, "device removed");
|
||||
wsMapRef.current.delete(id);
|
||||
const t = reconnectTimeoutsRef.current.get(id);
|
||||
if (t) {
|
||||
clearTimeout(t);
|
||||
reconnectTimeoutsRef.current.delete(id);
|
||||
}
|
||||
retriesRef.current.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
wsMapRef.current.forEach((ws) => ws.close(1000, "cleanup"));
|
||||
wsMapRef.current.clear();
|
||||
reconnectTimeoutsRef.current.forEach((t) => clearTimeout(t));
|
||||
reconnectTimeoutsRef.current.clear();
|
||||
generationRef.current += 1;
|
||||
shutdownAll();
|
||||
};
|
||||
}, [enabled, token, deviceIdsKey, deviceIds.length, connect]);
|
||||
}, [enabled, token, deviceIdsKey, deviceIds.length, keys?.join(), types?.join(), as]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,10 +48,14 @@ export function useWebSocket<T>(options: UseWebSocketOptions<T>) {
|
|||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const retriesRef = useRef(0);
|
||||
/** Bumps on each effect cleanup so late onclose / reconnect timers never run after unmount or dependency change. */
|
||||
const generationRef = useRef(0);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (!token) return;
|
||||
|
||||
const gen = generationRef.current;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set("token", token);
|
||||
if (rate > 0) params.set("rate", rate.toString());
|
||||
|
|
@ -85,6 +89,10 @@ export function useWebSocket<T>(options: UseWebSocketOptions<T>) {
|
|||
console.debug(`[ws] closed: ${path} (code: ${event.code})`);
|
||||
wsRef.current = null;
|
||||
|
||||
if (gen !== generationRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't reconnect on clean close or auth rejection
|
||||
if (event.code === 1000 || event.code === 4001 || event.code === 4003) {
|
||||
return;
|
||||
|
|
@ -94,22 +102,33 @@ export function useWebSocket<T>(options: UseWebSocketOptions<T>) {
|
|||
const delay = Math.min(1000 * Math.pow(2, retriesRef.current), 30000);
|
||||
retriesRef.current += 1;
|
||||
console.debug(`[ws] reconnecting in ${delay}ms...`);
|
||||
reconnectTimeoutRef.current = setTimeout(connect, delay);
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
if (gen !== generationRef.current) return;
|
||||
connect();
|
||||
}, delay);
|
||||
};
|
||||
}, [path, token, rate, keys?.join(), types?.join(), as]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!WEBSOCKETS_ENABLED || !enabled || !token) {
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close(1000, "disabled");
|
||||
wsRef.current = null;
|
||||
}
|
||||
return;
|
||||
retriesRef.current = 0;
|
||||
return () => {
|
||||
generationRef.current += 1;
|
||||
};
|
||||
}
|
||||
|
||||
retriesRef.current = 0;
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
generationRef.current += 1;
|
||||
if (reconnectTimeoutRef.current) {
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue