Skip to content

Commit

Permalink
Disconnect previous client if a user opens a secondary tab. Do not ov…
Browse files Browse the repository at this point in the history
…erwrite users score/cards if the user was previously in the game
  • Loading branch information
JordanPawlett committed May 3, 2020
1 parent c5f0c44 commit 311c565
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 28 deletions.
51 changes: 29 additions & 22 deletions services/games-service/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } {
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion services/games-service/src/games-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions services/games-service/src/turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface TurnDataWithState extends TurnData {
winner: string | string[];
winningCards: Card[];
errorMessage?: string;
initializing?: boolean;
}

export default class TurnHandler {
Expand Down
5 changes: 3 additions & 2 deletions services/rooms-service/src/rooms-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}

Expand Down
13 changes: 13 additions & 0 deletions services/websocket-gateway-service/src/BaseNamespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -40,6 +41,18 @@ export default class BaseNamespace {
});
}

protected remoteDisconnect(clientId: string): Promise<string> {
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<any> {
return this.admin.auth().verifyIdToken(token);
}
Expand Down
20 changes: 17 additions & 3 deletions services/websocket-gateway-service/src/DefaultNamespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}

0 comments on commit 311c565

Please sign in to comment.