From d6ab8bd1482e422be3580e9193205b5d2f47d38b Mon Sep 17 00:00:00 2001 From: legonzaur Date: Thu, 27 Jun 2024 19:40:15 +0200 Subject: [PATCH] Fix : add transaction --- .env.dev | 10 ++ .gitignore | 1 + .vscode/launch.json | 3 +- docker-compose.yml | 5 + src/games/controllers/games.controller.ts | 16 +-- src/games/services/games.service.ts | 117 ++++++++++++++++++---- src/main.ts | 3 + 7 files changed, 121 insertions(+), 34 deletions(-) create mode 100644 .env.dev diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..b0a8f7c --- /dev/null +++ b/.env.dev @@ -0,0 +1,10 @@ +SESSION_SECRET=dev + +DISCORD_CLIENTID= +DISCORD_CLIENTSECRET= + +MONGO_ENDPOINT=127.0.0.1 +MONGO_PASSWORD=example +MONGO_USER=root + +DEV_MODE=true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5c69b1f..9aa552a 100644 --- a/.gitignore +++ b/.gitignore @@ -398,3 +398,4 @@ Temporary Items dist .webpack .serverless/**/*.zip +mongodb-keyfile \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 7c5f392..f7ecbf6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,8 @@ "type": "node", "console": "internalConsole", "outputCapture": "std", - "internalConsoleOptions": "openOnSessionStart" + "internalConsoleOptions": "openOnSessionStart", + "envFile": "${workspaceFolder}/.env" } ] } \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c4dfeb5..4e27d04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,3 +10,8 @@ services: MONGO_INITDB_ROOT_PASSWORD: example ports: - 27017:27017 + command: mongod --replSet rs0 --keyFile /opt/keyfile/mongodb-keyfile + volumes: + - ./:/opt/keyfile/ + +# docker run --rm -it --network=host --name mongoContainer mongo:latest mongosh mongodb://127.0.0.1:27017 -u root -p example --eval "rs.initiate({'_id':'rs0', members: [{'_id':1, 'host':'127.0.0.1:27017'}]})" \ No newline at end of file diff --git a/src/games/controllers/games.controller.ts b/src/games/controllers/games.controller.ts index 19f6221..dedcfe3 100644 --- a/src/games/controllers/games.controller.ts +++ b/src/games/controllers/games.controller.ts @@ -82,20 +82,8 @@ export class GamesController { const currentTurn = game.teams.find( (t) => t._id.toHexString() == game.currentTurn, ); - if (!currentTurn.players.some((p) => p._id == player._id)) { - throw new HttpException('It is not your turn', HttpStatus.BAD_REQUEST); - } - if ( - !currentTurn.players.some( - (p) => p._id.toHexString() == player._id.toHexString(), - ) - ) { - throw new HttpException( - 'It is not your turn to play', - HttpStatus.FORBIDDEN, - ); - } - await this.gameService.endTurn(game); + + await this.gameService.endTurn(game, player._id.toHexString()); } @Sse('/subscribe') diff --git a/src/games/services/games.service.ts b/src/games/services/games.service.ts index 15e9f8c..9ade0cd 100644 --- a/src/games/services/games.service.ts +++ b/src/games/services/games.service.ts @@ -1,9 +1,10 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; +import { InjectModel, InjectConnection } from '@nestjs/mongoose'; import { Container, Game, Player } from '../schemas/game.schema'; -import { Document, Model } from 'mongoose'; +import mongoose, { Document, Model } from 'mongoose'; import { Card } from 'src/cards/schemas/cards.schema'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Console } from 'console'; function shuffle(a) { let j, x, i; @@ -16,33 +17,74 @@ function shuffle(a) { return a; } +function WithTransaction( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, +) { + const originalFunc = descriptor.value; + + descriptor.value = async function (...args: any[]) { + if (args.at(-1) instanceof mongoose.mongo.ClientSession) { + return originalFunc.apply(this, args); + } else { + await this.connection + .transaction(async (session) => { + return originalFunc.apply(this, [...args, session]); + }) + .catch((e) => { + throw new HttpException( + 'Another request is processing', + HttpStatus.TOO_MANY_REQUESTS, + ); + }); + } + }; + + return descriptor; +} + @Injectable() export class GameService { constructor( @InjectModel(Game.name) private gameModel: Model, @InjectModel(Card.name) private cardModel: Model, + @InjectConnection() private readonly connection: mongoose.Connection, + private eventEmitter: EventEmitter2, ) {} - async endTurn(game: Document & Game) { + @WithTransaction + async endTurn( + game: Document & Game, + currentPlayerId: string, + session?: mongoose.mongo.ClientSession, + ) { let currentTurn = game.teams.find( (t) => t._id.toHexString() == game.currentTurn, ); + if ( + !currentTurn.players.some((p) => p._id.toHexString() == currentPlayerId) + ) { + throw new HttpException( + 'It is not your turn to play', + HttpStatus.FORBIDDEN, + ); + } for (const player of currentTurn.players) { await this.distributeCards( player.board.cards.filter((c) => !c.defense), player.board, player.discard, game, + session, ); - - await this.drawToHand(5, player, game); + await this.drawToHand(5, player, game, session); } const index = game.teams.indexOf(currentTurn); game.currentTurn = game.teams[(index + 1) % game.teams.length]._id.toHexString(); - await game.save(); this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { type: 'endTurn', @@ -59,68 +101,105 @@ export class GameService { player.hand, player.board, game, + session, ); } + await game.save({ session }); } - - async drawToBoard(amount, player: Player, game: Document & Game) { + @WithTransaction + async drawToBoard( + amount, + player: Player, + game: Document & Game, + session?: mongoose.mongo.ClientSession, + ) { await this.drawCards( amount, player.stack, player.board, player.discard, game, + session, ); } - async drawToHand(amount, player: Player, game: Document & Game) { + @WithTransaction + async drawToHand( + amount, + player: Player, + game: Document & Game, + session?: mongoose.mongo.ClientSession, + ) { await this.drawCards( amount, player.stack, player.hand, player.discard, game, + session, ); } + + @WithTransaction private async drawCards( amount: number, from: Container, to: Container, discard: Container, game: Document & Game, + session?: mongoose.mongo.ClientSession, ) { if (amount <= from.cards.length) { - await this.distributeCards(from.cards.slice(0, amount), from, to, game); - await game.save(); + await this.distributeCards( + from.cards.slice(0, amount), + from, + to, + game, + session, + ); + await game.save({ session }); return; } else { const toDraw = from.cards.length; - await this.distributeCards(from.cards.slice(0, toDraw), from, to, game); + await this.distributeCards( + from.cards.slice(0, toDraw), + from, + to, + game, + session, + ); if (discard.cards.length == 0) { - await game.save(); + await game.save({ session }); return; } // restack all discarded - await this.distributeCards(discard.cards, discard, from, game); - await this.shuffleContainer(from, game); - await this.drawCards(amount - toDraw, from, to, discard, game); + await this.distributeCards(discard.cards, discard, from, game, session); + await this.shuffleContainer(from, game, session); + await this.drawCards(amount - toDraw, from, to, discard, game, session); } } - async shuffleContainer(container: Container, game: Document & Game) { + @WithTransaction + async shuffleContainer( + container: Container, + game: Document & Game, + session?: mongoose.mongo.ClientSession, + ) { container.cards = shuffle(container.cards); this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { type: 'shuffleContainer', container: container._id.toHexString(), }); - await game.save(); + await game.save({ session }); } + @WithTransaction async distributeCards( cards: Card[], from: Container, to: Container, game: Document & Game, + session?: mongoose.mongo.ClientSession, ) { if (cards.some((c) => !from.cards.includes(c))) { throw new HttpException( @@ -134,7 +213,7 @@ export class GameService { from.cards.splice(index, 1); to.cards.push(c); } - await game.save(); + await game.save({ session }); this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { type: 'distributeCards', from: from._id.toHexString(), diff --git a/src/main.ts b/src/main.ts index 985e7f2..3160926 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import * as session from 'express-session'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import MongoStore from 'connect-mongo'; +import mongoose from 'mongoose'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -15,6 +16,8 @@ async function bootstrap() { const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api', app, document); + mongoose.set('transactionAsyncLocalStorage', true); + app.use( session.default({ secret: process.env.SESSION_SECRET,