Fix : add transaction

This commit is contained in:
2024-06-27 19:40:15 +02:00
parent 224bd71a2c
commit d6ab8bd148
7 changed files with 121 additions and 34 deletions
+10
View File
@@ -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
+1
View File
@@ -398,3 +398,4 @@ Temporary Items
dist dist
.webpack .webpack
.serverless/**/*.zip .serverless/**/*.zip
mongodb-keyfile
+2 -1
View File
@@ -20,7 +20,8 @@
"type": "node", "type": "node",
"console": "internalConsole", "console": "internalConsole",
"outputCapture": "std", "outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart" "internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/.env"
} }
] ]
} }
+5
View File
@@ -10,3 +10,8 @@ services:
MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_ROOT_PASSWORD: example
ports: ports:
- 27017:27017 - 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'}]})"
+2 -14
View File
@@ -82,20 +82,8 @@ export class GamesController {
const currentTurn = game.teams.find( const currentTurn = game.teams.find(
(t) => t._id.toHexString() == game.currentTurn, (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); await this.gameService.endTurn(game, player._id.toHexString());
}
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);
} }
@Sse('/subscribe') @Sse('/subscribe')
+98 -19
View File
@@ -1,9 +1,10 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 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 { 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 { Card } from 'src/cards/schemas/cards.schema';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { Console } from 'console';
function shuffle(a) { function shuffle(a) {
let j, x, i; let j, x, i;
@@ -16,33 +17,74 @@ function shuffle(a) {
return 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() @Injectable()
export class GameService { export class GameService {
constructor( constructor(
@InjectModel(Game.name) private gameModel: Model<Game>, @InjectModel(Game.name) private gameModel: Model<Game>,
@InjectModel(Card.name) private cardModel: Model<Card>, @InjectModel(Card.name) private cardModel: Model<Card>,
@InjectConnection() private readonly connection: mongoose.Connection,
private eventEmitter: EventEmitter2, private eventEmitter: EventEmitter2,
) {} ) {}
async endTurn(game: Document<Game> & Game) { @WithTransaction
async endTurn(
game: Document<Game> & Game,
currentPlayerId: string,
session?: mongoose.mongo.ClientSession,
) {
let currentTurn = game.teams.find( let currentTurn = game.teams.find(
(t) => t._id.toHexString() == game.currentTurn, (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) { for (const player of currentTurn.players) {
await this.distributeCards( await this.distributeCards(
player.board.cards.filter((c) => !c.defense), player.board.cards.filter((c) => !c.defense),
player.board, player.board,
player.discard, player.discard,
game, game,
session,
); );
await this.drawToHand(5, player, game, session);
await this.drawToHand(5, player, game);
} }
const index = game.teams.indexOf(currentTurn); const index = game.teams.indexOf(currentTurn);
game.currentTurn = game.currentTurn =
game.teams[(index + 1) % game.teams.length]._id.toHexString(); game.teams[(index + 1) % game.teams.length]._id.toHexString();
await game.save();
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
type: 'endTurn', type: 'endTurn',
@@ -59,68 +101,105 @@ export class GameService {
player.hand, player.hand,
player.board, player.board,
game, game,
session,
); );
} }
await game.save({ session });
} }
@WithTransaction
async drawToBoard(amount, player: Player, game: Document<Game> & Game) { async drawToBoard(
amount,
player: Player,
game: Document<Game> & Game,
session?: mongoose.mongo.ClientSession,
) {
await this.drawCards( await this.drawCards(
amount, amount,
player.stack, player.stack,
player.board, player.board,
player.discard, player.discard,
game, game,
session,
); );
} }
async drawToHand(amount, player: Player, game: Document<Game> & Game) { @WithTransaction
async drawToHand(
amount,
player: Player,
game: Document<Game> & Game,
session?: mongoose.mongo.ClientSession,
) {
await this.drawCards( await this.drawCards(
amount, amount,
player.stack, player.stack,
player.hand, player.hand,
player.discard, player.discard,
game, game,
session,
); );
} }
@WithTransaction
private async drawCards( private async drawCards(
amount: number, amount: number,
from: Container, from: Container,
to: Container, to: Container,
discard: Container, discard: Container,
game: Document<Game> & Game, game: Document<Game> & Game,
session?: mongoose.mongo.ClientSession,
) { ) {
if (amount <= from.cards.length) { if (amount <= from.cards.length) {
await this.distributeCards(from.cards.slice(0, amount), from, to, game); await this.distributeCards(
await game.save(); from.cards.slice(0, amount),
from,
to,
game,
session,
);
await game.save({ session });
return; return;
} else { } else {
const toDraw = from.cards.length; 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) { if (discard.cards.length == 0) {
await game.save(); await game.save({ session });
return; return;
} }
// restack all discarded // restack all discarded
await this.distributeCards(discard.cards, discard, from, game); await this.distributeCards(discard.cards, discard, from, game, session);
await this.shuffleContainer(from, game); await this.shuffleContainer(from, game, session);
await this.drawCards(amount - toDraw, from, to, discard, game); await this.drawCards(amount - toDraw, from, to, discard, game, session);
} }
} }
async shuffleContainer(container: Container, game: Document<Game> & Game) { @WithTransaction
async shuffleContainer(
container: Container,
game: Document<Game> & Game,
session?: mongoose.mongo.ClientSession,
) {
container.cards = shuffle(container.cards); container.cards = shuffle(container.cards);
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
type: 'shuffleContainer', type: 'shuffleContainer',
container: container._id.toHexString(), container: container._id.toHexString(),
}); });
await game.save(); await game.save({ session });
} }
@WithTransaction
async distributeCards( async distributeCards(
cards: Card[], cards: Card[],
from: Container, from: Container,
to: Container, to: Container,
game: Document<Game> & Game, game: Document<Game> & Game,
session?: mongoose.mongo.ClientSession,
) { ) {
if (cards.some((c) => !from.cards.includes(c))) { if (cards.some((c) => !from.cards.includes(c))) {
throw new HttpException( throw new HttpException(
@@ -134,7 +213,7 @@ export class GameService {
from.cards.splice(index, 1); from.cards.splice(index, 1);
to.cards.push(c); to.cards.push(c);
} }
await game.save(); await game.save({ session });
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), { this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
type: 'distributeCards', type: 'distributeCards',
from: from._id.toHexString(), from: from._id.toHexString(),
+3
View File
@@ -3,6 +3,7 @@ import * as session from 'express-session';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import MongoStore from 'connect-mongo'; import MongoStore from 'connect-mongo';
import mongoose from 'mongoose';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
@@ -15,6 +16,8 @@ async function bootstrap() {
const document = SwaggerModule.createDocument(app, config); const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document); SwaggerModule.setup('api', app, document);
mongoose.set('transactionAsyncLocalStorage', true);
app.use( app.use(
session.default({ session.default({
secret: process.env.SESSION_SECRET, secret: process.env.SESSION_SECRET,