From 311c56575ab96aec3f8b0dcbcad2c37cef4c92a1 Mon Sep 17 00:00:00 2001 From: Jordan Pawlett Date: Sun, 3 May 2020 14:48:13 +0100 Subject: [PATCH] Disconnect previous client if a user opens a secondary tab. Do not overwrite users score/cards if the user was previously in the game --- services/games-service/src/game.ts | 51 +++++++++++-------- services/games-service/src/games-service.ts | 2 +- services/games-service/src/turn.ts | 1 + services/rooms-service/src/rooms-service.ts | 5 +- .../src/BaseNamespace.ts | 13 +++++ .../src/DefaultNamespace.ts | 20 ++++++-- 6 files changed, 64 insertions(+), 28 deletions(-) diff --git a/services/games-service/src/game.ts b/services/games-service/src/game.ts index 88f316b6..0712ec1a 100644 --- a/services/games-service/src/game.ts +++ b/services/games-service/src/game.ts @@ -88,28 +88,29 @@ export default class Game extends TurnHandler { try { // Try update the games prevState. // tslint:disable-next-line: max-line-length - await this.broker.call('games.update', { id: updatedTurn.gameId, prevTurnData: updatedTurn, gameState: updatedTurn.state }); + const updatedGame: GameInterface = await this.broker.call('games.update', { id: updatedTurn.gameId, prevTurnData: updatedTurn, gameState: updatedTurn.state }); + + this.logger.info('Turn update found, setting timer for next turn', updatedTurn.state); + switch (updatedTurn.state) { + case GameState.TURN_SETUP: + const timeout = updatedGame.prevTurnData.initializing ? 0 : 10000; + return this.setGameTimeout(updatedTurn.gameId, (game) => this.handleNextTurn(game), timeout); + case GameState.PICKING_CARDS: + return this.setGameTimeout(updatedTurn.gameId, (game) => this.handleWinnerSelection(game)); + case GameState.SELECTING_WINNER: + return this.setGameTimeout(updatedTurn.gameId, (game) => this.handleNoWinner(game, 'The Czar did not pick a winner! They have failed us all...')); + case GameState.ENEDED: + return this.setGameTimeout(updatedTurn.gameId, (game) => { + // kick everyone out and end the game; + this.destroyGame(game._id); + }); + default: + this.logger.error('Not sure which state to call'); + return; + } } catch (e) { this.logger.error(e); } - - this.logger.info('Turn update found, setting timer for next turn', updatedTurn.state); - switch (updatedTurn.state) { - case GameState.TURN_SETUP: - return this.setGameTimeout(updatedTurn.gameId, (game) => this.handleNextTurn(game), 10000); - case GameState.PICKING_CARDS: - return this.setGameTimeout(updatedTurn.gameId, (game) => this.handleWinnerSelection(game)); - case GameState.SELECTING_WINNER: - return this.setGameTimeout(updatedTurn.gameId, (game) => this.handleNoWinner(game, 'The Czar did not pick a winner! They have failed us all...')); - case GameState.ENEDED: - return this.setGameTimeout(updatedTurn.gameId, (game) => { - // kick everyone out and end the game; - this.destroyGame(game._id); - }); - default: - this.logger.error('Not sure which state to call'); - return; - } } private initalizePlayers(room: Room): { [id: string]: GamePlayer } { @@ -136,7 +137,8 @@ export default class Game extends TurnHandler { selectedCards: {}, winner: null, winningCards: [], - state: initalGameState + state: initalGameState, + initializing: true }; return this.fetchCards(room.options.decks) @@ -363,11 +365,16 @@ export default class Game extends TurnHandler { } - public async onPlayerJoin(gameId: string, playerId: string) { + public async onPlayerJoin(game: GameInterface, playerId: string) { + if (playerId in game.players) { + // player is already in the game, must've the refreshed page. + return null; + } + // Ensure the new player is included in the match. const newPlayer = { _id: playerId, cards: [], isCzar: false, score: 0 }; const playersProp = `players.${playerId}`; - return this.broker.call('games.update', { id: gameId, [playersProp]: newPlayer }); + return this.broker.call('games.update', { id: game._id, [playersProp]: newPlayer }); // Implement this. // if (this.lastGameState) { diff --git a/services/games-service/src/games-service.ts b/services/games-service/src/games-service.ts index 783d7a6e..6b54cec1 100644 --- a/services/games-service/src/games-service.ts +++ b/services/games-service/src/games-service.ts @@ -128,7 +128,7 @@ export default class GameService extends Service { const { clientId, roomId } = ctx.params; return this.getGameMatchingRoom(ctx, roomId) .then((game) => { - return this.gameService.onPlayerJoin(game._id, clientId); + return this.gameService.onPlayerJoin(game, clientId); }) .catch(err => { // game must not have started yet. diff --git a/services/games-service/src/turn.ts b/services/games-service/src/turn.ts index 49e0de25..2a9ccf2b 100644 --- a/services/games-service/src/turn.ts +++ b/services/games-service/src/turn.ts @@ -31,6 +31,7 @@ export interface TurnDataWithState extends TurnData { winner: string | string[]; winningCards: Card[]; errorMessage?: string; + initializing?: boolean; } export default class TurnHandler { diff --git a/services/rooms-service/src/rooms-service.ts b/services/rooms-service/src/rooms-service.ts index 4ebd3197..7381ddc7 100644 --- a/services/rooms-service/src/rooms-service.ts +++ b/services/rooms-service/src/rooms-service.ts @@ -347,12 +347,13 @@ export default class RoomsService extends Service { { $pull: { players: _id, spectators: _id } }, { returnOriginal: false } ) - .then(doc => { + .then(async doc => { // Client is not in any rooms if (!doc.value) { return null; } - this.entityChanged('updated', doc.value, ctx).then(() => doc.value); + await ctx.emit(`${this.name}.player.left`, { clientId: _id, roomId: doc.value?._id }); + return this.entityChanged('updated', doc.value, ctx).then(() => doc.value); }); } diff --git a/services/websocket-gateway-service/src/BaseNamespace.ts b/services/websocket-gateway-service/src/BaseNamespace.ts index 0a72ca6b..b77d3946 100644 --- a/services/websocket-gateway-service/src/BaseNamespace.ts +++ b/services/websocket-gateway-service/src/BaseNamespace.ts @@ -14,6 +14,7 @@ export interface RedisAdapter extends Adapter { clients: (callback: (error: Error, clients: string[]) => void) => void; clientRooms: (id: string, callback: (error: Error, rooms: string[]) => void) => void; remoteJoin: (id: string, room: string, callback: (error: Error) => void) => void; + remoteDisconnect: (id: string, close: boolean, callback: (error: Error) => void) => void; } export default class BaseNamespace { @@ -40,6 +41,18 @@ export default class BaseNamespace { }); } + protected remoteDisconnect(clientId: string): Promise { + return new Promise((resolve, reject) => { + this.adapter.remoteDisconnect(clientId, true, (err) => { + if (err) { + reject(err); + } else { + resolve(`${clientId} forcefully disconnected`); + } + }); + }); + } + private verifyAndDecode(token: string): Promise { return this.admin.auth().verifyIdToken(token); } diff --git a/services/websocket-gateway-service/src/DefaultNamespace.ts b/services/websocket-gateway-service/src/DefaultNamespace.ts index a21a57e9..6191ebe6 100644 --- a/services/websocket-gateway-service/src/DefaultNamespace.ts +++ b/services/websocket-gateway-service/src/DefaultNamespace.ts @@ -28,8 +28,9 @@ export default class DefaultNamespace extends BaseNamespace { setTimeout(async () => { const user = await this.broker.call('clients.get', { id: _id }) as any; const afterTimeoutTime = new Date().getTime(); - // If the user hasn't reconnected. Fire a disconnect event. - if (user.disconnectedAt && afterTimeoutTime - user.disconnectedAt > (timeout - 5000)) { + // If the user hasn't changed socketid or reconnected. Fire a disconnect event. + // tslint:disable-next-line: max-line-length + if (user.socket === client.id && user.disconnectedAt && afterTimeoutTime - user.disconnectedAt > (timeout - 5000)) { this.broker.emit('websocket-gateway.client.disconnected', { _id }); } }, timeout); @@ -46,6 +47,19 @@ export default class DefaultNamespace extends BaseNamespace { this.logger.info('Client Connected', client.id, 'to:', client.nsp.name); client.once('disconnect', () => this.onDisconnect(client)); - this.broker.emit('websocket-gateway.client.connected', { _id, socket: client.id }); + this.broker.call('clients.get', { id: _id }) + // catch, client might not exist. + .catch((err) => null) + .then(async (user: any) => { + // Update the user with the newly connected socket, before disconnecting previous. To reduce TTL. + await this.broker.emit('websocket-gateway.client.connected', { _id, socket: client.id }); + if (user && user.socket && user.socket !== client.id) { + // user already has a tab open. Forcefully disconnect it. + return this.remoteDisconnect(user.socket); + } + return null; + }) + // previous socket may not have been able to be disconnected. user may have closed the tab + .catch((err) => null); } }