Selaa lähdekoodia

feat: take control of master if it goes away

Fela Maslen 5 vuotta sitten
vanhempi
commit
882251b794
3 muutettua tiedostoa jossa 122 lisäystä ja 20 poistoa
  1. 3 20
      gmus/src/components/app/index.tsx
  2. 85 0
      gmus/src/hooks/master.spec.tsx
  3. 34 0
      gmus/src/hooks/master.ts

+ 3 - 20
gmus/src/components/app/index.tsx

@@ -1,8 +1,8 @@
-import React, { Dispatch, useCallback, useEffect, useRef } from 'react';
+import React, { Dispatch, useCallback } from 'react';
 
 import { LocalAction, stateSet } from '../../actions';
-import { masterStateUpdateTimeout } from '../../constants/system';
 import { DispatchContext, StateContext } from '../../context/state';
+import { useMaster } from '../../hooks/master';
 import { useKeepalive } from '../../hooks/socket';
 import { GlobalState } from '../../reducer/types';
 import { getSongUrl } from '../../utils/url';
@@ -16,29 +16,12 @@ export type Props = {
   dispatch: Dispatch<LocalAction>;
 };
 
-function useMaster(dispatch: Dispatch<LocalAction>, isMaster: boolean): void {
-  const masterUpdateTimer = useRef<number>(0);
-  useEffect(() => {
-    if (isMaster) {
-      masterUpdateTimer.current = window.setInterval(() => {
-        dispatch(stateSet());
-      }, masterStateUpdateTimeout);
-    }
-
-    return (): void => {
-      window.clearInterval(masterUpdateTimer.current);
-    };
-  }, [dispatch, isMaster]);
-}
-
 const uiProvider = UIProvider.Cmus;
 const UI = uiProviders[uiProvider];
 
 export const App: React.FC<Props> = ({ socket, state, dispatch }) => {
-  const isMaster = state.player.master === state.myClientName;
-
   useKeepalive(socket);
-  useMaster(dispatch, isMaster);
+  const isMaster = useMaster(dispatch, state.player.master, state.myClientName);
 
   const onTimeUpdate = useCallback(
     (currentTime: number): void => {

+ 85 - 0
gmus/src/hooks/master.spec.tsx

@@ -0,0 +1,85 @@
+import { act, render } from '@testing-library/react';
+import React from 'react';
+
+import { stateSet } from '../actions';
+import { masterStateUpdateTimeout } from '../constants/system';
+
+import { useMaster } from './master';
+
+describe(useMaster.name, () => {
+  const dispatch = jest.fn();
+
+  const TestComponent: React.FC<{ master: string; myClientName: string }> = ({
+    master,
+    myClientName,
+  }) => {
+    useMaster(dispatch, master, myClientName);
+    return null;
+  };
+
+  describe('when there is no master initially', () => {
+    it('should take control of master', () => {
+      expect.assertions(2);
+      const { unmount } = render(<TestComponent master="" myClientName="my-client-name" />);
+
+      expect(dispatch).toHaveBeenCalledTimes(1);
+      expect(dispatch).toHaveBeenCalledWith(stateSet({ master: 'my-client-name' }));
+
+      unmount();
+    });
+  });
+
+  describe('when the client is master', () => {
+    it('should continually refresh the server with the current state', () => {
+      expect.assertions(6);
+      const clock = jest.useFakeTimers();
+      const { unmount } = render(
+        <TestComponent master="the-master-client" myClientName="the-master-client" />,
+      );
+
+      act(() => {
+        clock.runTimersToTime(masterStateUpdateTimeout - 1);
+      });
+
+      expect(dispatch).toHaveBeenCalledTimes(0);
+      act(() => {
+        clock.runTimersToTime(1);
+      });
+
+      expect(dispatch).toHaveBeenCalledTimes(1);
+      expect(dispatch).toHaveBeenCalledWith(stateSet());
+
+      dispatch.mockClear();
+      expect(dispatch).toHaveBeenCalledTimes(0);
+
+      act(() => {
+        clock.runTimersToTime(masterStateUpdateTimeout);
+      });
+
+      expect(dispatch).toHaveBeenCalledTimes(1);
+      expect(dispatch).toHaveBeenCalledWith(stateSet());
+
+      unmount();
+      jest.useRealTimers();
+    });
+  });
+
+  describe('when the client is a slave', () => {
+    it('should not send state updates periodically', () => {
+      expect.assertions(1);
+      const clock = jest.useFakeTimers();
+      const { unmount } = render(
+        <TestComponent master="the-master-client" myClientName="my-client-name" />,
+      );
+
+      act(() => {
+        clock.runTimersToTime(masterStateUpdateTimeout);
+      });
+
+      expect(dispatch).not.toHaveBeenCalled();
+
+      unmount();
+      jest.useRealTimers();
+    });
+  });
+});

+ 34 - 0
gmus/src/hooks/master.ts

@@ -0,0 +1,34 @@
+import { Dispatch, useEffect, useRef } from 'react';
+
+import { LocalAction, stateSet } from '../actions';
+import { masterStateUpdateTimeout } from '../constants/system';
+
+export function useMaster(
+  dispatch: Dispatch<LocalAction>,
+  master: string,
+  myClientName: string,
+): boolean {
+  const isMaster = master === myClientName;
+
+  const masterUpdateTimer = useRef<number>(0);
+  useEffect(() => {
+    if (isMaster) {
+      masterUpdateTimer.current = window.setInterval(() => {
+        dispatch(stateSet());
+      }, masterStateUpdateTimeout);
+    }
+
+    return (): void => {
+      window.clearInterval(masterUpdateTimer.current);
+    };
+  }, [dispatch, isMaster]);
+
+  const noMaster = !master;
+  useEffect(() => {
+    if (noMaster) {
+      dispatch(stateSet({ master: myClientName }));
+    }
+  }, [dispatch, noMaster, myClientName]);
+
+  return isMaster;
+}