From 16a5939bb2ffaa94a87199baed3e468451e3dff7 Mon Sep 17 00:00:00 2001 From: Kislay Date: Wed, 19 Jun 2024 03:02:27 +0530 Subject: [PATCH] polling: Create module to handle polling Created `channel.ts` which polls and dispatches the received event via handlers configurable through setters. The polling begins as soon as the user is logged in, and is interrupted when the user logs out. TODO: Implement retrying polling after a fixed time in case of a connection failure. --- backend/src/controllers/gameControllers.ts | 10 ++- backend/src/types.ts | 16 +++- .../src/uno-game-engine/events/joinGame.ts | 2 +- .../src/uno-game-engine/events/leaveGame.ts | 2 +- frontend/src/channel.ts | 78 +++++++++++++++++++ frontend/src/contexts/AuthContext.tsx | 4 + frontend/src/contexts/GameContext.tsx | 35 +++++---- 7 files changed, 124 insertions(+), 23 deletions(-) create mode 100644 frontend/src/channel.ts diff --git a/backend/src/controllers/gameControllers.ts b/backend/src/controllers/gameControllers.ts index 7ea13dd..c5018f9 100644 --- a/backend/src/controllers/gameControllers.ts +++ b/backend/src/controllers/gameControllers.ts @@ -81,6 +81,14 @@ function propagateChanges(game: GameEngine) { // to their local game state, but that would be an extra implementation burden. // Instead, we can just send the new game state to the clients. for (const player of game.players) { - enqueueForSend(player.id, game); + enqueueForSend(player.id, { + type: 'STATE_SYNC', + data: { + players: game.players, + cards: game.cardDeck, + currentTurn: game.currentPlayerIndex, + lastThrownCard: game.lastThrownCard?.id || '', + }, + }); } } diff --git a/backend/src/types.ts b/backend/src/types.ts index dcea6bb..40cc553 100644 --- a/backend/src/types.ts +++ b/backend/src/types.ts @@ -1,8 +1,6 @@ // We declare those types which are used throughout the application here. // For types that are used only in one file, we can declare them in that file itself. -import { GameEngine } from './uno-game-engine/engine'; - export type CardType = 'number' | 'special' | 'wild'; export type CardColor = 'red' | 'blue' | 'green' | 'yellow' | 'wild'; @@ -48,7 +46,8 @@ export type GameEventType = | 'DRAW_CARD' | 'THROW_CARD' | 'JOIN_GAME' - | 'LEAVE_GAME'; + | 'LEAVE_GAME' + | 'STATE_SYNC'; export type GameEvent = | { @@ -72,9 +71,18 @@ export type GameEvent = | { type: 'LEAVE_GAME'; playerId: string; + } + | { + type: 'STATE_SYNC'; + data: { + players: Player[]; + cards: UNOCard[]; + currentTurn: number; + lastThrownCard: string; + }; }; // Represent all the events that can be sent to the client // a workaround for now to make things work - this will be refactored later -export type AppEvent = GameEvent | GameEngine; +export type AppEvent = GameEvent; //todo: Add more events diff --git a/backend/src/uno-game-engine/events/joinGame.ts b/backend/src/uno-game-engine/events/joinGame.ts index 33fe97d..468c817 100644 --- a/backend/src/uno-game-engine/events/joinGame.ts +++ b/backend/src/uno-game-engine/events/joinGame.ts @@ -1,4 +1,4 @@ -import { assert } from 'console'; +import assert from 'assert'; import { GameEngine } from '../engine'; import { EventResult, GameEvent, Player } from '../../types'; diff --git a/backend/src/uno-game-engine/events/leaveGame.ts b/backend/src/uno-game-engine/events/leaveGame.ts index c575102..5aa216c 100644 --- a/backend/src/uno-game-engine/events/leaveGame.ts +++ b/backend/src/uno-game-engine/events/leaveGame.ts @@ -1,4 +1,4 @@ -import { assert } from 'console'; +import assert from 'assert'; import { GameEngine } from '../engine'; import { EventResult, GameEvent } from '../../types'; import { getPlayer } from './eventHandlerUtils'; diff --git a/frontend/src/channel.ts b/frontend/src/channel.ts new file mode 100644 index 0000000..df160ea --- /dev/null +++ b/frontend/src/channel.ts @@ -0,0 +1,78 @@ +// this module is responsible for back and forth communication between the frontend and the backend +// using the long polling mechanism + +// todo: extract the event types from the backend and use them here +import * as types from './../../backend/src/types'; + +const GAME_EVENTS = { + DRAW_CARD: 'DRAW_CARD', + THROW_CARD: 'THROW_CARD', + JOIN_GAME: 'JOIN_GAME', + LEAVE_GAME: 'LEAVE_GAME', + STATE_SYNC: 'STATE_SYNC', +}; + +let jwt: string = ''; + +let gameEventsDispatcher: (event: types.AppEvent) => void = () => {}; + +export function setGameEventsDispatcher( + dispatcher: (event: types.AppEvent) => void +) { + gameEventsDispatcher = dispatcher; +} +let abortController: AbortController | null = null; +async function poll() { + if (abortController) { + abortController.abort(); + } + abortController = new AbortController(); + console.log('Polling'); + const res = await fetch( + `${process.env.REACT_APP_BACKEND_URL}/game/events`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: jwt, + }, + signal: abortController.signal, + } + ); + if (!res.ok) { + throw new Error((await res.json()).error); + } + const data: types.AppEvent = await res.json(); + console.log('Received event:', data); + if (GAME_EVENTS[data.type]) { + // it is a game event + gameEventsDispatcher(data); + } else { + console.log('No handler for event type: ', data.type); + } +} + +function pollLoop() { + poll() + .then(() => { + pollLoop(); + }) + .catch((e) => { + console.error('Error polling:', e); + // setTimeout(() => { + // console.log('Retrying polling'); + // pollLoop(); + // }, 5000); + }); +} + +export function startPolling(_jwt: string) { + jwt = _jwt; + pollLoop(); +} + +export function stopPolling() { + if (abortController) { + abortController.abort(); + } +} diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index e590deb..40a33ea 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -7,6 +7,7 @@ import { useState, } from 'react'; import { useToast } from '../library/toast/toast-context'; +import * as channel from '../channel'; export type User = { name: string; @@ -66,6 +67,7 @@ export function AuthProvider({ children }: { children: ReactElement }) { name: data.user.username, }); setJwt(localToken!); + channel.startPolling(localToken!); } } catch (e) { console.info('deleting existing jwt'); @@ -103,6 +105,7 @@ export function AuthProvider({ children }: { children: ReactElement }) { }); setJwt(data.token); localStorage.setItem('jwt', data.token); + channel.startPolling(data.token); }, [setUser, toast] ); @@ -111,6 +114,7 @@ export function AuthProvider({ children }: { children: ReactElement }) { setUser(null); setJwt(''); localStorage.removeItem('jwt'); + channel.stopPolling(); }, []); const getUser = useCallback(() => { diff --git a/frontend/src/contexts/GameContext.tsx b/frontend/src/contexts/GameContext.tsx index 7182f26..3766487 100644 --- a/frontend/src/contexts/GameContext.tsx +++ b/frontend/src/contexts/GameContext.tsx @@ -4,9 +4,10 @@ import Game from '../pages/Game'; import { useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from './AuthContext'; import { useToast } from '../library/toast/toast-context'; +import * as channel from '../channel'; interface GameState { - players: { id: number; name: string; cards: string[] }[]; + players: { id: string; cards: string[] }[]; cards: string[]; currentTurn: number; lastThrownCard: string; @@ -90,6 +91,7 @@ export const GameProvider = () => { throw new Error('Game state not received'); } setGameState(data.gameState); + // extract card and player data from the game state and store in maps console.log(data.gameState.id); } } catch (e) { @@ -105,22 +107,23 @@ export const GameProvider = () => { // polling useEffect(() => { - async function poll() { - const res = await fetch(`${backendUrl}/game/events`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: auth.jwt, - }, - }); - if (!res.ok) { - throw new Error((await res.json()).error); + // add event listener to listen for the game state changes + channel.setGameEventsDispatcher((event) => { + console.log('Received event:', event); + if (event.type === 'STATE_SYNC') { + setGameState({ + players: event.data.players.map((player) => { + return { + id: player.id, + cards: player.cards.map((card) => card.id), + }; + }), + cards: event.data.cards.map((card) => card.id), + currentTurn: event.data.currentTurn, + lastThrownCard: event.data.lastThrownCard, + }); } - const data = await res.json(); - // to be changed later to have a more sensible structure - setGameState(data.events[0]); - } - poll(); + }); }, [gameState]); return (