From bffc38ac5c4bda236b53e2963d6200fa3c655def Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Fri, 29 Mar 2024 17:07:45 +0700 Subject: [PATCH 01/17] feat: perf for scoreboard --- apps/server/package-lock.json | 125 ++++++++++++++++-- apps/server/package.json | 4 + .../server/src/controller/admin.controller.ts | 23 ++-- .../src/controller/player.controller.ts | 72 +++++----- apps/server/src/index.ts | 8 ++ .../src/middleware/logger.middleware.ts | 14 ++ apps/server/src/models/game.model.ts | 35 ++--- apps/server/src/models/history.model.ts | 16 +-- apps/server/src/router/admin.router.ts | 9 +- apps/server/src/server.ts | 10 +- apps/server/src/service/admin.service.ts | 8 +- apps/server/src/service/player.service.ts | 43 +++--- apps/server/src/utils/logger.ts | 26 +++- 13 files changed, 276 insertions(+), 117 deletions(-) create mode 100644 apps/server/src/middleware/logger.middleware.ts diff --git a/apps/server/package-lock.json b/apps/server/package-lock.json index b3ae3f0..b394800 100644 --- a/apps/server/package-lock.json +++ b/apps/server/package-lock.json @@ -14,14 +14,18 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "morgan": "^1.10.0", "pg": "^8.11.3", "redis": "^4.6.13", "sequelize": "^6.37.1", "socket.io": "^4.7.5", + "split": "^1.0.1", "winston": "^3.13.0" }, "devDependencies": { "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", + "@types/split": "^1.0.5", "nodemon": "^3.1.0", "prettier": "^3.2.5", "ts-node": "^10.9.2", @@ -298,6 +302,15 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -406,6 +419,25 @@ "@types/node": "*" } }, + "node_modules/@types/split": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/split/-/split-1.0.5.tgz", + "integrity": "sha512-gMiDr4vA6YofTpAkPQtP+5pvStIf3CMYphf32YAG/3RwogNL8ii1CQKDc+sxN62KuxPoRaJXcf2zDCDkEBH4FA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/through": "*" + } + }, + "node_modules/@types/through": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", + "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -498,6 +530,22 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1034,20 +1082,6 @@ "node": ">= 0.6" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -1424,6 +1458,45 @@ "node": "*" } }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1550,6 +1623,14 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", @@ -2171,6 +2252,17 @@ "node": ">=10.0.0" } }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -2229,6 +2321,11 @@ "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/apps/server/package.json b/apps/server/package.json index 1bceef1..9fa1f52 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -18,10 +18,12 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", + "morgan": "^1.10.0", "pg": "^8.11.3", "redis": "^4.6.13", "sequelize": "^6.37.1", "socket.io": "^4.7.5", + "split": "^1.0.1", "winston": "^3.13.0" }, "optionalDependencies": { @@ -30,6 +32,8 @@ }, "devDependencies": { "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", + "@types/split": "^1.0.5", "nodemon": "^3.1.0", "prettier": "^3.2.5", "ts-node": "^10.9.2", diff --git a/apps/server/src/controller/admin.controller.ts b/apps/server/src/controller/admin.controller.ts index 5278797..2b44be0 100644 --- a/apps/server/src/controller/admin.controller.ts +++ b/apps/server/src/controller/admin.controller.ts @@ -8,14 +8,14 @@ export class AdminController { constructor( private readonly io: Server, private readonly adminService: AdminService, - ) {} - async getGames(req: Request, res: Response) { - const games = await this.adminService.getAllGames() + ) { } + async listGames(req: Request, res: Response) { + const games = await this.adminService.listGames() res.json(games) } - async getGame(req: Request, res: Response) { - const game = await this.adminService.getGame(req.params.id) + async getGameByID(req: Request, res: Response) { + const game = await this.adminService.getGameByID(req.params.id) res.json(game) } @@ -24,13 +24,8 @@ export class AdminController { res.json(game) } - async getGameById(req: Request, res: Response) { - const game = await this.adminService.getGame(req.params.id) - res.json(game) - } - - async getState(req: Request, res: Response) { - const state = await this.adminService.getState() + async getGameState(req: Request, res: Response) { + const state = await this.adminService.getGameState() res.json(state) } @@ -38,13 +33,13 @@ export class AdminController { const game = await this.adminService.startGame(req.params.id) res.json(game) this.io.emit('events', 'start') - this.io.emit('state', await this.adminService.getState()) + this.io.emit('state', await this.adminService.getGameState().then(game => game.id)) } async endGame(req: Request, res: Response) { const game = await this.adminService.endGame(req.params.id) res.json(game) this.io.emit('events', 'stop') - this.io.emit('state', await this.adminService.getState()) + this.io.emit('state', await this.adminService.getGameState().then(game => game.id)) } } diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index 7e5e9c5..f416ffe 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -8,31 +8,32 @@ export class PlayerController { constructor( private readonly io: Server, private readonly playerService: PlayerService, - ) { } + ) { + this.setupScoreboardEmitter() + } + + async authenticateSocket(socket: Socket) { + const cid = (socket.handshake.headers.cid || + socket.handshake.auth.cid) as string + const fid = (socket.handshake.headers.fid || + socket.handshake.auth.fid) as string + const name = (socket.handshake.headers.name || + socket.handshake.auth.name) as string + + const ip = (socket.handshake.headers['x-forwarded-for'] || + socket.handshake.address) as string - async onConnection(socket: Socket) { - const cid = socket.handshake.headers.cid || socket.handshake.auth.cid - const fid = socket.handshake.headers.fid || socket.handshake.auth.fid - const name = socket.handshake.headers.name || socket.handshake.auth.name if (cid && fid) { await this.playerService - .login(cid as string, fid as string, socket.id) - .then((client) => { - if (client[0] === 0) return socket.disconnect(true) - socket.user = client[1][0] - }) + .login(cid, fid, socket.id) + .then((client) => (socket.user = client)) .catch((err) => { this.logger.error(err) socket.disconnect(true) }) } else if (name && fid) { await this.playerService - .register( - name as string, - fid as string, - socket.id, - socket.handshake.address, - ) + .register(name, fid, socket.id, ip) .then((client) => { socket.user = client socket.emit('cid', client.cid) @@ -42,9 +43,27 @@ export class PlayerController { socket.disconnect(true) }) } else socket.disconnect(true) + } + setupScoreboardEmitter() { + setInterval(async () => { + await this.playerService + .getScoreboard() + .then((score) => score && this.io.emit('scoreboard', score)) + .catch((err) => { + this.logger.error(err) + }) + }, 500) + } + + async onConnection(socket: Socket) { this.logger.info(`New connection: ${socket.id}`) - socket.emit('state', await this.playerService.getLastGame()) + + await this.authenticateSocket(socket) + await this.playerService.getGameState().then((game) => { + socket.emit('state', game.id) + socket.emit('events', game.status) + }) socket.on('submit', async (message) => { this.logger.info('Received message', message) @@ -58,25 +77,6 @@ export class PlayerController { }) }) - setInterval(async () => { - await this.playerService - .getScoreboard() - .then((score) => { - const scoreboard = - score - ?.map((s) => { - return `${s.key} ${s.total_vote}` - }) - .join(' ') || '-1' - socket.emit('scoreboard', scoreboard) - }) - .catch((err) => { - this.logger.error(err) - }) - }, 500) - - socket.emit('events', await this.playerService.getState()) - socket.on('disconnect', () => { this.logger.info(`Disconnected: ${socket.id}`) }) diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index b542b78..daec106 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -3,8 +3,16 @@ import { initServer } from './server' import { createLogger } from './utils/logger' import { createServer } from 'http' import { configDotenv } from 'dotenv' +import winston from 'winston' configDotenv({ path: '.env' }) +winston.addColors({ + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'white', +}) const logger = createLogger('MainContext') const app = express() const server = createServer(app) diff --git a/apps/server/src/middleware/logger.middleware.ts b/apps/server/src/middleware/logger.middleware.ts new file mode 100644 index 0000000..d92ab4d --- /dev/null +++ b/apps/server/src/middleware/logger.middleware.ts @@ -0,0 +1,14 @@ +import winston from 'winston' +import morgan from 'morgan' +import split from 'split' + +export default function createMorganLogger(logger: winston.Logger) { + return morgan( + ':remote-addr :method :url :status :res[content-length] - :response-time ms', + { + stream: split().on('data', (message) => { + logger.http(message) + }), + }, + ) +} diff --git a/apps/server/src/models/game.model.ts b/apps/server/src/models/game.model.ts index 951252c..bce83a6 100644 --- a/apps/server/src/models/game.model.ts +++ b/apps/server/src/models/game.model.ts @@ -5,8 +5,7 @@ import { GameHistory } from './history.model' export class Game extends Model - implements GameAttributes -{ + implements GameAttributes { public id!: string public title!: string public description!: string @@ -69,9 +68,12 @@ export class GameRepository { async getLastActiveGame() { const game = await Game.findOne({ order: [['updatedAt', 'DESC']], - attributes: ['id'], + attributes: ['id', 'open'], limit: 1, - }).then((res) => res?.id) + }).then((res) => ({ + id: res?.id, + status: res?.open ? 'playing' : 'waiting', + })) return game } @@ -79,10 +81,6 @@ export class GameRepository { return Game.create(game) } - async getActiveGame() { - return Game.findOne({ where: { open: true } }) - } - async createHistory( game_id: string, player_id: string, @@ -96,8 +94,15 @@ export class GameRepository { } async startGame(game_id: string) { - return Game.update({ open: false }, { where: {} }).then(() => - Game.update({ open: true }, { where: { id: game_id } }), + return Game.findOne({ where: { open: true } }).then(async (game) => { + await game?.update({ open: false }) + return Game.update({ open: true }, { where: { id: game_id }, returning: true }).then((res) => { + if (!res[1][0]) { + throw Error('Game not found') + } + return res[1][0] + }) + } ) } @@ -123,14 +128,14 @@ export class GameRepository { }) } - async endGame(game_id: string) { - return Game.update({ open: false }, { where: {} }).then(() => { + async endGame(id: string) { + return Game.update({ open: false }, { where: { id } }).then(() => { const keys = Game.findOne({ - where: { id: game_id }, + where: { id }, attributes: ['actions'], }).then((res) => res?.actions.map((action: any) => action.key)) return GameHistory.findAll({ - where: { game_id }, + where: { game_id: id }, attributes: ['key', [fn('sum', col('vote')), 'total_vote']], group: ['key'], }) @@ -151,7 +156,7 @@ export class GameRepository { ) return Game.update( { winner }, - { where: { id: game_id }, returning: true }, + { where: { id }, returning: true }, ) }) }) diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index a1e27dd..0b2d076 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -4,11 +4,11 @@ import { GameHistoryInput, } from '$/interface/history.interface' import { sequelizeConnection } from '$/utils/database' +import { RedisClientType } from '@redis/client' export class GameHistory extends Model - implements GameHistoryAttributes -{ + implements GameHistoryAttributes { public game_id!: string public player_id!: string public key!: string @@ -44,15 +44,7 @@ GameHistory.init( ) export class GameHistoryRepository { - async getAllGameHistorys(): Promise { - return GameHistory.findAll() - } + constructor(private readonly redis: RedisClientType) { } - async getGameHistoryByGameId(game_id: string) { - return await GameHistory.findAll({ where: { game_id } }) - } - - async createGameHistory(gameHistory: GameHistory) { - return GameHistory.create(gameHistory) - } + getHistory(game_id: string) { } } diff --git a/apps/server/src/router/admin.router.ts b/apps/server/src/router/admin.router.ts index 4b30b6a..61b7dc6 100644 --- a/apps/server/src/router/admin.router.ts +++ b/apps/server/src/router/admin.router.ts @@ -13,17 +13,22 @@ export class AdminRouter extends BaseRouter { this.router.get( '/games', basicAuth, - this.adminController.getGames.bind(this.adminController), + this.adminController.listGames.bind(this.adminController), ) this.router.post( '/games', basicAuth, this.adminController.createGame.bind(this.adminController), ) + this.router.get( + '/games/state', + basicAuth, + this.adminController.getGameState.bind(this.adminController), + ) this.router.get( '/games/:id', basicAuth, - this.adminController.getGame.bind(this.adminController), + this.adminController.getGameByID.bind(this.adminController), ) this.router.post( '/games/:id/start', diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index bafb86d..a6a007e 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -14,12 +14,17 @@ import { AdminService } from './service/admin.service' import { sequelizeConnection } from './utils/database' import { ClientRepository } from './models/client.model' import cors from 'cors' +import { createLogger } from './utils/logger' +import createMorganLogger from './middleware/logger.middleware' export async function initServer(app: Express, server: HTTPServer) { app.use(cors()) + app.use(createMorganLogger(createLogger(''))) app.use(express.json()) app.use(express.urlencoded({ extended: true })) + const logger = createLogger('InitServer') + const pubClient = createClient({ url: process.env.REDIS_URL }) const subClient = pubClient.duplicate() @@ -28,10 +33,10 @@ export async function initServer(app: Express, server: HTTPServer) { await sequelizeConnection .authenticate() .then(() => { - console.log('Connection has been established successfully.') + logger.info(' Database Connection has been established successfully.') }) .catch((err) => { - console.error('Unable to connect to the database:', err) + logger.error('Unable to connect to the database:', { err }) }) sequelizeConnection.sync({ force: process.env.FORCE_DB_SYNC === 'true' }) @@ -46,7 +51,6 @@ export async function initServer(app: Express, server: HTTPServer) { credentials: true, methods: ['GET', 'POST'], allowedHeaders: ['fid', 'cid', 'name'], - exposedHeaders: ['fid', 'cid', 'name'], }, allowEIO3: true, }) diff --git a/apps/server/src/service/admin.service.ts b/apps/server/src/service/admin.service.ts index 9ad8aba..2c74ad4 100644 --- a/apps/server/src/service/admin.service.ts +++ b/apps/server/src/service/admin.service.ts @@ -1,17 +1,17 @@ import { Game, GameRepository } from '$/models/game.model' export class AdminService { - constructor(private readonly gameRepository: GameRepository) {} + constructor(private readonly gameRepository: GameRepository) { } - async getGame(id: string) { + async getGameByID(id: string) { return this.gameRepository.getGameById(id).catch((error) => ({ error })) } - async getAllGames() { + async listGames() { return this.gameRepository.getAllGames() } - async getState() { + async getGameState() { const games = await this.gameRepository.getLastActiveGame() return games } diff --git a/apps/server/src/service/player.service.ts b/apps/server/src/service/player.service.ts index 299914e..59096ec 100644 --- a/apps/server/src/service/player.service.ts +++ b/apps/server/src/service/player.service.ts @@ -1,7 +1,9 @@ import { Client, ClientRepository } from '$/models/client.model' import { GameRepository } from '$/models/game.model' +import { createLogger } from '$/utils/logger' export class PlayerService { + logger = createLogger('PlayerService') constructor( private readonly gameRepository: GameRepository, private readonly clientRepository: ClientRepository, @@ -11,18 +13,13 @@ export class PlayerService { return await this.gameRepository.getGameById(id).catch((err) => ({ err })) } - async getState() { - const games = await this.gameRepository.getActiveGame() - return games?.open === true ? 'playing' : 'waiting' - } - - async getLastGame() { + async getGameState() { return this.gameRepository.getLastActiveGame() } async submit(client: Client, action: string, vote: number) { - const game = await this.gameRepository.getActiveGame() - if (game?.id) { + const game = await this.gameRepository.getLastActiveGame() + if (game?.id && game.status === 'playing') { return this.gameRepository.createHistory(game.id, client.id, action, vote) } throw Error('NO GAME ON') @@ -34,9 +31,11 @@ export class PlayerService { async register(name: string, fid: string, sid: string, ipAddr?: string) { const cid = crypto.randomUUID() + if (await this.clientRepository.searchPlayer({ fid })) { - throw Error('BREACH!') + throw Error(`FID ${fid} already used`) } + const client = new Client({ cid, ipAddr, @@ -49,15 +48,29 @@ export class PlayerService { } async getScoreboard() { - const game = await this.gameRepository.getActiveGame() - if (game?.id) { - return this.gameRepository.calculateVotes(game.id) + const game = await this.gameRepository.getLastActiveGame() + if (game?.id && game.status === 'playing') { + return this.gameRepository + .calculateVotes(game.id) + .then((score) => + score?.map((s) => `${s.key} ${s.total_vote}`).join(' '), + ) } - throw Error('NO GAME ON') + return undefined } async login(cid: string, fid: string, sid: string) { - const updated = await this.clientRepository.updateSID(cid, fid, sid) - return updated + return await this.clientRepository + .updateSID(cid, fid, sid) + .then((aff) => { + if (aff[0] === 1) { + return aff[1][0] + } + throw new Error(`User not found with CID:${cid} FID:${fid}`) + }) + .catch((err) => { + this.logger.warn(err) + throw new Error('Bad CID Authentication') + }) } } diff --git a/apps/server/src/utils/logger.ts b/apps/server/src/utils/logger.ts index 30b4211..09a868c 100644 --- a/apps/server/src/utils/logger.ts +++ b/apps/server/src/utils/logger.ts @@ -1,11 +1,33 @@ import winston from 'winston' +const level = () => { + const env = process.env.NODE_ENV || 'development' + const isDevelopment = env === 'development' + return isDevelopment ? 'debug' : 'warn' +} + export function createLogger(module: string) { return winston.createLogger({ + levels: { + error: 0, + http: 1, + warn: 2, + info: 3, + debug: 4, + }, + level: level(), transports: [new winston.transports.Console()], format: winston.format.combine( - winston.format.timestamp(), - winston.format.json(), + winston.format.errors({ stack: true }), + winston.format.timestamp({ + format: 'YYYY-MM-DD hh:mm:ss.SSS A', + }), + winston.format.align(), + winston.format.colorize({ all: true }), + winston.format.printf( + (info) => + `[${info.timestamp}] ${info.level}: ${info.module} ${info.message}`, + ), ), defaultMeta: { module }, }) From 523ba747fad15bb265f7aa21a6550896c332b0c6 Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 07:47:32 +0700 Subject: [PATCH 02/17] feat: improve perf --- apps/server/package-lock.json | 1 + apps/server/package.json | 1 + .../server/src/controller/admin.controller.ts | 29 +++++-- .../src/controller/player.controller.ts | 14 ++- apps/server/src/models/game.model.ts | 16 ++-- apps/server/src/models/history.model.ts | 85 ++++++++++++++++++- apps/server/src/router/admin.router.ts | 10 +++ apps/server/src/server.ts | 6 +- apps/server/src/service/admin.service.ts | 17 +++- apps/server/src/service/player.service.ts | 20 +++-- apps/server/src/utils/database.ts | 2 +- 11 files changed, 170 insertions(+), 31 deletions(-) diff --git a/apps/server/package-lock.json b/apps/server/package-lock.json index b394800..b2c3b1c 100644 --- a/apps/server/package-lock.json +++ b/apps/server/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@redis/search": "^1.1.6", "@socket.io/redis-adapter": "^8.3.0", "@types/pg": "^8.11.4", "cors": "^2.8.5", diff --git a/apps/server/package.json b/apps/server/package.json index 9fa1f52..fdb135c 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -13,6 +13,7 @@ "author": "", "license": "ISC", "dependencies": { + "@redis/search": "^1.1.6", "@socket.io/redis-adapter": "^8.3.0", "@types/pg": "^8.11.4", "cors": "^2.8.5", diff --git a/apps/server/src/controller/admin.controller.ts b/apps/server/src/controller/admin.controller.ts index 2b44be0..c109c91 100644 --- a/apps/server/src/controller/admin.controller.ts +++ b/apps/server/src/controller/admin.controller.ts @@ -30,16 +30,35 @@ export class AdminController { } async startGame(req: Request, res: Response) { - const game = await this.adminService.startGame(req.params.id) + const game = await this.adminService.startGame(req.params.id, req.query.reset === 'true') res.json(game) - this.io.emit('events', 'start') - this.io.emit('state', await this.adminService.getGameState().then(game => game.id)) + this.io.sockets.emit('events', 'start') + this.io.sockets.emit( + 'state', + await this.adminService.getGameState().then((game) => game.id), + ) } async endGame(req: Request, res: Response) { const game = await this.adminService.endGame(req.params.id) res.json(game) - this.io.emit('events', 'stop') - this.io.emit('state', await this.adminService.getGameState().then(game => game.id)) + this.io.sockets.emit('events', 'stop') + this.io.sockets.emit( + 'state', + await this.adminService.getGameState().then((game) => game.id), + ) + } + + async getGameSummary(req: Request, res: Response) { + const summary = await this.adminService.getGameSummary(req.params.id) + res.json(summary) + } + + async setScreenState(req: Request, res: Response) { + if (req.params.state !== 'full' && req.params.state !== 'overlay') + return res.status(400) + const response = await this.adminService.setScreenState(req.params.state) + this.io.sockets.emit('screen', req.params.state) + res.json(response) } } diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index f416ffe..2a89de0 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -49,11 +49,20 @@ export class PlayerController { setInterval(async () => { await this.playerService .getScoreboard() - .then((score) => score && this.io.emit('scoreboard', score)) + .then((score) => this.io.sockets.emit('scoreboard', score)) .catch((err) => { this.logger.error(err) }) - }, 500) + }, 200) + + setInterval(async () => { + await this.playerService + .getScreenState() + .then(state => this.io.sockets.emit('screen', state)) + .catch((err) => { + this.logger.error(err) + }) + }, 5000) } async onConnection(socket: Socket) { @@ -72,7 +81,6 @@ export class PlayerController { .submit(socket.user, data[0], parseInt(data[1])) .catch((err) => { this.logger.error(err) - socket.disconnect(true) }) }) diff --git a/apps/server/src/models/game.model.ts b/apps/server/src/models/game.model.ts index bce83a6..824f3a8 100644 --- a/apps/server/src/models/game.model.ts +++ b/apps/server/src/models/game.model.ts @@ -5,7 +5,8 @@ import { GameHistory } from './history.model' export class Game extends Model - implements GameAttributes { + implements GameAttributes +{ public id!: string public title!: string public description!: string @@ -96,14 +97,16 @@ export class GameRepository { async startGame(game_id: string) { return Game.findOne({ where: { open: true } }).then(async (game) => { await game?.update({ open: false }) - return Game.update({ open: true }, { where: { id: game_id }, returning: true }).then((res) => { + return Game.update( + { open: true }, + { where: { id: game_id }, returning: true }, + ).then((res) => { if (!res[1][0]) { throw Error('Game not found') } return res[1][0] }) - } - ) + }) } async calculateVotes(game_id: string) { @@ -154,10 +157,7 @@ export class GameRepository { const winner = votes?.reduce((prev, current) => prev.total_vote > current.total_vote ? prev : current, ) - return Game.update( - { winner }, - { where: { id }, returning: true }, - ) + return Game.update({ winner }, { where: { id }, returning: true }) }) }) } diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index 0b2d076..14ec80f 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -4,7 +4,14 @@ import { GameHistoryInput, } from '$/interface/history.interface' import { sequelizeConnection } from '$/utils/database' -import { RedisClientType } from '@redis/client' +import { + RedisClientType, + RedisDefaultModules, + RedisFunctions, + RedisScripts, +} from 'redis' +import { createLogger } from '$/utils/logger' +import { Game } from './game.model' export class GameHistory extends Model @@ -44,7 +51,79 @@ GameHistory.init( ) export class GameHistoryRepository { - constructor(private readonly redis: RedisClientType) { } + logger = createLogger('GameHistoryRepository') + constructor( + private readonly redis: RedisClientType< + RedisDefaultModules, + RedisFunctions, + RedisScripts + >, + ) { } + + async createHistory( + game_id: string, + player_id: string, + key: string, + vote: number, + ) { + await this.redis.incrBy(`game::${game_id}::${key}`, vote) + return GameHistory.findOne({ where: { game_id, player_id, key } }).then(async res => { + if (res) { + return GameHistory.increment({ vote }, { where: { game_id, player_id, key } }) + } + else { + return GameHistory.create({ game_id, player_id, key, vote }) + } + }) + } + + async getHistoryByPlayerID(game_id: string, player_id: string) { + return GameHistory.findAll({ where: { game_id, player_id } }) + } + + async getHistoryByGameID(game_id: string) { + return GameHistory.findAll({ where: { game_id } }) + } + + async summaryGame(game_id: string) { + const game = await Game.findByPk(game_id) + if (!game) { + throw new Error('Game not found') + } + const keys = await this.redis.keys(`game::${game_id}::*`) + const votes = await this.redis.mGet(keys) + + return keys.map((key, index) => ({ + key: key.split('::')[2], + vote: votes[index], + })) + + } + + async setScreenState(state: 'full' | 'overlay') { + return this.redis.set('state::screen', state).catch((err) => { + this.logger.error(err) + throw new Error("Can't set screen state") + }) + } + + async getScreenState() { + return await this.redis + .get('state::screen') + .then((state) => state || 'full') + } - getHistory(game_id: string) { } + async startGame(id: string, reset: boolean) { + return Game.findByPk(id).then(async (game) => { + if (game && reset) { + game.actions.forEach((action) => { + this.redis.set(`game::${id}::${action.key}`, 0) + }) + await GameHistory.update({ vote: 0 }, { where: { game_id: id } }) + } + }).then(() => "OK").catch((err) => { + this.logger.error(err) + throw new Error("Can't set game redis keys") + }) + } } diff --git a/apps/server/src/router/admin.router.ts b/apps/server/src/router/admin.router.ts index 61b7dc6..30dea88 100644 --- a/apps/server/src/router/admin.router.ts +++ b/apps/server/src/router/admin.router.ts @@ -40,5 +40,15 @@ export class AdminRouter extends BaseRouter { basicAuth, this.adminController.endGame.bind(this.adminController), ) + this.router.put( + '/screen/state/:state', + basicAuth, + this.adminController.setScreenState.bind(this.adminController), + ) + this.router.get( + '/games/:id/summary', + basicAuth, + this.adminController.getGameSummary.bind(this.adminController), + ) } } diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index a6a007e..c84a37d 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -16,6 +16,7 @@ import { ClientRepository } from './models/client.model' import cors from 'cors' import { createLogger } from './utils/logger' import createMorganLogger from './middleware/logger.middleware' +import { GameHistoryRepository } from './models/history.model' export async function initServer(app: Express, server: HTTPServer) { app.use(cors()) @@ -42,6 +43,7 @@ export async function initServer(app: Express, server: HTTPServer) { const gameRepository = new GameRepository() const clientRepository = new ClientRepository() + const gameHistoryRepository = new GameHistoryRepository(pubClient) const playerIO = new Server(server, { adapter: createAdapter(pubClient, subClient), @@ -56,7 +58,7 @@ export async function initServer(app: Express, server: HTTPServer) { }) const playerController = new PlayerController( playerIO, - new PlayerService(gameRepository, clientRepository), + new PlayerService(gameRepository, clientRepository, gameHistoryRepository), ) playerIO.on( 'connection', @@ -65,7 +67,7 @@ export async function initServer(app: Express, server: HTTPServer) { const adminController = new AdminController( playerIO, - new AdminService(gameRepository), + new AdminService(gameRepository, gameHistoryRepository), ) const playerRouter = new PlayerRouter(playerController) diff --git a/apps/server/src/service/admin.service.ts b/apps/server/src/service/admin.service.ts index 2c74ad4..869a5e6 100644 --- a/apps/server/src/service/admin.service.ts +++ b/apps/server/src/service/admin.service.ts @@ -1,7 +1,11 @@ import { Game, GameRepository } from '$/models/game.model' +import { GameHistoryRepository } from '$/models/history.model' export class AdminService { - constructor(private readonly gameRepository: GameRepository) { } + constructor( + private readonly gameRepository: GameRepository, + private readonly gameHistoryRepository: GameHistoryRepository, + ) { } async getGameByID(id: string) { return this.gameRepository.getGameById(id).catch((error) => ({ error })) @@ -27,11 +31,20 @@ export class AdminService { return this.gameRepository.createGame(gameModel) } - async startGame(id: string) { + async startGame(id: string, reset: boolean) { + this.gameHistoryRepository.startGame(id, reset) return this.gameRepository.startGame(id) } async endGame(id: string) { return this.gameRepository.endGame(id) } + + async getGameSummary(id: string) { + return this.gameHistoryRepository.summaryGame(id) + } + + async setScreenState(state: 'full' | 'overlay') { + return this.gameHistoryRepository.setScreenState(state) + } } diff --git a/apps/server/src/service/player.service.ts b/apps/server/src/service/player.service.ts index 59096ec..e25ddc4 100644 --- a/apps/server/src/service/player.service.ts +++ b/apps/server/src/service/player.service.ts @@ -1,5 +1,6 @@ import { Client, ClientRepository } from '$/models/client.model' import { GameRepository } from '$/models/game.model' +import { GameHistoryRepository } from '$/models/history.model' import { createLogger } from '$/utils/logger' export class PlayerService { @@ -7,7 +8,8 @@ export class PlayerService { constructor( private readonly gameRepository: GameRepository, private readonly clientRepository: ClientRepository, - ) {} + private readonly gameHistoryRepository: GameHistoryRepository, + ) { } async getGame(id: string) { return await this.gameRepository.getGameById(id).catch((err) => ({ err })) @@ -19,10 +21,10 @@ export class PlayerService { async submit(client: Client, action: string, vote: number) { const game = await this.gameRepository.getLastActiveGame() - if (game?.id && game.status === 'playing') { - return this.gameRepository.createHistory(game.id, client.id, action, vote) + if (game && game.id && game.status === 'playing') { + return await this.gameHistoryRepository.createHistory(game.id, client.id, action, vote) } - throw Error('NO GAME ON') + throw Error('No game is playing') } async getMyID(ipAddr?: string, cid?: string, sid?: string) { @@ -50,10 +52,10 @@ export class PlayerService { async getScoreboard() { const game = await this.gameRepository.getLastActiveGame() if (game?.id && game.status === 'playing') { - return this.gameRepository - .calculateVotes(game.id) + return this.gameHistoryRepository + .summaryGame(game.id) .then((score) => - score?.map((s) => `${s.key} ${s.total_vote}`).join(' '), + score?.map((s) => `${s.key} ${s.vote}`).join(' '), ) } return undefined @@ -73,4 +75,8 @@ export class PlayerService { throw new Error('Bad CID Authentication') }) } + + async getScreenState() { + return await this.gameHistoryRepository.getScreenState() + } } diff --git a/apps/server/src/utils/database.ts b/apps/server/src/utils/database.ts index b783ce2..10a393a 100644 --- a/apps/server/src/utils/database.ts +++ b/apps/server/src/utils/database.ts @@ -11,7 +11,7 @@ function createConnectionPool() { sync: { force: (process.env.NODE_ENV || 'development') !== 'production', }, - logging: false, + logging: (process.env.NODE_ENV || 'development') !== 'production', }) } From e2f7fc74317bd31cae62505500e45ed834a1f289 Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 08:31:27 +0700 Subject: [PATCH 03/17] feat: improve perf --- .../server/src/controller/admin.controller.ts | 3 +- .../src/controller/player.controller.ts | 38 +++++++++---------- apps/server/src/models/history.model.ts | 26 ++++++------- apps/server/src/service/admin.service.ts | 2 + apps/server/src/service/player.service.ts | 24 ++++++------ apps/server/src/utils/database.ts | 2 +- 6 files changed, 46 insertions(+), 49 deletions(-) diff --git a/apps/server/src/controller/admin.controller.ts b/apps/server/src/controller/admin.controller.ts index c109c91..0d964a8 100644 --- a/apps/server/src/controller/admin.controller.ts +++ b/apps/server/src/controller/admin.controller.ts @@ -47,6 +47,7 @@ export class AdminController { 'state', await this.adminService.getGameState().then((game) => game.id), ) + this.io.engine.emit('stop') } async getGameSummary(req: Request, res: Response) { @@ -58,7 +59,7 @@ export class AdminController { if (req.params.state !== 'full' && req.params.state !== 'overlay') return res.status(400) const response = await this.adminService.setScreenState(req.params.state) - this.io.sockets.emit('screen', req.params.state) + this.io.sockets.to('scoreboard').emit('screen', req.params.state) res.json(response) } } diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index 2a89de0..54fa02a 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -5,11 +5,19 @@ import { createLogger } from '$/utils/logger' export class PlayerController { private readonly logger = createLogger('PlayerController') + private interval: NodeJS.Timeout | null = null constructor( private readonly io: Server, private readonly playerService: PlayerService, ) { - this.setupScoreboardEmitter() + this.interval = setInterval(async () => { + await this.playerService + .getScreenState() + .then(state => this.io.sockets.to('scoreboard').emit('screen', state)) + .catch((err) => { + this.logger.error(err) + }) + }, 5000) } async authenticateSocket(socket: Socket) { @@ -45,26 +53,6 @@ export class PlayerController { } else socket.disconnect(true) } - setupScoreboardEmitter() { - setInterval(async () => { - await this.playerService - .getScoreboard() - .then((score) => this.io.sockets.emit('scoreboard', score)) - .catch((err) => { - this.logger.error(err) - }) - }, 200) - - setInterval(async () => { - await this.playerService - .getScreenState() - .then(state => this.io.sockets.emit('screen', state)) - .catch((err) => { - this.logger.error(err) - }) - }, 5000) - } - async onConnection(socket: Socket) { this.logger.info(`New connection: ${socket.id}`) @@ -74,6 +62,8 @@ export class PlayerController { socket.emit('events', game.status) }) + socket.on('subscribe', (m, cb) => socket.join('scoreboard') && cb("OK")) + socket.on('submit', async (message) => { this.logger.info('Received message', message) const data = message.split(' ') @@ -83,6 +73,12 @@ export class PlayerController { this.logger.error(err) socket.disconnect(true) }) + await this.playerService + .getScoreboard() + .then((score) => this.io.sockets.to('scoreboard').emit('scoreboard', score)) + .catch((err) => { + this.logger.error(err) + }) }) socket.on('disconnect', () => { diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index 14ec80f..9cc64f0 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -52,12 +52,10 @@ GameHistory.init( export class GameHistoryRepository { logger = createLogger('GameHistoryRepository') + private keys: string[] = [] constructor( private readonly redis: RedisClientType< - RedisDefaultModules, - RedisFunctions, - RedisScripts - >, + RedisDefaultModules, RedisFunctions, RedisScripts> ) { } async createHistory( @@ -86,11 +84,7 @@ export class GameHistoryRepository { } async summaryGame(game_id: string) { - const game = await Game.findByPk(game_id) - if (!game) { - throw new Error('Game not found') - } - const keys = await this.redis.keys(`game::${game_id}::*`) + const keys = this.keys.map((key) => `game::${game_id}::${key}`) const votes = await this.redis.mGet(keys) return keys.map((key, index) => ({ @@ -115,11 +109,15 @@ export class GameHistoryRepository { async startGame(id: string, reset: boolean) { return Game.findByPk(id).then(async (game) => { - if (game && reset) { - game.actions.forEach((action) => { - this.redis.set(`game::${id}::${action.key}`, 0) - }) - await GameHistory.update({ vote: 0 }, { where: { game_id: id } }) + + if (game) { + this.keys = game.actions.map((action) => action.key) + if (reset) { + game.actions.forEach((action) => { + this.redis.set(`game::${id}::${action.key}`, 0) + }) + await GameHistory.update({ vote: 0 }, { where: { game_id: id } }) + } } }).then(() => "OK").catch((err) => { this.logger.error(err) diff --git a/apps/server/src/service/admin.service.ts b/apps/server/src/service/admin.service.ts index 869a5e6..faabecf 100644 --- a/apps/server/src/service/admin.service.ts +++ b/apps/server/src/service/admin.service.ts @@ -47,4 +47,6 @@ export class AdminService { async setScreenState(state: 'full' | 'overlay') { return this.gameHistoryRepository.setScreenState(state) } + + } diff --git a/apps/server/src/service/player.service.ts b/apps/server/src/service/player.service.ts index e25ddc4..a19619f 100644 --- a/apps/server/src/service/player.service.ts +++ b/apps/server/src/service/player.service.ts @@ -49,18 +49,6 @@ export class PlayerService { return await client.save() } - async getScoreboard() { - const game = await this.gameRepository.getLastActiveGame() - if (game?.id && game.status === 'playing') { - return this.gameHistoryRepository - .summaryGame(game.id) - .then((score) => - score?.map((s) => `${s.key} ${s.vote}`).join(' '), - ) - } - return undefined - } - async login(cid: string, fid: string, sid: string) { return await this.clientRepository .updateSID(cid, fid, sid) @@ -79,4 +67,16 @@ export class PlayerService { async getScreenState() { return await this.gameHistoryRepository.getScreenState() } + + async getScoreboard() { + const game = await this.gameRepository.getLastActiveGame() + if (game?.id && game.status === 'playing') { + return this.gameHistoryRepository + .summaryGame(game.id) + .then((score) => + score?.map((s) => `${s.key} ${s.vote}`).join(' '), + ) + } + return undefined + } } diff --git a/apps/server/src/utils/database.ts b/apps/server/src/utils/database.ts index 10a393a..e7a65ff 100644 --- a/apps/server/src/utils/database.ts +++ b/apps/server/src/utils/database.ts @@ -11,7 +11,7 @@ function createConnectionPool() { sync: { force: (process.env.NODE_ENV || 'development') !== 'production', }, - logging: (process.env.NODE_ENV || 'development') !== 'production', + logging: true, }) } From 3639d1a3f1582179e55861d1e021258696ddba45 Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 08:31:41 +0700 Subject: [PATCH 04/17] feat: improve socket time --- .../server/src/controller/admin.controller.ts | 10 +++- .../src/controller/player.controller.ts | 20 ++++--- apps/server/src/models/game.model.ts | 6 +- apps/server/src/models/history.model.ts | 60 ++++++++++--------- apps/server/src/service/admin.service.ts | 8 +-- apps/server/src/service/player.service.ts | 22 +++---- 6 files changed, 70 insertions(+), 56 deletions(-) diff --git a/apps/server/src/controller/admin.controller.ts b/apps/server/src/controller/admin.controller.ts index 0d964a8..c93e4d8 100644 --- a/apps/server/src/controller/admin.controller.ts +++ b/apps/server/src/controller/admin.controller.ts @@ -16,6 +16,7 @@ export class AdminController { async getGameByID(req: Request, res: Response) { const game = await this.adminService.getGameByID(req.params.id) + if (!game) return res.status(404).send({ err: 'Game not found' }) res.json(game) } @@ -30,7 +31,10 @@ export class AdminController { } async startGame(req: Request, res: Response) { - const game = await this.adminService.startGame(req.params.id, req.query.reset === 'true') + const game = await this.adminService.startGame( + req.params.id, + req.query.reset === 'true', + ) res.json(game) this.io.sockets.emit('events', 'start') this.io.sockets.emit( @@ -51,7 +55,9 @@ export class AdminController { } async getGameSummary(req: Request, res: Response) { - const summary = await this.adminService.getGameSummary(req.params.id) + const game = await this.adminService.getGameByID(req.params.id) + if (!game) return res.status(404).send({ err: 'Game not found' }) + const summary = await this.adminService.getGameSummary(game.id, game.actions.map((a) => a.key)) res.json(summary) } diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index 54fa02a..afde94d 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -13,7 +13,7 @@ export class PlayerController { this.interval = setInterval(async () => { await this.playerService .getScreenState() - .then(state => this.io.sockets.to('scoreboard').emit('screen', state)) + .then((state) => this.io.sockets.to('scoreboard').emit('screen', state)) .catch((err) => { this.logger.error(err) }) @@ -62,23 +62,27 @@ export class PlayerController { socket.emit('events', game.status) }) - socket.on('subscribe', (m, cb) => socket.join('scoreboard') && cb("OK")) + socket.on('subscribe', (m, cb) => socket.join('scoreboard') && cb('OK')) socket.on('submit', async (message) => { this.logger.info('Received message', message) const data = message.split(' ') this.playerService .submit(socket.user, data[0], parseInt(data[1])) + .then((game) => { + console.log(game) + if (game.id && game.game) + return this.playerService + .getScoreboard(game.id, game.game.actions.map((a) => a.key)) + .then((score) => + this.io.sockets.to('scoreboard').emit('scoreboard', score), + ) + } + ) .catch((err) => { this.logger.error(err) socket.disconnect(true) }) - await this.playerService - .getScoreboard() - .then((score) => this.io.sockets.to('scoreboard').emit('scoreboard', score)) - .catch((err) => { - this.logger.error(err) - }) }) socket.on('disconnect', () => { diff --git a/apps/server/src/models/game.model.ts b/apps/server/src/models/game.model.ts index 824f3a8..52280ba 100644 --- a/apps/server/src/models/game.model.ts +++ b/apps/server/src/models/game.model.ts @@ -5,8 +5,7 @@ import { GameHistory } from './history.model' export class Game extends Model - implements GameAttributes -{ + implements GameAttributes { public id!: string public title!: string public description!: string @@ -69,10 +68,11 @@ export class GameRepository { async getLastActiveGame() { const game = await Game.findOne({ order: [['updatedAt', 'DESC']], - attributes: ['id', 'open'], + attributes: ['id', 'open', 'actions'], limit: 1, }).then((res) => ({ id: res?.id, + game: res, status: res?.open ? 'playing' : 'waiting', })) return game diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index 9cc64f0..95bbfc1 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -52,10 +52,12 @@ GameHistory.init( export class GameHistoryRepository { logger = createLogger('GameHistoryRepository') - private keys: string[] = [] constructor( private readonly redis: RedisClientType< - RedisDefaultModules, RedisFunctions, RedisScripts> + RedisDefaultModules, + RedisFunctions, + RedisScripts + >, ) { } async createHistory( @@ -65,14 +67,18 @@ export class GameHistoryRepository { vote: number, ) { await this.redis.incrBy(`game::${game_id}::${key}`, vote) - return GameHistory.findOne({ where: { game_id, player_id, key } }).then(async res => { - if (res) { - return GameHistory.increment({ vote }, { where: { game_id, player_id, key } }) - } - else { - return GameHistory.create({ game_id, player_id, key, vote }) - } - }) + return GameHistory.findOne({ where: { game_id, player_id, key } }).then( + async (res) => { + if (res) { + return GameHistory.increment( + { vote }, + { where: { game_id, player_id, key } }, + ) + } else { + return GameHistory.create({ game_id, player_id, key, vote }) + } + }, + ) } async getHistoryByPlayerID(game_id: string, player_id: string) { @@ -83,15 +89,14 @@ export class GameHistoryRepository { return GameHistory.findAll({ where: { game_id } }) } - async summaryGame(game_id: string) { - const keys = this.keys.map((key) => `game::${game_id}::${key}`) + async summaryGame(game_id: string, game_keys: string[]) { + const keys = game_keys.map((key) => `game::${game_id}::${key}`) const votes = await this.redis.mGet(keys) return keys.map((key, index) => ({ key: key.split('::')[2], vote: votes[index], })) - } async setScreenState(state: 'full' | 'overlay') { @@ -108,20 +113,21 @@ export class GameHistoryRepository { } async startGame(id: string, reset: boolean) { - return Game.findByPk(id).then(async (game) => { - - if (game) { - this.keys = game.actions.map((action) => action.key) - if (reset) { - game.actions.forEach((action) => { - this.redis.set(`game::${id}::${action.key}`, 0) - }) - await GameHistory.update({ vote: 0 }, { where: { game_id: id } }) + return Game.findByPk(id) + .then(async (game) => { + if (game) { + if (reset) { + game.actions.forEach((action) => { + this.redis.set(`game::${id}::${action.key}`, 0) + }) + await GameHistory.update({ vote: 0 }, { where: { game_id: id } }) + } } - } - }).then(() => "OK").catch((err) => { - this.logger.error(err) - throw new Error("Can't set game redis keys") - }) + }) + .then(() => 'OK') + .catch((err) => { + this.logger.error(err) + throw new Error("Can't set game redis keys") + }) } } diff --git a/apps/server/src/service/admin.service.ts b/apps/server/src/service/admin.service.ts index faabecf..f99d15e 100644 --- a/apps/server/src/service/admin.service.ts +++ b/apps/server/src/service/admin.service.ts @@ -8,7 +8,7 @@ export class AdminService { ) { } async getGameByID(id: string) { - return this.gameRepository.getGameById(id).catch((error) => ({ error })) + return this.gameRepository.getGameById(id) } async listGames() { @@ -40,13 +40,11 @@ export class AdminService { return this.gameRepository.endGame(id) } - async getGameSummary(id: string) { - return this.gameHistoryRepository.summaryGame(id) + async getGameSummary(id: string, game_keys: string[]) { + return this.gameHistoryRepository.summaryGame(id, game_keys) } async setScreenState(state: 'full' | 'overlay') { return this.gameHistoryRepository.setScreenState(state) } - - } diff --git a/apps/server/src/service/player.service.ts b/apps/server/src/service/player.service.ts index a19619f..8883f2f 100644 --- a/apps/server/src/service/player.service.ts +++ b/apps/server/src/service/player.service.ts @@ -22,7 +22,13 @@ export class PlayerService { async submit(client: Client, action: string, vote: number) { const game = await this.gameRepository.getLastActiveGame() if (game && game.id && game.status === 'playing') { - return await this.gameHistoryRepository.createHistory(game.id, client.id, action, vote) + await this.gameHistoryRepository.createHistory( + game.id, + client.id, + action, + vote, + ) + return game } throw Error('No game is playing') } @@ -68,15 +74,9 @@ export class PlayerService { return await this.gameHistoryRepository.getScreenState() } - async getScoreboard() { - const game = await this.gameRepository.getLastActiveGame() - if (game?.id && game.status === 'playing') { - return this.gameHistoryRepository - .summaryGame(game.id) - .then((score) => - score?.map((s) => `${s.key} ${s.vote}`).join(' '), - ) - } - return undefined + async getScoreboard(game_id: string, game_keys: string[]) { + return this.gameHistoryRepository + .summaryGame(game_id, game_keys) + .then((score) => score?.map((s) => `${s.key} ${s.vote}`).join(' ')) } } From 44c587cf618684de39892073ba086af5bf1352de Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 08:43:09 +0700 Subject: [PATCH 05/17] feat: scoreboard weight --- apps/server/src/controller/player.controller.ts | 13 +++++++++++-- apps/server/src/models/history.model.ts | 2 +- apps/server/src/service/player.service.ts | 8 +++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index afde94d..de5f06d 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -62,7 +62,17 @@ export class PlayerController { socket.emit('events', game.status) }) - socket.on('subscribe', (m, cb) => socket.join('scoreboard') && cb('OK')) + socket.on('subscribe', (m, cb) => { + socket.join('scoreboard'); + this.playerService.getGameState().then((game) => { + if (game.id && game.game && game.status === 'playing') + this.playerService + .getScoreboard(game.id, game.game.actions.map((a) => a.key)) + .then((score) => { + socket.emit('scoreboard', score) + }) + }) + }) socket.on('submit', async (message) => { this.logger.info('Received message', message) @@ -70,7 +80,6 @@ export class PlayerController { this.playerService .submit(socket.user, data[0], parseInt(data[1])) .then((game) => { - console.log(game) if (game.id && game.game) return this.playerService .getScoreboard(game.id, game.game.actions.map((a) => a.key)) diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index 95bbfc1..a32a9f9 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -95,7 +95,7 @@ export class GameHistoryRepository { return keys.map((key, index) => ({ key: key.split('::')[2], - vote: votes[index], + vote: parseInt(votes[index] || '0'), })) } diff --git a/apps/server/src/service/player.service.ts b/apps/server/src/service/player.service.ts index 8883f2f..b5b9efb 100644 --- a/apps/server/src/service/player.service.ts +++ b/apps/server/src/service/player.service.ts @@ -77,6 +77,12 @@ export class PlayerService { async getScoreboard(game_id: string, game_keys: string[]) { return this.gameHistoryRepository .summaryGame(game_id, game_keys) - .then((score) => score?.map((s) => `${s.key} ${s.vote}`).join(' ')) + .then((score) => { + const total_vote = score.reduce((acc, s) => acc + s.vote, 0) + if (total_vote === 0) { + return score.map((s) => `${s.key} ${(100 / score.length).toFixed(2)}`).join(' ') + } + return score?.map((s) => `${s.key} ${(s.vote / total_vote * 100).toFixed(2)}`).join(' ') + }) } } From 86c7b99614bf8a5eab0c8bb895387174556e971d Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 08:47:31 +0700 Subject: [PATCH 06/17] fix: scoreboard event when start game --- apps/server/src/controller/admin.controller.ts | 1 + apps/server/src/models/game.model.ts | 2 +- apps/server/src/service/admin.service.ts | 12 ++++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/server/src/controller/admin.controller.ts b/apps/server/src/controller/admin.controller.ts index c93e4d8..23256b4 100644 --- a/apps/server/src/controller/admin.controller.ts +++ b/apps/server/src/controller/admin.controller.ts @@ -41,6 +41,7 @@ export class AdminController { 'state', await this.adminService.getGameState().then((game) => game.id), ) + this.io.sockets.to('scoreboard').emit('scoreboard', await this.adminService.getScoreboard(game.id, game.actions.map((a) => a.key))) } async endGame(req: Request, res: Response) { diff --git a/apps/server/src/models/game.model.ts b/apps/server/src/models/game.model.ts index 52280ba..b938104 100644 --- a/apps/server/src/models/game.model.ts +++ b/apps/server/src/models/game.model.ts @@ -104,7 +104,7 @@ export class GameRepository { if (!res[1][0]) { throw Error('Game not found') } - return res[1][0] + return res[1][0] as Game }) }) } diff --git a/apps/server/src/service/admin.service.ts b/apps/server/src/service/admin.service.ts index f99d15e..d222771 100644 --- a/apps/server/src/service/admin.service.ts +++ b/apps/server/src/service/admin.service.ts @@ -36,6 +36,18 @@ export class AdminService { return this.gameRepository.startGame(id) } + async getScoreboard(game_id: string, game_keys: string[]) { + return this.gameHistoryRepository + .summaryGame(game_id, game_keys) + .then((score) => { + const total_vote = score.reduce((acc, s) => acc + s.vote, 0) + if (total_vote === 0) { + return score.map((s) => `${s.key} ${(100 / score.length).toFixed(2)}`).join(' ') + } + return score?.map((s) => `${s.key} ${(s.vote / total_vote * 100).toFixed(2)}`).join(' ') + }) + } + async endGame(id: string) { return this.gameRepository.endGame(id) } From 00b759922ecc5d28f9cdb8df221beca219116cd7 Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 13:12:06 +0700 Subject: [PATCH 07/17] feat: db logging --- apps/server/src/utils/database.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/utils/database.ts b/apps/server/src/utils/database.ts index e7a65ff..b783ce2 100644 --- a/apps/server/src/utils/database.ts +++ b/apps/server/src/utils/database.ts @@ -11,7 +11,7 @@ function createConnectionPool() { sync: { force: (process.env.NODE_ENV || 'development') !== 'production', }, - logging: true, + logging: false, }) } From f416dc894adf0ca1f9ec32bafb31e05650ac8b99 Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 13:33:49 +0700 Subject: [PATCH 08/17] feat: remove healthz logging --- apps/server/src/controller/player.controller.ts | 7 ++++--- apps/server/src/server.ts | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index de5f06d..fe61a2d 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -75,11 +75,12 @@ export class PlayerController { }) socket.on('submit', async (message) => { - this.logger.info('Received message', message) - const data = message.split(' ') + this.logger.debug("Received message: " + String(message).trim()) + const data = String(message).trim().split(' ') this.playerService .submit(socket.user, data[0], parseInt(data[1])) .then((game) => { + this.logger.debug(`Incremented: ${String(message).trim()}`) if (game.id && game.game) return this.playerService .getScoreboard(game.id, game.game.actions.map((a) => a.key)) @@ -89,7 +90,7 @@ export class PlayerController { } ) .catch((err) => { - this.logger.error(err) + this.logger.warn("Failed to increment: " + err) socket.disconnect(true) }) }) diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index c84a37d..e57a853 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -20,6 +20,9 @@ import { GameHistoryRepository } from './models/history.model' export async function initServer(app: Express, server: HTTPServer) { app.use(cors()) + app.get('/healthz', (req, res) => { + res.send('OK') + }) app.use(createMorganLogger(createLogger(''))) app.use(express.json()) app.use(express.urlencoded({ extended: true })) @@ -76,9 +79,5 @@ export async function initServer(app: Express, server: HTTPServer) { app.use(playerRouter.prefix, playerRouter.router) app.use(adminRouter.prefix, adminRouter.router) - app.get('/healthz', (req, res) => { - res.send('OK') - }) - return app } From e054cec04ac5052dcc4097f95b60953c3b9db55e Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 13:59:14 +0700 Subject: [PATCH 09/17] feat: remove pg layer history --- apps/server/src/models/game.model.ts | 22 ----------- apps/server/src/models/history.model.ts | 48 +++++++++++++---------- apps/server/src/service/admin.service.ts | 2 +- apps/server/src/service/player.service.ts | 2 +- 4 files changed, 30 insertions(+), 44 deletions(-) diff --git a/apps/server/src/models/game.model.ts b/apps/server/src/models/game.model.ts index b938104..8887a95 100644 --- a/apps/server/src/models/game.model.ts +++ b/apps/server/src/models/game.model.ts @@ -109,28 +109,6 @@ export class GameRepository { }) } - async calculateVotes(game_id: string) { - const keys = Game.findOne({ - where: { id: game_id }, - attributes: ['actions'], - }).then((res) => res?.actions.map((action: any) => action.key)) - return GameHistory.findAll({ - where: { game_id }, - attributes: ['key', [fn('sum', col('vote')), 'total_vote']], - group: ['key'], - }).then(async (votes) => { - return keys.then((keys) => { - return keys?.map((key: string) => { - const vote = votes.find((vote) => vote.key === key) - return { - key, - total_vote: parseInt(vote?.dataValues.total_vote || '0'), - } - }) - }) - }) - } - async endGame(id: string) { return Game.update({ open: false }, { where: { id } }).then(() => { const keys = Game.findOne({ diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index a32a9f9..3fe31bb 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -66,19 +66,7 @@ export class GameHistoryRepository { key: string, vote: number, ) { - await this.redis.incrBy(`game::${game_id}::${key}`, vote) - return GameHistory.findOne({ where: { game_id, player_id, key } }).then( - async (res) => { - if (res) { - return GameHistory.increment( - { vote }, - { where: { game_id, player_id, key } }, - ) - } else { - return GameHistory.create({ game_id, player_id, key, vote }) - } - }, - ) + return await this.redis.incrBy(`game::${game_id}::${key}`, vote) } async getHistoryByPlayerID(game_id: string, player_id: string) { @@ -115,14 +103,12 @@ export class GameHistoryRepository { async startGame(id: string, reset: boolean) { return Game.findByPk(id) .then(async (game) => { - if (game) { - if (reset) { - game.actions.forEach((action) => { - this.redis.set(`game::${id}::${action.key}`, 0) - }) - await GameHistory.update({ vote: 0 }, { where: { game_id: id } }) - } + if (game && reset) { + game.actions.forEach((action) => { + this.redis.set(`game::${id}::${action.key}`, 0) + }) } + return this.redis.set('state::game', id) }) .then(() => 'OK') .catch((err) => { @@ -130,4 +116,26 @@ export class GameHistoryRepository { throw new Error("Can't set game redis keys") }) } + + async getLastActiveGame() { + return await this.redis.get('state::game').then((game) => { + if (game) { + return Game.findByPk(game).then((res) => ({ + id: res?.id, + game: res, + status: res?.open ? 'playing' : 'waiting', + })) + } + return Game.findOne({ + order: [['updatedAt', 'DESC']], + attributes: ['id', 'open', 'actions'], + limit: 1, + }).then((res) => ({ + id: res?.id, + game: res, + status: res?.open ? 'playing' : 'waiting', + })) + }) + } + } diff --git a/apps/server/src/service/admin.service.ts b/apps/server/src/service/admin.service.ts index d222771..1ec4b86 100644 --- a/apps/server/src/service/admin.service.ts +++ b/apps/server/src/service/admin.service.ts @@ -16,7 +16,7 @@ export class AdminService { } async getGameState() { - const games = await this.gameRepository.getLastActiveGame() + const games = await this.gameHistoryRepository.getLastActiveGame() return games } diff --git a/apps/server/src/service/player.service.ts b/apps/server/src/service/player.service.ts index b5b9efb..7280e6c 100644 --- a/apps/server/src/service/player.service.ts +++ b/apps/server/src/service/player.service.ts @@ -20,7 +20,7 @@ export class PlayerService { } async submit(client: Client, action: string, vote: number) { - const game = await this.gameRepository.getLastActiveGame() + const game = await this.gameHistoryRepository.getLastActiveGame() if (game && game.id && game.status === 'playing') { await this.gameHistoryRepository.createHistory( game.id, From 03451961775e5aefbb6794a86cc40c462340182c Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 14:28:11 +0700 Subject: [PATCH 10/17] feat: scoreboard log --- apps/server/src/controller/player.controller.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index fe61a2d..167bcfd 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -81,12 +81,14 @@ export class PlayerController { .submit(socket.user, data[0], parseInt(data[1])) .then((game) => { this.logger.debug(`Incremented: ${String(message).trim()}`) - if (game.id && game.game) + if (game.id && game.game) { + this.logger.debug("Getting scoreboard updated") return this.playerService .getScoreboard(game.id, game.game.actions.map((a) => a.key)) .then((score) => this.io.sockets.to('scoreboard').emit('scoreboard', score), ) + } } ) .catch((err) => { From 5bf3e3a654baa05ea7ec99624a14c4ab15c22f82 Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 16:25:46 +0700 Subject: [PATCH 11/17] fix: all rooms broadvaster --- apps/server/src/controller/admin.controller.ts | 2 +- apps/server/src/controller/player.controller.ts | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/server/src/controller/admin.controller.ts b/apps/server/src/controller/admin.controller.ts index 23256b4..e4613c0 100644 --- a/apps/server/src/controller/admin.controller.ts +++ b/apps/server/src/controller/admin.controller.ts @@ -66,7 +66,7 @@ export class AdminController { if (req.params.state !== 'full' && req.params.state !== 'overlay') return res.status(400) const response = await this.adminService.setScreenState(req.params.state) - this.io.sockets.to('scoreboard').emit('screen', req.params.state) + this.io.to('scoreboard').emit('screen', req.params.state) res.json(response) } } diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index 167bcfd..aaac96c 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -5,19 +5,10 @@ import { createLogger } from '$/utils/logger' export class PlayerController { private readonly logger = createLogger('PlayerController') - private interval: NodeJS.Timeout | null = null constructor( private readonly io: Server, private readonly playerService: PlayerService, ) { - this.interval = setInterval(async () => { - await this.playerService - .getScreenState() - .then((state) => this.io.sockets.to('scoreboard').emit('screen', state)) - .catch((err) => { - this.logger.error(err) - }) - }, 5000) } async authenticateSocket(socket: Socket) { @@ -86,7 +77,7 @@ export class PlayerController { return this.playerService .getScoreboard(game.id, game.game.actions.map((a) => a.key)) .then((score) => - this.io.sockets.to('scoreboard').emit('scoreboard', score), + this.io.to('scoreboard').emit('scoreboard', score), ) } } From 5a29b7095788f8d3d59534680e4dde18e5d51e32 Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 17:08:36 +0700 Subject: [PATCH 12/17] fix: subClient connect --- apps/server/src/server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index e57a853..3b5ef2d 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -33,6 +33,7 @@ export async function initServer(app: Express, server: HTTPServer) { const subClient = pubClient.duplicate() await pubClient.connect() + await subClient.connect() await sequelizeConnection .authenticate() From d18029fbdeac24d63851c0f0c72a7ba9a9539dec Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 17:21:35 +0700 Subject: [PATCH 13/17] feat: weight score --- apps/server/src/models/history.model.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index 3fe31bb..2748f2f 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -66,7 +66,14 @@ export class GameHistoryRepository { key: string, vote: number, ) { - return await this.redis.incrBy(`game::${game_id}::${key}`, vote) + const total = await this.redis.incrBy(`game::${game_id}::${key}`, vote) + this.redis.keys(`game::${game_id}::*`).then((keys) => { + keys.forEach(async (k) => { + if (k !== `game::${game_id}::${key}`) { + this.redis.decrBy(k, Math.floor((total - parseInt(await this.redis.get(k) || '0')) * vote / 100)) + } + }) + }) } async getHistoryByPlayerID(game_id: string, player_id: string) { From d7124b9aa2f39a5c7cfa11bc611ea1c77789ec4c Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 17:26:58 +0700 Subject: [PATCH 14/17] feat: weight score clamp --- apps/server/src/models/history.model.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index 2748f2f..53ef2c7 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -70,7 +70,10 @@ export class GameHistoryRepository { this.redis.keys(`game::${game_id}::*`).then((keys) => { keys.forEach(async (k) => { if (k !== `game::${game_id}::${key}`) { - this.redis.decrBy(k, Math.floor((total - parseInt(await this.redis.get(k) || '0')) * vote / 100)) + const kTotal = parseInt(await this.redis.get(k) || '0') + const decrease = ((Math.floor((total - kTotal) * vote / 100))) + const remain = kTotal - decrease + if (remain > 0) this.redis.decrBy(k, decrease) } }) }) From 336c4083c1ceee0296fed2b7e2dac39920f26eca Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 17:31:14 +0700 Subject: [PATCH 15/17] feat: weight score optimizer --- apps/server/src/models/history.model.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/server/src/models/history.model.ts b/apps/server/src/models/history.model.ts index 53ef2c7..79ee76a 100644 --- a/apps/server/src/models/history.model.ts +++ b/apps/server/src/models/history.model.ts @@ -67,11 +67,14 @@ export class GameHistoryRepository { vote: number, ) { const total = await this.redis.incrBy(`game::${game_id}::${key}`, vote) - this.redis.keys(`game::${game_id}::*`).then((keys) => { + this.redis.keys(`game::${game_id}::*`).then(async (keys) => { + const totalVote = await this.redis.mGet(keys).then((votes) => + votes.reduce((acc, v) => acc + parseInt(v || '0'), 0), + ) || 1 keys.forEach(async (k) => { if (k !== `game::${game_id}::${key}`) { const kTotal = parseInt(await this.redis.get(k) || '0') - const decrease = ((Math.floor((total - kTotal) * vote / 100))) + const decrease = ((Math.floor((total - kTotal) * vote / totalVote))) const remain = kTotal - decrease if (remain > 0) this.redis.decrBy(k, decrease) } From c9a26c8edd8716977b97cebac1262c11670cd609 Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 18:25:26 +0700 Subject: [PATCH 16/17] fix: async order --- apps/server/src/controller/player.controller.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index aaac96c..a72f442 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -47,12 +47,6 @@ export class PlayerController { async onConnection(socket: Socket) { this.logger.info(`New connection: ${socket.id}`) - await this.authenticateSocket(socket) - await this.playerService.getGameState().then((game) => { - socket.emit('state', game.id) - socket.emit('events', game.status) - }) - socket.on('subscribe', (m, cb) => { socket.join('scoreboard'); this.playerService.getGameState().then((game) => { @@ -91,6 +85,12 @@ export class PlayerController { socket.on('disconnect', () => { this.logger.info(`Disconnected: ${socket.id}`) }) + + await this.authenticateSocket(socket) + await this.playerService.getGameState().then((game) => { + socket.emit('state', game.id) + socket.emit('events', game.status) + }) } async getGames(req: Request, res: Response) { From 61b3b243fd2b87db6eaa49934eb1b476f894086c Mon Sep 17 00:00:00 2001 From: Thanapat Chotipun Date: Sat, 30 Mar 2024 18:26:32 +0700 Subject: [PATCH 17/17] perf: order async --- apps/server/src/controller/player.controller.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/server/src/controller/player.controller.ts b/apps/server/src/controller/player.controller.ts index a72f442..29976fd 100644 --- a/apps/server/src/controller/player.controller.ts +++ b/apps/server/src/controller/player.controller.ts @@ -47,6 +47,13 @@ export class PlayerController { async onConnection(socket: Socket) { this.logger.info(`New connection: ${socket.id}`) + this.authenticateSocket(socket).then(() => { + this.playerService.getGameState().then((game) => { + socket.emit('state', game.id) + socket.emit('events', game.status) + }) + }) + socket.on('subscribe', (m, cb) => { socket.join('scoreboard'); this.playerService.getGameState().then((game) => { @@ -85,12 +92,6 @@ export class PlayerController { socket.on('disconnect', () => { this.logger.info(`Disconnected: ${socket.id}`) }) - - await this.authenticateSocket(socket) - await this.playerService.getGameState().then((game) => { - socket.emit('state', game.id) - socket.emit('events', game.status) - }) } async getGames(req: Request, res: Response) {