From af69bc73db307c55f9aaa1e4b638799c22d28e9b Mon Sep 17 00:00:00 2001 From: Kislay Date: Thu, 13 Jun 2024 01:27:36 +0530 Subject: [PATCH] backend: Complete the event propagation mechanism. - A client can send a polling get request at game/events. If there are events for the client, they are flushed immediately. - A client can post a game event at game/events. The active game of the client is fetched, and the event is dispatched on the game. The change is then enqueued to be sent to all the current players' clients. --- backend/src/controllers/gameControllers.js | 21 ------------ backend/src/controllers/gameControllers.ts | 37 ++++++++++++++++++++++ backend/src/eventRecipients.ts | 17 +++++++--- backend/src/models/userModel.ts | 5 +++ backend/src/routes/gameRoutes.ts | 13 +++++--- backend/src/uno-game-engine/engine.ts | 4 +-- 6 files changed, 66 insertions(+), 31 deletions(-) delete mode 100644 backend/src/controllers/gameControllers.js create mode 100644 backend/src/controllers/gameControllers.ts diff --git a/backend/src/controllers/gameControllers.js b/backend/src/controllers/gameControllers.js deleted file mode 100644 index 14cdcde..0000000 --- a/backend/src/controllers/gameControllers.js +++ /dev/null @@ -1,21 +0,0 @@ -import { getAllClients, scheduleSend } from '../eventRecipients'; - -async function handleEvent(req, res) { - const event = req.body; - - const clientIds = getAllClients(); - - const eligibleClientIds = filterEligibleClients(clientIds); - - eligibleClientIds.forEach((clientId) => { - scheduleSend(clientId, event); - }); - - res.status(200).send({ message: 'Event propagated to clients.' }); -} - -function filterEligibleClients(clientIds) { - return clientIds; -} - -export default handleEvent; diff --git a/backend/src/controllers/gameControllers.ts b/backend/src/controllers/gameControllers.ts new file mode 100644 index 0000000..8c737ca --- /dev/null +++ b/backend/src/controllers/gameControllers.ts @@ -0,0 +1,37 @@ +import { Response } from 'express'; +import { enqueueForSend } from '../eventRecipients'; +import { AuthRequest } from '../middlewares/authMiddleware'; +import { retrieveGame } from '../gameStore'; + +export async function handleGameEvent(req: AuthRequest, res: Response) { + const event = req.body; + const activeGameId = req.user.activeGameId; + if (!activeGameId) { + res.status(404).send({ + message: 'User is not actively playing any game', + }); + return; + } + const game = retrieveGame(activeGameId); + if (!game) { + res.status(404).send({ message: 'Game not found' }); + return; + } + //todo: When game data is retrieved from database, it is not an instance of GameEngine + // so we would need to convert it to an instance of GameEngine + const result = game.dispatchEvent(event); + if (result.type === 'ERROR') { + res.status(400).send({ message: result.message }); + return; + } else { + // the game state after a successful event is propagated to all clients + // we can choose to relay the event received, so that the clients apply the event + // 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. + // todo: send updated game state rather than event + for (const player of game.players) { + enqueueForSend(player.id, event); + } + res.status(200).send({ message: 'Event propagated to clients.' }); + } +} diff --git a/backend/src/eventRecipients.ts b/backend/src/eventRecipients.ts index 2358e5d..4d43435 100644 --- a/backend/src/eventRecipients.ts +++ b/backend/src/eventRecipients.ts @@ -14,9 +14,10 @@ const clients = new Map(); const eventQueue = new Map(); export function addClient(clientId: ClientId, res: Response) { - // todo: We can immediately send any events that are in the queue. + // if there are new events for this client, we will send them immediately + // else we will withhold the response object until there are new events clients.set(clientId, res); - eventQueue.set(clientId, []); + doSendEvents(clientId); } export function removeClient(clientId: ClientId) { @@ -32,8 +33,16 @@ export function getAllClients(): ClientId[] { return Array.from(clients.keys()); } -export function scheduleSend(clientId: ClientId, event: AppEvent) { - eventQueue.get(clientId)?.push(event); +export function enqueueForSend(clientId: ClientId, event: AppEvent) { + if (!eventQueue.has(clientId)) { + // this is most probably not going to happen, as a polling request is expected + // to be made before any app event is sent to the client, but better safe than sorry + eventQueue.set(clientId, [event]); + } else { + eventQueue.get(clientId)?.push(event); + } + // send the events immediately if the client is waiting + doSendEvents(clientId); } export function doSendEvents(clientId: ClientId) { diff --git a/backend/src/models/userModel.ts b/backend/src/models/userModel.ts index 2721571..418fa1d 100644 --- a/backend/src/models/userModel.ts +++ b/backend/src/models/userModel.ts @@ -5,6 +5,7 @@ import { NextFunction } from 'express'; export interface IUser extends mongoose.Document { username: string; password: string; + activeGameId: string | null; isPasswordCorrect: (password: string) => Promise; } @@ -20,6 +21,10 @@ const userSchema = new mongoose.Schema( required: [true, 'Password is required'], minlength: [6, 'Password must be at least 6 characters long'], }, + activeGameId: { + type: String, + default: null, + }, }, { timestamps: true, diff --git a/backend/src/routes/gameRoutes.ts b/backend/src/routes/gameRoutes.ts index 7864512..f272825 100644 --- a/backend/src/routes/gameRoutes.ts +++ b/backend/src/routes/gameRoutes.ts @@ -1,13 +1,18 @@ import express from 'express'; import { addClient } from '../eventRecipients'; -import handleEvent from '../controllers/gameControllers'; +import { handleGameEvent } from '../controllers/gameControllers'; +import { AuthRequest, verifyToken } from '../middlewares/authMiddleware'; const router = express.Router(); +router.use(verifyToken); -router.get('/events', (req, res) => { - addClient('user_id', res); +router.get('/events', function (req: AuthRequest, res) { + const clientId = req.user.id as string; + // note: we might need to use a different client id if the user is allowed to have multiple clients + // ie, the user is allowed to play multiple games on multiple devices at the same time + addClient(clientId, res); }); -router.post('/events', handleEvent); +router.post('/events', handleGameEvent); export default router; diff --git a/backend/src/uno-game-engine/engine.ts b/backend/src/uno-game-engine/engine.ts index 72a2cc8..e7df390 100644 --- a/backend/src/uno-game-engine/engine.ts +++ b/backend/src/uno-game-engine/engine.ts @@ -81,8 +81,8 @@ export class GameEngine { return { type: 'ERROR', message: (error as Error).message }; } } - dispatchEvent(event: GameEvent) { + dispatchEvent(event: GameEvent): EventResult { // handle different types of events based on event.type - handleEvent(this, event); + return handleEvent(this, event); } }