Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion src/hooks/sockets/useChairmanSocket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe('useChairmanSocket', () => {
expect(latestCallback).toHaveBeenCalledTimes(1);
});

it('소켓이 연결(재연결 포함)되면 onSyncRequest를 호출해 현재 상태를 먼저 공유해야 한다', () => {
it('소켓이 연결(재연결 포함)되면 채널 재구독이 끝난 뒤 onSyncRequest를 호출해 현재 상태를 먼저 공유해야 한다', async () => {
const connectionListeners: Array<() => void> = [];
addConnectionListener.mockImplementation((listener: () => void) => {
connectionListeners.push(listener);
Expand All @@ -344,6 +344,13 @@ describe('useChairmanSocket', () => {
connectionListeners.forEach((listener) => listener());
});

// 연결 리스너 실행 시점(재구독 전)에는 아직 공유하지 않는다
expect(onSyncRequest).not.toHaveBeenCalled();

await act(async () => {
await Promise.resolve();
});

expect(onSyncRequest).toHaveBeenCalledTimes(1);
});

Expand All @@ -368,6 +375,44 @@ describe('useChairmanSocket', () => {
};
}

it('사회자 채널을 구독할 때 최신 사회자 토큰을 첨부해야 한다', () => {
let resolveHeaders: () => Record<string, string> = () => ({});
subscribe.mockImplementation(
(
_destination: string,
_callback: (message: IMessage) => void,
headers: () => Record<string, string>,
) => {
resolveHeaders = headers;
},
);
let token = 'first-token';

renderHook(() => useChairmanSocket(123, { getAuthToken: () => token }));

expect(resolveHeaders().Authorization).toBe('first-token');

token = 'refreshed-token';
expect(resolveHeaders().Authorization).toBe('refreshed-token');
});

it('사회자 토큰이 없으면 Authorization 헤더를 첨부하지 않아야 한다', () => {
let resolveHeaders: () => Record<string, string> = () => ({});
subscribe.mockImplementation(
(
_destination: string,
_callback: (message: IMessage) => void,
headers: () => Record<string, string>,
) => {
resolveHeaders = headers;
},
);

renderHook(() => useChairmanSocket(123, { getAuthToken: () => null }));

expect(resolveHeaders()).not.toHaveProperty('Authorization');
});

it('사회자 채널 구독과 이벤트 발행에 같은 사회자 세션 식별자를 첨부해야 한다', () => {
const channel = captureChairmanChannel();

Expand Down
28 changes: 25 additions & 3 deletions src/hooks/sockets/useChairmanSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,20 @@ interface UseChairmanSocketOptions {
* 연결된 동안에는 heartbeat로 `CHAIRMAN_HEARTBEAT_INTERVAL_MS`마다 호출됩니다.
*/
onSyncRequest?: () => void;

/**
* 사회자 채널을 구독할 때 첨부할 사회자 토큰을 반환합니다.
* 서버는 유효한 사회자 토큰을 가진 테이블 소유자의 구독만 허용합니다.
* 재연결 후 다시 구독할 때마다 호출되므로 갱신된 토큰이 첨부됩니다.
*/
getAuthToken?: () => string | null | undefined;
}

export default function useChairmanSocket(
roomId: number,
options: UseChairmanSocketOptions = {},
) {
const { onSyncRequest } = options;
const { onSyncRequest, getAuthToken } = options;
const queryClient = useQueryClient();
const {
connect,
Expand Down Expand Up @@ -97,6 +104,11 @@ export default function useChairmanSocket(
onSyncRequestRef.current = onSyncRequest;
}, [onSyncRequest]);

const getAuthTokenRef = useRef(getAuthToken);
useEffect(() => {
getAuthTokenRef.current = getAuthToken;
}, [getAuthToken]);

// 마지막으로 발행한 이벤트의 version
const versionRef = useRef(0);

Expand Down Expand Up @@ -148,7 +160,11 @@ export default function useChairmanSocket(
useEffect(() => {
return addConnectionListener(() => {
resetSignalState();
onSyncRequestRef.current?.();
// 연결 리스너는 채널 재구독보다 먼저 실행되므로, 사회자 채널 구독(활성 사회자 등록)이
// 서버에 먼저 도착하도록 재구독이 끝난 뒤에 현재 상태를 공유한다
queueMicrotask(() => {
onSyncRequestRef.current?.();
});
});
}, [addConnectionListener, resetSignalState]);

Expand Down Expand Up @@ -183,7 +199,13 @@ export default function useChairmanSocket(
setLastSignalTime(Date.now());
onSyncRequestRef.current?.();
},
() => ({ [CHAIRMAN_SESSION_HEADER]: chairmanSessionIdRef.current }),
() => {
const authToken = getAuthTokenRef.current?.();
return {
[CHAIRMAN_SESSION_HEADER]: chairmanSessionIdRef.current,
...(authToken ? { Authorization: authToken } : {}),
};
},
);

return () => {
Expand Down
9 changes: 8 additions & 1 deletion src/page/TimerPage/hooks/useLiveShare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ export function useLiveShare(
isLiveShareModalOpen && isValidTableId,
);

// 사회자 채널을 (재)구독할 때 최신 사회자 토큰을 첨부하기 위해 ref로 보관
const chairmanTokenRef = useRef(chairmanToken);
useEffect(() => {
chairmanTokenRef.current = chairmanToken;
}, [chairmanToken]);
const getAuthToken = useCallback(() => chairmanTokenRef.current, []);

// 소켓 훅
const {
connect,
Expand All @@ -55,7 +62,7 @@ export function useLiveShare(
isReplaced,
sendDebateEvent,
error: socketError,
} = useChairmanSocket(tableId, { onSyncRequest });
} = useChairmanSocket(tableId, { onSyncRequest, getAuthToken });

// 공유용 URL
const shareUrl = useMemo(() => {
Expand Down
Loading