Skip to content

Commit

Permalink
backend: Complete the event propagation mechanism.
Browse files Browse the repository at this point in the history
- 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.
kuv2707 committed Jun 12, 2024
1 parent ee3bf21 commit af69bc7
Showing 6 changed files with 66 additions and 31 deletions.
21 changes: 0 additions & 21 deletions backend/src/controllers/gameControllers.js

This file was deleted.

37 changes: 37 additions & 0 deletions backend/src/controllers/gameControllers.ts
Original file line number Diff line number Diff line change
@@ -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.' });
}
}
17 changes: 13 additions & 4 deletions backend/src/eventRecipients.ts
Original file line number Diff line number Diff line change
@@ -14,9 +14,10 @@ const clients = new Map<ClientId, Response>();
const eventQueue = new Map<ClientId, AppEvent[]>();

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) {
5 changes: 5 additions & 0 deletions backend/src/models/userModel.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>;
}

@@ -20,6 +21,10 @@ const userSchema = new mongoose.Schema<IUser>(
required: [true, 'Password is required'],
minlength: [6, 'Password must be at least 6 characters long'],
},
activeGameId: {
type: String,
default: null,
},
},
{
timestamps: true,
13 changes: 9 additions & 4 deletions backend/src/routes/gameRoutes.ts
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 2 additions & 2 deletions backend/src/uno-game-engine/engine.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit af69bc7

Please sign in to comment.