Skip to content

Commit

Permalink
polling: Create module to handle polling
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kuv2707 committed Jun 18, 2024
1 parent dc670a1 commit 16a5939
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 23 deletions.
10 changes: 9 additions & 1 deletion backend/src/controllers/gameControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || '',
},
});
}
}
16 changes: 12 additions & 4 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -48,7 +46,8 @@ export type GameEventType =
| 'DRAW_CARD'
| 'THROW_CARD'
| 'JOIN_GAME'
| 'LEAVE_GAME';
| 'LEAVE_GAME'
| 'STATE_SYNC';

export type GameEvent =
| {
Expand All @@ -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
2 changes: 1 addition & 1 deletion backend/src/uno-game-engine/events/joinGame.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert } from 'console';
import assert from 'assert';
import { GameEngine } from '../engine';
import { EventResult, GameEvent, Player } from '../../types';

Expand Down
2 changes: 1 addition & 1 deletion backend/src/uno-game-engine/events/leaveGame.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
78 changes: 78 additions & 0 deletions frontend/src/channel.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
4 changes: 4 additions & 0 deletions frontend/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -103,6 +105,7 @@ export function AuthProvider({ children }: { children: ReactElement }) {
});
setJwt(data.token);
localStorage.setItem('jwt', data.token);
channel.startPolling(data.token);
},
[setUser, toast]
);
Expand All @@ -111,6 +114,7 @@ export function AuthProvider({ children }: { children: ReactElement }) {
setUser(null);
setJwt('');
localStorage.removeItem('jwt');
channel.stopPolling();
}, []);

const getUser = useCallback(() => {
Expand Down
35 changes: 19 additions & 16 deletions frontend/src/contexts/GameContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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 (
Expand Down

0 comments on commit 16a5939

Please sign in to comment.