Quellcode durchsuchen

feat: reconnect with exponential backoff

Fela Maslen vor 4 Jahren
Ursprung
Commit
109d962966
2 geänderte Dateien mit 89 neuen und 20 gelöschten Zeilen
  1. 28 0
      gmus-web/src/hooks/socket.spec.tsx
  2. 61 20
      gmus-web/src/hooks/socket.ts

+ 28 - 0
gmus-web/src/hooks/socket.spec.tsx

@@ -349,4 +349,32 @@ describe(useSocket.name, () => {
       expect(onMessage).toHaveBeenCalledWith(expect.objectContaining({ data: 'foo' }));
     });
   });
+
+  describe('when an error occurs', () => {
+    let server: WS;
+    beforeEach(() => {
+      server = new WS('ws://my-api.url:1234/pubsub');
+    });
+
+    it('should reconnect automatically', async () => {
+      expect.assertions(1);
+      const { getByText } = render(<TestComponent />);
+      act(() => {
+        fireEvent.click(getByText('Identify!'));
+      });
+
+      await server.connected;
+
+      act(() => {
+        server.error();
+      });
+      server = new WS('ws://my-api.url:1234/pubsub');
+
+      await server.connected;
+
+      server.send('foo');
+
+      expect(onMessage).toHaveBeenCalledTimes(1);
+    });
+  });
 });

+ 61 - 20
gmus-web/src/hooks/socket.ts

@@ -61,6 +61,18 @@ export function useDispatchWithEffects(
   return dispatchWithEffects;
 }
 
+const getConnectAttemptDelayMs = (connectAttemptNumber: number): number =>
+  Math.min(16000, 1000 * 2 ** connectAttemptNumber);
+
+type SocketState = {
+  uniqueName: string;
+  tempName: string;
+  socket: WebSocket | null;
+  error: boolean;
+  connecting: boolean;
+  connectAttemptNumber: number;
+};
+
 export function useSocket(
   onMessage: OnMessage,
   onLogin: (name: string) => void,
@@ -73,33 +85,46 @@ export function useSocket(
   connected: boolean;
 } {
   const [storedName, saveName] = useStorageState<string>(localStorage, clientNameKey, '');
-  const [uniqueName, setUniqueName] = useState<string>(getUniqueName(storedName));
-  const [tempName, onIdentify] = useState<string>(storedName);
+  const [{ uniqueName, tempName, socket, error, connecting }, setState] = useState<SocketState>({
+    uniqueName: getUniqueName(storedName),
+    tempName: storedName,
+    socket: null,
+    error: false,
+    connecting: false,
+    connectAttemptNumber: 0,
+  });
 
-  const [socket, setSocket] = useState<WebSocket | null>(null);
-  const [error, setError] = useState<boolean>(false);
-
-  const [connecting, setConnecting] = useState<boolean>(false);
+  const connectAttemptTimer = useRef<number>();
 
   useEffect(() => {
+    if (!tempName) {
+      setState((last) => ({ ...last, connecting: false, error: false }));
+      return (): void => {
+        // pass
+      };
+    }
+
     let cancelled = false;
     let ws: WebSocket | undefined;
-    if (tempName) {
-      setConnecting(true);
+
+    const connect = (): void => {
+      setState((last) => ({ ...last, connecting: true }));
 
       const uniqueTempName = getUniqueName(tempName);
       ws = new WebSocket(`${getPubsubUrl()}?client-name=${uniqueTempName}`);
 
       ws.onopen = (): void => {
         if (!cancelled && ws && ws.readyState === ws.OPEN) {
-          setError(false);
-          setConnecting(false);
+          setState((last) => ({
+            ...last,
+            error: false,
+            connecting: false,
+            connectAttemptNumber: 0,
+            uniqueName: uniqueTempName,
+            socket: ws ?? null,
+          }));
 
-          onIdentify('');
           saveName(tempName);
-          setUniqueName(uniqueTempName);
-
-          setSocket(ws);
           onLogin(uniqueTempName);
         }
       };
@@ -107,19 +132,35 @@ export function useSocket(
       ws.onmessage = onMessage;
 
       ws.onclose = (): void => {
-        setError(false);
-        setSocket(null);
+        setState((last) => ({ ...last, error: false, socket: null }));
       };
-    } else {
-      setConnecting(false);
-      setError(false);
-    }
+
+      ws.onerror = (): void => {
+        setState((last) => {
+          clearTimeout(connectAttemptTimer.current);
+          connectAttemptTimer.current = setTimeout(
+            connect,
+            getConnectAttemptDelayMs(last.connectAttemptNumber),
+          );
+
+          return { ...last, error: true, connectAttemptNumber: last.connectAttemptNumber + 1 };
+        });
+      };
+    };
+
+    connect();
 
     return (): void => {
+      clearTimeout(connectAttemptTimer.current);
       cancelled = true;
     };
   }, [onMessage, onLogin, tempName, saveName]);
 
+  const onIdentify = useCallback(
+    (name: string) => setState((last) => ({ ...last, tempName: name })),
+    [],
+  );
+
   return {
     name: uniqueName,
     onIdentify,