File size: 2,131 Bytes
4817546
 
604d53b
4817546
604d53b
 
4817546
 
 
 
 
 
 
 
 
604d53b
 
 
 
 
4817546
 
 
 
604d53b
 
 
 
 
 
 
4817546
 
604d53b
4817546
 
 
604d53b
4817546
604d53b
 
 
 
 
 
4817546
 
 
 
604d53b
 
4817546
604d53b
4817546
 
 
 
 
604d53b
 
 
 
 
4817546
 
 
 
 
 
 
 
604d53b
 
 
 
 
 
 
4817546
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import { useEffect, useRef, useState } from "react";

export type ConnectionStatus = "connected" | "reconnecting" | "disconnected";

export function useResilientSocket<T>(url: string, onMessage: (msg: T) => void): ConnectionStatus {
  const [status, setStatus] = useState<ConnectionStatus>("disconnected");
  const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  const reconnectAttemptRef = useRef(0);
  const savedCallback = useRef(onMessage);

  useEffect(() => {
    savedCallback.current = onMessage;
  }, [onMessage]);

  useEffect(() => {
    if (!url) {
      const timer = setTimeout(() => setStatus("disconnected"), 0);
      return () => clearTimeout(timer);
    }

    let ws: WebSocket;
    let isComponentMounted = true;

    const connect = () => {
      try {
        ws = new WebSocket(url);
      } catch {
        setStatus("disconnected");
        return;
      }

      ws.onopen = () => {
        if (!isComponentMounted) return;
        setStatus("connected");
        reconnectAttemptRef.current = 0; // Reset attempts
      };

      ws.onmessage = (event: MessageEvent) => {
        if (!isComponentMounted) return;
        try {
          const msg = JSON.parse(event.data) as T;
          savedCallback.current(msg);
        } catch {
          // ignore malformed frame
        }
      };

      ws.onclose = () => {
        if (!isComponentMounted) return;
        setStatus("reconnecting");
        const timeout = Math.min(1000 * 2 ** reconnectAttemptRef.current, 30000); // Max 30s
        reconnectAttemptRef.current += 1;

        reconnectTimeoutRef.current = setTimeout(connect, timeout);
      };

      ws.onerror = () => {
        if (!isComponentMounted) return;
        try {
          ws.close();
        } catch {
          // ignore already closed
        }
      };
    };

    connect();

    return () => {
      isComponentMounted = false;
      if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
      if (ws) {
        try {
          ws.close();
        } catch {
          // ignore
        }
      }
    };
  }, [url]);

  return status;
}