From 8061d56d77555efa33fc685b1666bfac75ab711e Mon Sep 17 00:00:00 2001 From: teejusb <5017202+teejusb@users.noreply.github.com> Date: Sat, 30 Mar 2024 12:49:51 -0700 Subject: [PATCH 01/32] Changes --- src/events/events.gateway.ts | 81 ++++++++++++++++++----- src/events/utils.ts | 125 ++++++++++++++++++----------------- src/main.ts | 4 ++ src/types/models.types.ts | 1 + 4 files changed, 134 insertions(+), 77 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index fac136c..2d73d26 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -8,11 +8,12 @@ import { import { Server, Socket } from 'socket.io'; import { LOBBYMAN, LobbyInfo, Machine, Spectator } from '../types/models.types'; import { - DisconnectMachine, - DisconnectSpectator, - CanJoinLobby, - GenerateLobbyCode, - GetPlayerCountForLobby, + disconnectMachine, + disconnectSpectator, + canJoinLobby, + generateLobbyCode, + getPlayerCountForLobby, + getLobbyForMachine, } from './utils'; @WebSocketGateway({ @@ -30,11 +31,11 @@ export class EventsGateway { */ handleDisconnect(client: Socket) { if (client.id in LOBBYMAN.machineConnections) { - DisconnectMachine(client.id); + disconnectMachine(client.id); } if (client.id in LOBBYMAN.spectatorConnections) { - DisconnectSpectator(client.id); + disconnectSpectator(client.id); } } @@ -52,17 +53,17 @@ export class EventsGateway { @MessageBody('password') password: string, ): Promise { if (client.id in LOBBYMAN.spectatorConnections) { - DisconnectSpectator(client.id); + disconnectSpectator(client.id); } if (client.id in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - DisconnectMachine(client.id); + disconnectMachine(client.id); } - let code = GenerateLobbyCode(); + let code = generateLobbyCode(); while (code in LOBBYMAN.lobbies) { - code = GenerateLobbyCode(); + code = generateLobbyCode(); } LOBBYMAN.lobbies[code] = { @@ -72,6 +73,7 @@ export class EventsGateway { [client.id]: { ...machine, socket: client, + ready: false, }, }, spectators: {}, @@ -98,20 +100,21 @@ export class EventsGateway { @MessageBody('code') code: string, @MessageBody('password') password: string, ): Promise { - if (CanJoinLobby(code, password)) { + if (canJoinLobby(code, password)) { if (client.id in LOBBYMAN.spectatorConnections) { - DisconnectSpectator(client.id); + disconnectSpectator(client.id); } if (client.id in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - DisconnectMachine(client.id); + disconnectMachine(client.id); } const lobby = LOBBYMAN.lobbies[code]; lobby.machines[client.id] = { ...machine, socket: client, + ready: false, }; LOBBYMAN.machineConnections[client.id] = code; console.log('Machine ' + `${client.id}` + 'joined ' + `${code}`); @@ -127,7 +130,7 @@ export class EventsGateway { */ @SubscribeMessage('leaveLobby') async leaveLobby(@ConnectedSocket() client: Socket): Promise { - return DisconnectMachine(client.id); + return disconnectMachine(client.id); } /** @@ -149,11 +152,11 @@ export class EventsGateway { if (lobby) { if ( !(client.id in LOBBYMAN.machineConnections) && - CanJoinLobby(code, password) + canJoinLobby(code, password) ) { if (client.id in LOBBYMAN.spectatorConnections) { // A spectator can only spectate one lobby at a time. - DisconnectSpectator(client.id); + disconnectSpectator(client.id); } lobby.spectators[client.id] = { @@ -179,11 +182,53 @@ export class EventsGateway { lobbyInfo.push({ code: lobby.code, isPasswordProtected: lobby.password.length !== 0, - playerCount: GetPlayerCountForLobby(lobby), + playerCount: getPlayerCountForLobby(lobby), spectatorCount: Object.keys(lobby.spectators).length, }); } console.log('Found ' + lobbyInfo.length + ' lobbies'); return lobbyInfo; } + + /** Updates the ready state of the machine. + * @returns, true if we successfully readied up, false otherwise. + */ + @SubscribeMessage('readyUp') + async readyUp( + @ConnectedSocket client: Socket, + ): Promise { + const lobby = getLobbyForMachine(client.id); + if (lobby === undefined) { return false; } + + const machine = lobby.machines[client.id]; + if (machine === undefined) { return false; } + + machine.ready = true; + return true; + } + + /** Starts the song of the same lobby as the machine + */ + async startSong(@ConnectedSocket client: Socket): Promise { + const lobby = getLobbyForMachine(client.id); + if (lobby === undefined) { return false; } + + let allReady = true; + for (const machine of Object.values(lobby.machines)) { + if (!machine.ready) { + allReady = false; + break; + } + } + + if (allReady) { + for (const machine of Object.values(lobby.machines)) { + machine.socket.emit('start'); + } + + } + return false; + } + + } diff --git a/src/events/utils.ts b/src/events/utils.ts index da233f3..a5451fd 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -8,24 +8,23 @@ import { LOBBYMAN, Lobby } from '../types/models.types'; * @returns True if the lobby exists and the password is correct, false * otherwise. */ -export function CanJoinLobby(code: string, password: string) { +export function canJoinLobby(code: string, password: string) { // Does the lobby we're trying to join exist? - if (code in LOBBYMAN.lobbies) { - const lobby = LOBBYMAN.lobbies[code]; - // Join either if the lobby is public, or one has provided a valid - // password for a private lobby. - if (!lobby.password || lobby.password === password) { - return true; - } + const lobby = LOBBYMAN.lobbies[code]; + if (lobby === undefined) { return false; } + + // Join either if the lobby is public, or one has provided a valid + // password for a private lobby. + if (!lobby.password || lobby.password === password) { + return true; } - return false; } /** * Generates a random lobby code. * @returns A random lobby code of 4 uppercase characters. */ -export function GenerateLobbyCode(): string { +export function generateLobbyCode(): string { const lobbyCodeLength = 4; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; let result = ''; @@ -40,7 +39,7 @@ export function GenerateLobbyCode(): string { * @param lobby, The lobby to get the player count for. * @returns The number of players in the lobby. */ -export function GetPlayerCountForLobby(lobby: Lobby): number { +export function getPlayerCountForLobby(lobby: Lobby): number { let playerCount = 0; for (const machine of Object.values(lobby.machines)) { if (machine.player1 !== undefined) { @@ -59,42 +58,41 @@ export function GetPlayerCountForLobby(lobby: Lobby): number { * @param socketId, The socket ID of the machine to disconnect. * @returns True if the machine left the lobby, false otherwise. */ -export function DisconnectMachine(socketId: SocketId): boolean { +export function disconnectMachine(socketId: SocketId): boolean { const code = LOBBYMAN.machineConnections[socketId]; - if (code) { - const lobby = LOBBYMAN.lobbies[code]; - if (lobby) { - const machine = lobby.machines[socketId]; - if (machine) { - if (machine.socket) { - if (machine.socket.id in LOBBYMAN.machineConnections) { - delete LOBBYMAN.machineConnections[machine.socket.id]; - } - - machine.socket.leave(code); - // Don't disconnect here, as we may be re-using the connection. - // In the case of `leaveLobby`, the client can manually disconnect. - } - delete lobby.machines[socketId]; - delete LOBBYMAN.machineConnections[socketId]; - - if (GetPlayerCountForLobby(lobby) === 0) { - for (const spectator of Object.values(lobby.spectators)) { - if (spectator.socket) { - spectator.socket.leave(code); - // Force a disconnect. If there are no more players in the lobby, - // we should remove the spectators as well. - spectator.socket.disconnect(); - delete LOBBYMAN.spectatorConnections[spectator.socket.id]; - } - } - delete LOBBYMAN.lobbies[code]; - } - return true; + if (code === undefined) { return false; } + + const lobby = LOBBYMAN.lobbies[code]; + if (lobby === undefined) { return false; } + + const machine = lobby.machines[socketId]; + if (machine === undefined) { return false; } + + if (machine.socket) { + if (machine.socket.id in LOBBYMAN.machineConnections) { + delete LOBBYMAN.machineConnections[machine.socket.id]; + } + + machine.socket.leave(code); + // Don't disconnect here, as we may be re-using the connection. + // In the case of `leaveLobby`, the client can manually disconnect. + } + delete lobby.machines[socketId]; + delete LOBBYMAN.machineConnections[socketId]; + + if (getPlayerCountForLobby(lobby) === 0) { + for (const spectator of Object.values(lobby.spectators)) { + if (spectator.socket) { + spectator.socket.leave(code); + // Force a disconnect. If there are no more players in the lobby, + // we should remove the spectators as well. + spectator.socket.disconnect(); + delete LOBBYMAN.spectatorConnections[spectator.socket.id]; } } + delete LOBBYMAN.lobbies[code]; } - return false; + return true; } /** @@ -102,22 +100,31 @@ export function DisconnectMachine(socketId: SocketId): boolean { * @param socketId, The socket ID of the spectator to disconnect. * @returns, True if the spectator left the lobby, false otherwise. */ -export function DisconnectSpectator(socketId: SocketId): boolean { +export function disconnectSpectator(socketId: SocketId): boolean { const code = LOBBYMAN.spectatorConnections[socketId]; - if (code) { - const lobby = LOBBYMAN.lobbies[code]; - if (lobby) { - const spectator = lobby.spectators[socketId]; - if (spectator) { - if (spectator.socket) { - spectator.socket.leave(code); - // Don't disconnect here, as we may be re-using the connection. - } - delete lobby.spectators[socketId]; - delete LOBBYMAN.spectatorConnections[socketId]; - return true; - } - } + if (code === undefined) { return false; } + + const lobby = LOBBYMAN.lobbies[code]; + if (lobby === undefined) { return false; } + + const spectator = lobby.spectators[socketId]; + if (spectator === undefined) { return false; } + + if (spectator.socket) { + spectator.socket.leave(code); + // Don't disconnect here, as we may be re-using the connection. } - return false; + delete lobby.spectators[socketId]; + delete LOBBYMAN.spectatorConnections[socketId]; + return true; } + +/** Gets the lobby for specific connection. + * @returns, lobby if the machine is part of one, or undefined otherwise. + */ +export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { + const code = LOBBYMAN.machineConnections[socketId]; + if (code === undefined) { return undefined }; + + return LOBBYMAN.lobbies[code]; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index c07c8e5..25b1400 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,14 +1,18 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { LOBBYMAN } from './types/models.types'; +import { WsAdapter } from '@nestjs/platform-ws'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.useWebSocketAdapter(new WsAdapter(app)); LOBBYMAN.lobbies = {}; LOBBYMAN.machineConnections = {}; LOBBYMAN.spectatorConnections = {}; await app.listen(3000); + + console.log(`Application is running on: ${await app.getUrl()}`); } bootstrap(); diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 1cddc4a..10854f6 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -58,6 +58,7 @@ export class Machine { player1?: Player; player2?: Player; socket?: Socket; + ready?: boolean; } export class Lobby { From a9dd78bb874a5c5a2e6da713e5cc97a5985f0e06 Mon Sep 17 00:00:00 2001 From: teejusb <5017202+teejusb@users.noreply.github.com> Date: Sat, 30 Mar 2024 17:28:34 -0700 Subject: [PATCH 02/32] Fix issues + update dependencies --- package-lock.json | 846 +++++++++++++++++------------------ package.json | 17 +- src/events/events.gateway.ts | 8 +- src/events/utils.ts | 1 + 4 files changed, 434 insertions(+), 438 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6042904..c1949c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,12 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0", - "@nestjs/platform-express": "^9.0.0", - "@nestjs/platform-socket.io": "^9.4.0", - "@nestjs/websockets": "^9.4.0", + "@nestjs/common": "^10.3.7", + "@nestjs/core": "^10.3.7", + "@nestjs/platform-express": "^10.3.7", + "@nestjs/platform-socket.io": "^10.3.7", + "@nestjs/platform-ws": "^10.3.7", + "@nestjs/websockets": "^10.3.7", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", @@ -22,7 +23,7 @@ "devDependencies": { "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", - "@nestjs/testing": "^9.0.0", + "@nestjs/testing": "^10.3.7", "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/node": "^16.0.0", @@ -186,12 +187,13 @@ "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -237,23 +239,23 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", - "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", + "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", "dev": true, "dependencies": { - "@babel/types": "^7.21.4", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -280,43 +282,43 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -375,30 +377,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -428,14 +430,15 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -513,9 +516,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -687,34 +690,34 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", - "debug": "^4.1.0", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -731,13 +734,13 @@ } }, "node_modules/@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1281,14 +1284,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -1304,9 +1307,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -1329,21 +1332,15 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -1519,12 +1516,12 @@ } }, "node_modules/@nestjs/common": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.4.0.tgz", - "integrity": "sha512-RUcVAQsEF4WPrmzFXEOUfZnPwrLTe1UVlzXTlSyfqfqbdWDPKDGlIPVelBLfc5/+RRUQ0I5iE4+CQvpCmkqldw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz", + "integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==", "dependencies": { "iterare": "1.2.1", - "tslib": "2.5.0", + "tslib": "2.6.2", "uid": "2.0.2" }, "funding": { @@ -1532,16 +1529,12 @@ "url": "https://opencollective.com/nest" }, "peerDependencies": { - "cache-manager": "<=5", "class-transformer": "*", "class-validator": "*", - "reflect-metadata": "^0.1.12", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, "class-transformer": { "optional": true }, @@ -1551,16 +1544,16 @@ } }, "node_modules/@nestjs/core": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.4.0.tgz", - "integrity": "sha512-yTLryCgFD0462wPe4HIzhyTcDgibt8Stfwb5YzcX7Ma0NM4m8uBIpcPG109KBubp8ZmV85e5mw4rl20qLQQVsQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.7.tgz", + "integrity": "sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "3.2.0", - "tslib": "2.5.0", + "tslib": "2.6.2", "uid": "2.0.2" }, "funding": { @@ -1568,11 +1561,11 @@ "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/microservices": "^9.0.0", - "@nestjs/platform-express": "^9.0.0", - "@nestjs/websockets": "^9.0.0", - "reflect-metadata": "^0.1.12", + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -1588,43 +1581,81 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.4.0.tgz", - "integrity": "sha512-PpnfghpNq7mwG43z3+pacHulsabUCBMla4nUikntXT525ORpZSDvh/nLi1HLfE4w5+FcINc8/RBOyYTeRVmiRQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", + "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.2", + "express": "4.19.2", "multer": "1.4.4-lts.1", - "tslib": "2.5.0" + "tslib": "2.6.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0" + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" } }, "node_modules/@nestjs/platform-socket.io": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-9.4.0.tgz", - "integrity": "sha512-pk5uWItnsrFKzvQrFcAmyfcb8cpGgoj4yR4+vbA5H/MLcv+8vGqruQO8riN8jAYGNPN9Y02ihBKbIvQqn92M5g==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", + "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", + "dependencies": { + "socket.io": "4.7.5", + "tslib": "2.6.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/platform-ws": { + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-10.3.7.tgz", + "integrity": "sha512-lOvZ8u5UdL0FgAOdWosDXefVgDikPd4j5el81emkx+H0pFsysfHXSSoSvMLQijdhENqHSl3buRhO5n3M3uia1w==", "dependencies": { - "socket.io": "4.6.1", - "tslib": "2.5.0" + "tslib": "2.6.2", + "ws": "8.16.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/websockets": "^9.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", "rxjs": "^7.1.0" } }, + "node_modules/@nestjs/platform-ws/node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@nestjs/schematics": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-9.1.0.tgz", @@ -1641,22 +1672,22 @@ } }, "node_modules/@nestjs/testing": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.4.0.tgz", - "integrity": "sha512-xZWp363P4otcebg++gSjUcdCfTK0RorORzyFq3aLaSAQOlq8kxfFDRIKzEATR4aOUfqTMMsAA8lhnMJWf35N6A==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.7.tgz", + "integrity": "sha512-PmwZXyoCC/m3F3IFgpgD+SNN6cDPQa/vi3YQxFruvfX3cuHq+P6ZFvBB7hwaKKsLlhA0so42LsMm41oFBkdouw==", "dev": true, "dependencies": { - "tslib": "2.5.0" + "tslib": "2.6.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0", - "@nestjs/microservices": "^9.0.0", - "@nestjs/platform-express": "^9.0.0" + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" }, "peerDependenciesMeta": { "@nestjs/microservices": { @@ -1668,19 +1699,19 @@ } }, "node_modules/@nestjs/websockets": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-9.4.0.tgz", - "integrity": "sha512-RATR9C0cKhXp3mTQAg75iItyUuRhVwU39Xe/kl0XLpvAhWzhnGrn6CxSTRRzBfp3F68DOKvs7/ODDY51f+rdXw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.7.tgz", + "integrity": "sha512-iYdsWiRNPUy0XzPoW44bx2MW1griuraTr5fNhoe2rUSNO0mEW1aeXp4v56KeZDLAss31WbeckC5P3N223Fys5g==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", - "tslib": "2.5.0" + "tslib": "2.6.2" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0", - "@nestjs/platform-socket.io": "^9.0.0", - "reflect-metadata": "^0.1.12", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { @@ -1866,9 +1897,9 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dependencies": { "@types/node": "*" } @@ -3408,9 +3439,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -3696,9 +3727,9 @@ } }, "node_modules/engine.io": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", - "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -3708,11 +3739,11 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/engine.io-client": { @@ -3743,6 +3774,14 @@ "node": ">= 0.6" } }, + "node_modules/engine.io/node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", @@ -3918,9 +3957,9 @@ } }, "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4208,16 +4247,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -4248,29 +4287,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -4289,20 +4305,6 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -5640,9 +5642,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -6527,9 +6529,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -7609,9 +7611,9 @@ } }, "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -7924,9 +7926,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -8115,26 +8117,28 @@ } }, "node_modules/socket.io": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", - "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.4.1", + "engine.io": "~6.5.2", "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" + "socket.io-parser": "~4.2.4" }, "engines": { - "node": ">=10.0.0" + "node": ">=10.2.0" } }, "node_modules/socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", "dependencies": { + "debug": "~4.3.4", "ws": "~8.11.0" } }, @@ -8153,9 +8157,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", - "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -8842,9 +8846,9 @@ } }, "node_modules/tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -9248,9 +9252,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -9511,12 +9515,13 @@ } }, "@babel/code-frame": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", - "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" } }, "@babel/compat-data": { @@ -9549,22 +9554,22 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "@babel/generator": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", - "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.1.tgz", + "integrity": "sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==", "dev": true, "requires": { - "@babel/types": "^7.21.4", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" } }, @@ -9582,36 +9587,36 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-imports": { @@ -9655,24 +9660,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -9693,14 +9698,15 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "dependencies": { "ansi-styles": { @@ -9762,9 +9768,9 @@ } }, "@babel/parser": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", - "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -9885,31 +9891,31 @@ } }, "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" } }, "@babel/traverse": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", - "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.21.4", - "@babel/generator": "^7.21.4", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.4", - "@babel/types": "^7.21.4", - "debug": "^4.1.0", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" }, "dependencies": { @@ -9922,13 +9928,13 @@ } }, "@babel/types": { - "version": "7.21.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", - "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -10352,14 +10358,14 @@ } }, "@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" } }, "@jridgewell/resolve-uri": { @@ -10369,9 +10375,9 @@ "dev": true }, "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true }, "@jridgewell/source-map": { @@ -10391,21 +10397,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.18", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", - "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - }, - "dependencies": { - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - } + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@lukeed/csprng": { @@ -10534,47 +10532,64 @@ } }, "@nestjs/common": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.4.0.tgz", - "integrity": "sha512-RUcVAQsEF4WPrmzFXEOUfZnPwrLTe1UVlzXTlSyfqfqbdWDPKDGlIPVelBLfc5/+RRUQ0I5iE4+CQvpCmkqldw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz", + "integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==", "requires": { "iterare": "1.2.1", - "tslib": "2.5.0", + "tslib": "2.6.2", "uid": "2.0.2" } }, "@nestjs/core": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.4.0.tgz", - "integrity": "sha512-yTLryCgFD0462wPe4HIzhyTcDgibt8Stfwb5YzcX7Ma0NM4m8uBIpcPG109KBubp8ZmV85e5mw4rl20qLQQVsQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.7.tgz", + "integrity": "sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", "path-to-regexp": "3.2.0", - "tslib": "2.5.0", + "tslib": "2.6.2", "uid": "2.0.2" } }, "@nestjs/platform-express": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.4.0.tgz", - "integrity": "sha512-PpnfghpNq7mwG43z3+pacHulsabUCBMla4nUikntXT525ORpZSDvh/nLi1HLfE4w5+FcINc8/RBOyYTeRVmiRQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", + "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", "requires": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.2", + "express": "4.19.2", "multer": "1.4.4-lts.1", - "tslib": "2.5.0" + "tslib": "2.6.2" } }, "@nestjs/platform-socket.io": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-9.4.0.tgz", - "integrity": "sha512-pk5uWItnsrFKzvQrFcAmyfcb8cpGgoj4yR4+vbA5H/MLcv+8vGqruQO8riN8jAYGNPN9Y02ihBKbIvQqn92M5g==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", + "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", "requires": { - "socket.io": "4.6.1", - "tslib": "2.5.0" + "socket.io": "4.7.5", + "tslib": "2.6.2" + } + }, + "@nestjs/platform-ws": { + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-10.3.7.tgz", + "integrity": "sha512-lOvZ8u5UdL0FgAOdWosDXefVgDikPd4j5el81emkx+H0pFsysfHXSSoSvMLQijdhENqHSl3buRhO5n3M3uia1w==", + "requires": { + "tslib": "2.6.2", + "ws": "8.16.0" + }, + "dependencies": { + "ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "requires": {} + } } }, "@nestjs/schematics": { @@ -10590,22 +10605,22 @@ } }, "@nestjs/testing": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.4.0.tgz", - "integrity": "sha512-xZWp363P4otcebg++gSjUcdCfTK0RorORzyFq3aLaSAQOlq8kxfFDRIKzEATR4aOUfqTMMsAA8lhnMJWf35N6A==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.7.tgz", + "integrity": "sha512-PmwZXyoCC/m3F3IFgpgD+SNN6cDPQa/vi3YQxFruvfX3cuHq+P6ZFvBB7hwaKKsLlhA0so42LsMm41oFBkdouw==", "dev": true, "requires": { - "tslib": "2.5.0" + "tslib": "2.6.2" } }, "@nestjs/websockets": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-9.4.0.tgz", - "integrity": "sha512-RATR9C0cKhXp3mTQAg75iItyUuRhVwU39Xe/kl0XLpvAhWzhnGrn6CxSTRRzBfp3F68DOKvs7/ODDY51f+rdXw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.7.tgz", + "integrity": "sha512-iYdsWiRNPUy0XzPoW44bx2MW1griuraTr5fNhoe2rUSNO0mEW1aeXp4v56KeZDLAss31WbeckC5P3N223Fys5g==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", - "tslib": "2.5.0" + "tslib": "2.6.2" } }, "@nodelib/fs.scandir": { @@ -10769,9 +10784,9 @@ "dev": true }, "@types/cors": { - "version": "2.8.13", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", - "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "requires": { "@types/node": "*" } @@ -11915,9 +11930,9 @@ "dev": true }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" }, "cookie-signature": { "version": "1.0.6", @@ -12133,9 +12148,9 @@ } }, "engine.io": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", - "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -12145,7 +12160,7 @@ "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", + "engine.io-parser": "~5.2.1", "ws": "~8.11.0" }, "dependencies": { @@ -12153,6 +12168,11 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + }, + "engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==" } } }, @@ -12349,9 +12369,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -12509,16 +12529,16 @@ } }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -12546,25 +12566,6 @@ "vary": "~1.1.2" }, "dependencies": { - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -12582,17 +12583,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } } } }, @@ -13578,9 +13568,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -14258,9 +14248,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true } } @@ -14986,9 +14976,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "type-fest": { @@ -15263,9 +15253,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -15421,23 +15411,25 @@ } }, "socket.io": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", - "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", + "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.4.1", + "engine.io": "~6.5.2", "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.1" + "socket.io-parser": "~4.2.4" } }, "socket.io-adapter": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", - "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", "requires": { + "debug": "~4.3.4", "ws": "~8.11.0" } }, @@ -15453,9 +15445,9 @@ } }, "socket.io-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", - "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -15946,9 +15938,9 @@ } }, "tslib": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", - "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsutils": { "version": "3.21.0", @@ -16243,9 +16235,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true }, "wrap-ansi": { diff --git a/package.json b/package.json index 0e01dac..0eb7c8f 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,12 @@ "posttest": "npm run lint" }, "dependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0", - "@nestjs/platform-express": "^9.0.0", - "@nestjs/platform-socket.io": "^9.4.0", - "@nestjs/websockets": "^9.4.0", + "@nestjs/common": "^10.3.7", + "@nestjs/core": "^10.3.7", + "@nestjs/platform-express": "^10.3.7", + "@nestjs/platform-socket.io": "^10.3.7", + "@nestjs/platform-ws": "^10.3.7", + "@nestjs/websockets": "^10.3.7", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", @@ -40,7 +41,7 @@ "devDependencies": { "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", - "@nestjs/testing": "^9.0.0", + "@nestjs/testing": "^10.3.7", "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/node": "^16.0.0", @@ -50,6 +51,7 @@ "eslint": "^8.0.1", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", + "gts": "^3.1.1", "jest": "28.1.3", "prettier": "^2.3.2", "source-map-support": "^0.5.20", @@ -58,8 +60,7 @@ "ts-loader": "^9.2.3", "ts-node": "^10.0.0", "tsconfig-paths": "4.1.0", - "typescript": "^4.7.4", - "gts": "^3.1.1" + "typescript": "^4.7.4" }, "jest": { "moduleFileExtensions": [ diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 2d73d26..acaef49 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -195,7 +195,7 @@ export class EventsGateway { */ @SubscribeMessage('readyUp') async readyUp( - @ConnectedSocket client: Socket, + @ConnectedSocket() client: Socket, ): Promise { const lobby = getLobbyForMachine(client.id); if (lobby === undefined) { return false; } @@ -209,7 +209,7 @@ export class EventsGateway { /** Starts the song of the same lobby as the machine */ - async startSong(@ConnectedSocket client: Socket): Promise { + async startSong(@ConnectedSocket() client: Socket): Promise { const lobby = getLobbyForMachine(client.id); if (lobby === undefined) { return false; } @@ -223,7 +223,9 @@ export class EventsGateway { if (allReady) { for (const machine of Object.values(lobby.machines)) { - machine.socket.emit('start'); + if (machine.socket !== undefined) { + machine.socket.emit('start'); + } } } diff --git a/src/events/utils.ts b/src/events/utils.ts index a5451fd..62fba33 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -18,6 +18,7 @@ export function canJoinLobby(code: string, password: string) { if (!lobby.password || lobby.password === password) { return true; } + return false; } /** From 91dd115add67344732afd0a2a1f6f1ebcf1e90a0 Mon Sep 17 00:00:00 2001 From: teejusb <5017202+teejusb@users.noreply.github.com> Date: Sun, 31 Mar 2024 11:12:06 -0700 Subject: [PATCH 03/32] Emit state --- src/events/events.gateway.ts | 39 ++++++++++++++---------------------- src/events/utils.ts | 19 +++++++++++++++++- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index acaef49..18e9c6d 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -14,6 +14,7 @@ import { generateLobbyCode, getPlayerCountForLobby, getLobbyForMachine, + getLobbyState, } from './utils'; @WebSocketGateway({ @@ -191,27 +192,23 @@ export class EventsGateway { } /** Updates the ready state of the machine. + * @param client, The socket that connected. * @returns, true if we successfully readied up, false otherwise. - */ + */ @SubscribeMessage('readyUp') - async readyUp( - @ConnectedSocket() client: Socket, - ): Promise { + async readyUp(@ConnectedSocket() client: Socket): Promise { const lobby = getLobbyForMachine(client.id); - if (lobby === undefined) { return false; } + if (lobby === undefined) { + return false; + } const machine = lobby.machines[client.id]; - if (machine === undefined) { return false; } - + if (machine === undefined) { + return false; + } + machine.ready = true; - return true; - } - - /** Starts the song of the same lobby as the machine - */ - async startSong(@ConnectedSocket() client: Socket): Promise { - const lobby = getLobbyForMachine(client.id); - if (lobby === undefined) { return false; } + client.nsp.to(lobby.code).emit('state', getLobbyState(client.id)) let allReady = true; for (const machine of Object.values(lobby.machines)) { @@ -222,15 +219,9 @@ export class EventsGateway { } if (allReady) { - for (const machine of Object.values(lobby.machines)) { - if (machine.socket !== undefined) { - machine.socket.emit('start'); - } - } - + client.nsp.to(lobby.code).emit('startSong'); } - return false; + + return true; } - - } diff --git a/src/events/utils.ts b/src/events/utils.ts index 62fba33..2b6ebb9 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -1,5 +1,5 @@ import { SocketId } from 'socket.io-adapter'; -import { LOBBYMAN, Lobby } from '../types/models.types'; +import { LOBBYMAN, Lobby, Machine } from '../types/models.types'; /** * Determines if the correct credentials are provided to join a lobby. @@ -128,4 +128,21 @@ export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { if (code === undefined) { return undefined }; return LOBBYMAN.lobbies[code]; +} + +export function getLobbyState(socketId: SocketId): Machine[] | null { + const lobby = getLobbyForMachine(socketId); + if (lobby === undefined) { + return null; + } + + const machineState: Machine[] = []; + for (const machine of Object.values(lobby.machines)) { + const machineCopy = { ...machine }; + // Remove the socket from the machine state since we shouldn't be sending + // it back to the client. + machineCopy.socket = undefined; + machineState.push(machineCopy); + } + return machineState; } \ No newline at end of file From 47f49262cf1ac58d65140dab084936d14789827e Mon Sep 17 00:00:00 2001 From: teejusb <5017202+teejusb@users.noreply.github.com> Date: Sat, 20 Jul 2024 00:03:34 -0600 Subject: [PATCH 04/32] Fix linting --- src/events/events.gateway.ts | 3 +-- src/events/utils.ts | 34 +++++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 18e9c6d..3f74473 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -208,7 +208,7 @@ export class EventsGateway { } machine.ready = true; - client.nsp.to(lobby.code).emit('state', getLobbyState(client.id)) + client.nsp.to(lobby.code).emit('state', getLobbyState(client.id)); let allReady = true; for (const machine of Object.values(lobby.machines)) { @@ -221,7 +221,6 @@ export class EventsGateway { if (allReady) { client.nsp.to(lobby.code).emit('startSong'); } - return true; } } diff --git a/src/events/utils.ts b/src/events/utils.ts index 2b6ebb9..52889b9 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -11,7 +11,9 @@ import { LOBBYMAN, Lobby, Machine } from '../types/models.types'; export function canJoinLobby(code: string, password: string) { // Does the lobby we're trying to join exist? const lobby = LOBBYMAN.lobbies[code]; - if (lobby === undefined) { return false; } + if (lobby === undefined) { + return false; + } // Join either if the lobby is public, or one has provided a valid // password for a private lobby. @@ -61,13 +63,19 @@ export function getPlayerCountForLobby(lobby: Lobby): number { */ export function disconnectMachine(socketId: SocketId): boolean { const code = LOBBYMAN.machineConnections[socketId]; - if (code === undefined) { return false; } + if (code === undefined) { + return false; + } const lobby = LOBBYMAN.lobbies[code]; - if (lobby === undefined) { return false; } + if (lobby === undefined) { + return false; + } const machine = lobby.machines[socketId]; - if (machine === undefined) { return false; } + if (machine === undefined) { + return false; + } if (machine.socket) { if (machine.socket.id in LOBBYMAN.machineConnections) { @@ -103,13 +111,19 @@ export function disconnectMachine(socketId: SocketId): boolean { */ export function disconnectSpectator(socketId: SocketId): boolean { const code = LOBBYMAN.spectatorConnections[socketId]; - if (code === undefined) { return false; } + if (code === undefined) { + return false; + } const lobby = LOBBYMAN.lobbies[code]; - if (lobby === undefined) { return false; } + if (lobby === undefined) { + return false; + } const spectator = lobby.spectators[socketId]; - if (spectator === undefined) { return false; } + if (spectator === undefined) { + return false; + } if (spectator.socket) { spectator.socket.leave(code); @@ -125,7 +139,9 @@ export function disconnectSpectator(socketId: SocketId): boolean { */ export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { const code = LOBBYMAN.machineConnections[socketId]; - if (code === undefined) { return undefined }; + if (code === undefined) { + return undefined; + } return LOBBYMAN.lobbies[code]; } @@ -145,4 +161,4 @@ export function getLobbyState(socketId: SocketId): Machine[] | null { machineState.push(machineCopy); } return machineState; -} \ No newline at end of file +} From b843ee6ffe651f8bb1c4e5467d182c4e0f908084 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Tue, 29 Oct 2024 09:31:34 -0700 Subject: [PATCH 05/32] Add internal clientId, basic message handling --- package-lock.json | 142 +++++++++++++++++++++++++++++++++-- package.json | 5 +- src/events/events.gateway.ts | 51 +++++++++++-- 3 files changed, 182 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1949c8..225c9e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,9 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", - "socket.io-client": "^4.6.1" + "socket.io-client": "^4.6.1", + "uuid": "^11.0.2", + "ws": "^8.18.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", @@ -28,6 +30,7 @@ "@types/jest": "28.1.8", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", @@ -2090,6 +2093,16 @@ "@types/superagent": "*" } }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -3758,6 +3771,27 @@ "xmlhttprequest-ssl": "~2.0.0" } }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/engine.io-parser": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", @@ -3782,6 +3816,27 @@ "node": ">=10.0.0" } }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", @@ -8142,6 +8197,27 @@ "ws": "~8.11.0" } }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-client": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", @@ -9019,6 +9095,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", + "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -9296,15 +9385,16 @@ } }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -10977,6 +11067,15 @@ "@types/superagent": "*" } }, + "@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -12173,6 +12272,12 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==" + }, + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} } } }, @@ -12186,6 +12291,14 @@ "engine.io-parser": "~5.0.3", "ws": "~8.11.0", "xmlhttprequest-ssl": "~2.0.0" + }, + "dependencies": { + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} + } } }, "engine.io-parser": { @@ -15431,6 +15544,14 @@ "requires": { "debug": "~4.3.4", "ws": "~8.11.0" + }, + "dependencies": { + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "requires": {} + } } }, "socket.io-client": { @@ -16057,6 +16178,11 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, + "uuid": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", + "integrity": "sha512-14FfcOJmqdjbBPdDjFQyk/SdT4NySW4eM0zcG+HqbHP5jzuH56xO3J1DGhgs/cEMCfwYi3HQI1gnTO62iaG+tQ==" + }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -16267,9 +16393,9 @@ } }, "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "requires": {} }, "xmlhttprequest-ssl": { diff --git a/package.json b/package.json index 0eb7c8f..9ac627d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,9 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", - "socket.io-client": "^4.6.1" + "socket.io-client": "^4.6.1", + "uuid": "^11.0.2", + "ws": "^8.18.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", @@ -46,6 +48,7 @@ "@types/jest": "28.1.8", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 3f74473..95332a4 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -1,11 +1,15 @@ import { ConnectedSocket, MessageBody, + OnGatewayConnection, + OnGatewayDisconnect, SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; +import { WebSocket } from 'ws'; +import { v4 as uuidv4 } from 'uuid'; import { LOBBYMAN, LobbyInfo, Machine, Spectator } from '../types/models.types'; import { disconnectMachine, @@ -22,21 +26,54 @@ import { origin: '*', }, }) -export class EventsGateway { +export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; + afterInit(server: Server) { + console.log('WebSocket server initialized'); + } + + private clients: Map = new Map(); + + handleConnection(client: WebSocket, ...args: any[]) { + const clientId = uuidv4(); + console.log('Client connected: ', clientId); + this.clients.set(clientId, client); + client.on('message', (message: Buffer) => { + console.log('Handle the message', message); + this.handleMessage(clientId, JSON.parse(message.toString())); + }); + client.on('close', () => this.handleDisconnect(clientId)); + } + + /** Handles any incoming message + * @param client the client sending the message + * @param payload the message payload + */ + handleMessage(clientId: string, payload: { message: string }): string { + console.log('Message received:', payload.message); + this.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify({ from: clientId, ...payload })); + } + }); + return 'message received ' + JSON.stringify(payload); + } + /** * Cleans up the lobby manager when a client disconnects. - * @param client, The socket that disconnected. + * @param clientId, The client id that disconnected. */ - handleDisconnect(client: Socket) { - if (client.id in LOBBYMAN.machineConnections) { - disconnectMachine(client.id); + handleDisconnect(clientId: string) { + this.clients.delete(clientId); + + if (clientId in LOBBYMAN.machineConnections) { + disconnectMachine(clientId); } - if (client.id in LOBBYMAN.spectatorConnections) { - disconnectSpectator(client.id); + if (clientId in LOBBYMAN.spectatorConnections) { + disconnectSpectator(clientId); } } From 9ce0907c753ef66b0a001b203d7e67529a4d11b4 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Wed, 30 Oct 2024 11:27:26 -0700 Subject: [PATCH 06/32] Handle createLobby message --- src/events/events.gateway.ts | 109 +++++++++++++++++++++-------------- src/events/events.types.ts | 19 ++++++ src/events/utils.ts | 37 ++++++------ src/types/models.types.ts | 7 +-- 4 files changed, 106 insertions(+), 66 deletions(-) create mode 100644 src/events/events.types.ts diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 95332a4..4dd0121 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -10,7 +10,13 @@ import { import { Server, Socket } from 'socket.io'; import { WebSocket } from 'ws'; import { v4 as uuidv4 } from 'uuid'; -import { LOBBYMAN, LobbyInfo, Machine, Spectator } from '../types/models.types'; +import { + LOBBYMAN, + LobbyInfo, + Machine, + SocketId, + Spectator, +} from '../types/models.types'; import { disconnectMachine, disconnectSpectator, @@ -20,6 +26,12 @@ import { getLobbyForMachine, getLobbyState, } from './utils'; +import { + CreateLobbyPayload, + LobbyCreatedPayload, + Message, + MessageType, +} from './events.types'; @WebSocketGateway({ cors: { @@ -30,35 +42,42 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; - afterInit(server: Server) { - console.log('WebSocket server initialized'); - } - - private clients: Map = new Map(); + private rooms: Map> = new Map(); + private clients: Map = new Map(); + private handlers: Map< + MessageType, + (socketId: SocketId, payload: any) => Promise + > = new Map(); - handleConnection(client: WebSocket, ...args: any[]) { - const clientId = uuidv4(); - console.log('Client connected: ', clientId); - this.clients.set(clientId, client); - client.on('message', (message: Buffer) => { - console.log('Handle the message', message); - this.handleMessage(clientId, JSON.parse(message.toString())); - }); - client.on('close', () => this.handleDisconnect(clientId)); + afterInit(server: Server) { + this.handlers.set('createLobby', this.createLobby); } - /** Handles any incoming message - * @param client the client sending the message - * @param payload the message payload - */ - handleMessage(clientId: string, payload: { message: string }): string { - console.log('Message received:', payload.message); - this.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify({ from: clientId, ...payload })); + handleConnection(socket: WebSocket, ...args: any[]) { + const socketId = uuidv4(); + console.log('Client connected: ', socketId); + this.clients.set(socketId, socket); + socket.on('message', async (messageBuffer: Buffer) => { + const message: Message = JSON.parse(messageBuffer.toString()); + if (!message.type || !message.payload) { + throw new Error('Message requires a type and a payload'); + } + if (!this.handlers.has(message.type)) { + throw new Error(`No handler for message type "${message.type}"`); } + const handler = this.handlers.get(message.type); + if (!handler) { + throw new Error('Missing handler'); // Should not happen, but makes TS happy + } + const response = await handler(socketId, message.payload); + + this.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(response)); + } + }); }); - return 'message received ' + JSON.stringify(payload); + socket.on('close', () => this.handleDisconnect(socketId)); } /** @@ -79,24 +98,22 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { /** * Creates a new lobby and connects a machine to it. - * @param client, The socket that connected. + * @param socketId, The socket that connected. * @param machine, The machine that connected. * @param password, The password for the lobby (empty implies public lobby). * @returns, The code for the newly created lobby. */ - @SubscribeMessage('createLobby') async createLobby( - @ConnectedSocket() client: Socket, - @MessageBody('machine') machine: Machine, - @MessageBody('password') password: string, - ): Promise { - if (client.id in LOBBYMAN.spectatorConnections) { - disconnectSpectator(client.id); + socketId: string, + { machine, password }: CreateLobbyPayload, + ): Promise { + if (socketId in LOBBYMAN.spectatorConnections) { + disconnectSpectator(socketId); } - if (client.id in LOBBYMAN.machineConnections) { + if (socketId in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - disconnectMachine(client.id); + disconnectMachine(socketId); } let code = generateLobbyCode(); @@ -105,22 +122,26 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } LOBBYMAN.lobbies[code] = { - code: code, - password: password ? password : '', + code, + password: password || '', machines: { - [client.id]: { + [socketId]: { ...machine, - socket: client, + socketId, ready: false, }, }, spectators: {}, }; - client.join(code); - LOBBYMAN.machineConnections[client.id] = code; + // TODO: this context is weird from the handler, maybe just move rooms to LOBBYMAN + // if (!this.rooms.has(code)) { + // this.rooms.set(code, []); + // } + // this.rooms.get(code)?.push(socketId); + LOBBYMAN.machineConnections[socketId] = code; console.log('Created lobby ' + code); - return code; + return { type: 'lobbyCreated', payload: { code } as LobbyCreatedPayload }; } /** @@ -151,7 +172,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const lobby = LOBBYMAN.lobbies[code]; lobby.machines[client.id] = { ...machine, - socket: client, + socketId: client.id, ready: false, }; LOBBYMAN.machineConnections[client.id] = code; @@ -199,7 +220,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { lobby.spectators[client.id] = { ...spectator, - socket: client, + socketId: client.id, }; client.join(code); LOBBYMAN.spectatorConnections[client.id] = code; diff --git a/src/events/events.types.ts b/src/events/events.types.ts new file mode 100644 index 0000000..1dc9bc1 --- /dev/null +++ b/src/events/events.types.ts @@ -0,0 +1,19 @@ +import { Machine } from '../types/models.types'; + +export type MessageType = 'createLobby' | 'lobbyCreated'; + +export type MessagePayload = CreateLobbyPayload | LobbyCreatedPayload; + +export interface Message { + type: MessageType; + payload: MessagePayload; +} + +export interface CreateLobbyPayload { + machine: Machine; + password: string; +} + +export interface LobbyCreatedPayload { + code: Machine; +} diff --git a/src/events/utils.ts b/src/events/utils.ts index 52889b9..b864275 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -77,12 +77,13 @@ export function disconnectMachine(socketId: SocketId): boolean { return false; } - if (machine.socket) { - if (machine.socket.id in LOBBYMAN.machineConnections) { - delete LOBBYMAN.machineConnections[machine.socket.id]; + if (machine.socketId) { + if (machine.socketId in LOBBYMAN.machineConnections) { + delete LOBBYMAN.machineConnections[machine.socketId]; } - machine.socket.leave(code); + // TODO + // machine.socket.leave(code); // Don't disconnect here, as we may be re-using the connection. // In the case of `leaveLobby`, the client can manually disconnect. } @@ -91,12 +92,14 @@ export function disconnectMachine(socketId: SocketId): boolean { if (getPlayerCountForLobby(lobby) === 0) { for (const spectator of Object.values(lobby.spectators)) { - if (spectator.socket) { - spectator.socket.leave(code); + if (spectator.socketId) { + // TODO + // spectator.socket.leave(code); // Force a disconnect. If there are no more players in the lobby, // we should remove the spectators as well. - spectator.socket.disconnect(); - delete LOBBYMAN.spectatorConnections[spectator.socket.id]; + // TODO + // spectator.socket.disconnect(); + delete LOBBYMAN.spectatorConnections[spectator.socketId]; } } delete LOBBYMAN.lobbies[code]; @@ -125,8 +128,9 @@ export function disconnectSpectator(socketId: SocketId): boolean { return false; } - if (spectator.socket) { - spectator.socket.leave(code); + if (spectator.socketId) { + // TODO + // spectator.socket.leave(code); // Don't disconnect here, as we may be re-using the connection. } delete lobby.spectators[socketId]; @@ -152,13 +156,10 @@ export function getLobbyState(socketId: SocketId): Machine[] | null { return null; } - const machineState: Machine[] = []; - for (const machine of Object.values(lobby.machines)) { - const machineCopy = { ...machine }; - // Remove the socket from the machine state since we shouldn't be sending - // it back to the client. - machineCopy.socket = undefined; - machineState.push(machineCopy); - } + // Send back the machine state with the socket ids omitted + const machineState = Object.entries(lobby.machines).map((m) => ({ + socketId, + ...m, + })); return machineState; } diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 10854f6..626fa28 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -1,5 +1,4 @@ -import { Socket } from 'socket.io'; -import { SocketId } from 'socket.io-adapter'; +export type SocketId = string; export class Judgments { fantasticPlus: number; @@ -20,7 +19,7 @@ export class Judgments { export class Spectator { profileName: string; - socket?: Socket; + socketId?: SocketId; } export class SongInfo { @@ -57,7 +56,7 @@ export class Player { export class Machine { player1?: Player; player2?: Player; - socket?: Socket; + socketId?: SocketId; ready?: boolean; } From 65199cd8df9a862bab9815edd4d48d828ec5a36b Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Wed, 30 Oct 2024 20:47:28 -0700 Subject: [PATCH 07/32] Manage connections, room joining/leaving --- src/events/events.gateway.ts | 36 +++++-------- src/events/events.types.ts | 11 +++- src/events/utils.ts | 20 +++---- src/types/models.types.ts | 100 +++++++++++++++++++++++++++++++++-- 4 files changed, 129 insertions(+), 38 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 4dd0121..4daf865 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -9,11 +9,12 @@ import { } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { WebSocket } from 'ws'; -import { v4 as uuidv4 } from 'uuid'; import { + CLIENTS, LOBBYMAN, LobbyInfo, Machine, + ROOMMAN, SocketId, Spectator, } from '../types/models.types'; @@ -42,8 +43,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; - private rooms: Map> = new Map(); - private clients: Map = new Map(); private handlers: Map< MessageType, (socketId: SocketId, payload: any) => Promise @@ -54,9 +53,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } handleConnection(socket: WebSocket, ...args: any[]) { - const socketId = uuidv4(); - console.log('Client connected: ', socketId); - this.clients.set(socketId, socket); + const socketId = CLIENTS.connect(socket); + socket.on('message', async (messageBuffer: Buffer) => { const message: Message = JSON.parse(messageBuffer.toString()); if (!message.type || !message.payload) { @@ -71,28 +69,24 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } const response = await handler(socketId, message.payload); - this.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(response)); - } - }); + CLIENTS.send(response); }); socket.on('close', () => this.handleDisconnect(socketId)); } /** * Cleans up the lobby manager when a client disconnects. - * @param clientId, The client id that disconnected. + * @param socketId, The socket id that disconnected. */ - handleDisconnect(clientId: string) { - this.clients.delete(clientId); + handleDisconnect(socketId: string) { + CLIENTS.disconnect(socketId); - if (clientId in LOBBYMAN.machineConnections) { - disconnectMachine(clientId); + if (socketId in LOBBYMAN.machineConnections) { + disconnectMachine(socketId); } - if (clientId in LOBBYMAN.spectatorConnections) { - disconnectSpectator(clientId); + if (socketId in LOBBYMAN.spectatorConnections) { + disconnectSpectator(socketId); } } @@ -133,11 +127,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { }, spectators: {}, }; - // TODO: this context is weird from the handler, maybe just move rooms to LOBBYMAN - // if (!this.rooms.has(code)) { - // this.rooms.set(code, []); - // } - // this.rooms.get(code)?.push(socketId); + ROOMMAN.join(socketId, code); LOBBYMAN.machineConnections[socketId] = code; console.log('Created lobby ' + code); diff --git a/src/events/events.types.ts b/src/events/events.types.ts index 1dc9bc1..36b751a 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -1,8 +1,11 @@ import { Machine } from '../types/models.types'; -export type MessageType = 'createLobby' | 'lobbyCreated'; +export type MessageType = 'createLobby' | 'lobbyCreated' | 'clientDisconnected'; -export type MessagePayload = CreateLobbyPayload | LobbyCreatedPayload; +export type MessagePayload = + | CreateLobbyPayload + | LobbyCreatedPayload + | ClientDisconnectedPayload; export interface Message { type: MessageType; @@ -17,3 +20,7 @@ export interface CreateLobbyPayload { export interface LobbyCreatedPayload { code: Machine; } + +export interface ClientDisconnectedPayload { + reason: string; +} diff --git a/src/events/utils.ts b/src/events/utils.ts index b864275..d61caed 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -1,5 +1,11 @@ import { SocketId } from 'socket.io-adapter'; -import { LOBBYMAN, Lobby, Machine } from '../types/models.types'; +import { + CLIENTS, + LOBBYMAN, + Lobby, + Machine, + ROOMMAN, +} from '../types/models.types'; /** * Determines if the correct credentials are provided to join a lobby. @@ -82,8 +88,7 @@ export function disconnectMachine(socketId: SocketId): boolean { delete LOBBYMAN.machineConnections[machine.socketId]; } - // TODO - // machine.socket.leave(code); + ROOMMAN.leave(machine.socketId, code); // Don't disconnect here, as we may be re-using the connection. // In the case of `leaveLobby`, the client can manually disconnect. } @@ -93,12 +98,10 @@ export function disconnectMachine(socketId: SocketId): boolean { if (getPlayerCountForLobby(lobby) === 0) { for (const spectator of Object.values(lobby.spectators)) { if (spectator.socketId) { - // TODO - // spectator.socket.leave(code); + ROOMMAN.leave(spectator.socketId, code); // Force a disconnect. If there are no more players in the lobby, // we should remove the spectators as well. - // TODO - // spectator.socket.disconnect(); + CLIENTS.disconnect(spectator.socketId); delete LOBBYMAN.spectatorConnections[spectator.socketId]; } } @@ -129,8 +132,7 @@ export function disconnectSpectator(socketId: SocketId): boolean { } if (spectator.socketId) { - // TODO - // spectator.socket.leave(code); + ROOMMAN.leave(spectator.socketId, code); // Don't disconnect here, as we may be re-using the connection. } delete lobby.spectators[socketId]; diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 626fa28..9820e78 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -1,5 +1,11 @@ +import WebSocket = require('ws'); +import { v4 as uuidv4 } from 'uuid'; +import { Message } from '../events/events.types'; + export type SocketId = string; +export type LobbyCode = string; + export class Judgments { fantasticPlus: number; fantastics: number; @@ -61,7 +67,7 @@ export class Machine { } export class Lobby { - code: string; + code: LobbyCode; // Empty string here is equivalent to "no password". We could use undefined // but we can consider them the same. password: string; @@ -72,7 +78,7 @@ export class Lobby { } export class LobbyInfo { - code: string; + code: LobbyCode; isPasswordProtected: boolean; playerCount: number; spectatorCount: number; @@ -83,8 +89,94 @@ export class LOBBYMAN { static lobbies: Record; // Mapping from socketId to the lobby code of the lobby it's connected to. - static machineConnections: Record; + static machineConnections: Record; // Mapping from socketId to the lobby code for the spectators. - static spectatorConnections: Record; + static spectatorConnections: Record; +} + +export class ROOMMAN { + // Mapping of lobby ids (rooms) to the socketIds in that room + private static rooms: Map> = new Map(); + + static join(socketId: SocketId, code: LobbyCode) { + if (!this.rooms.has(code)) { + this.rooms.set(code, []); + } + const sockets = this.rooms.get(code)!; + if (sockets.includes(socketId)) { + console.warn(`Socket ${socketId} is already in room ${code}`); + return; + } + console.info(`Socket ${socketId} is joining room ${code}`); + sockets.push(socketId); + console.log(this.rooms); + } + + static leave(socketId: SocketId, code: LobbyCode) { + if (!this.rooms.has(code)) { + console.warn(`No room for code ${code}`); + return; + } + const sockets = this.rooms.get(code)!; + if (!sockets.includes(socketId)) { + console.warn(`Socket ${socketId} is not in room ${code}`); + return; + } + console.info(`Socket ${socketId} is leaving room ${code}`); + this.rooms.set( + code, + sockets.filter((s) => s !== socketId), + ); + console.log(this.rooms); + } +} + +export class CLIENTS { + private static clients: Map = new Map(); + + static send(response: Message) { + this.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(response)); + } + }); + } + + static disconnect(socketId: SocketId, reason?: string) { + if (!this.clients.has(socketId)) { + console.warn(`Client ${socketId} not connected`); + return; + } + + const message: Message = { + type: 'clientDisconnected', + payload: { reason: reason || 'Just because' }, + }; + + const client = this.clients.get(socketId); + if (!client) return; + + if (client.readyState === WebSocket.OPEN) { + this.clients.get(socketId)?.close(1000, JSON.stringify(message)); + } + this.clients.delete(socketId); + } + + static connect(socket: WebSocket): string { + // Assert we're not already connected + const entry = Object.entries(this.clients).find( + ([, value]) => socket === value, + ); + if (entry) { + console.warn(`Socket ${entry[0]} is already connected`); + return entry[0]; + } + + // Generate an id for the entry, set and return it + const socketId = uuidv4(); + this.clients.set(socketId, socket); + console.log('Socket connected: ', socketId); + return socketId; + } } From 3f53a970132367fb3f9d84d81e115c3701bd4c7b Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Wed, 30 Oct 2024 22:17:59 -0700 Subject: [PATCH 08/32] Convert more message types --- src/events/events.gateway.ts | 163 ++++++++++++++++++----------------- src/events/events.types.ts | 91 ++++++++++++++++++- src/events/utils.ts | 8 +- src/types/models.types.ts | 16 +++- 4 files changed, 189 insertions(+), 89 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 4daf865..70f27ab 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -1,22 +1,17 @@ import { - ConnectedSocket, - MessageBody, OnGatewayConnection, OnGatewayDisconnect, - SubscribeMessage, WebSocketGateway, WebSocketServer, } from '@nestjs/websockets'; -import { Server, Socket } from 'socket.io'; +import { Server } from 'socket.io'; import { WebSocket } from 'ws'; import { CLIENTS, LOBBYMAN, LobbyInfo, - Machine, ROOMMAN, SocketId, - Spectator, } from '../types/models.types'; import { disconnectMachine, @@ -29,9 +24,11 @@ import { } from './utils'; import { CreateLobbyPayload, + JoinLobbyPayload, LobbyCreatedPayload, Message, MessageType, + SpectateLobbyPayload, } from './events.types'; @WebSocketGateway({ @@ -50,6 +47,11 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { afterInit(server: Server) { this.handlers.set('createLobby', this.createLobby); + this.handlers.set('joinLobby', this.joinLobby); + this.handlers.set('leaveLobby', this.leaveLobby); + this.handlers.set('spectateLobby', this.spectateLobby); + this.handlers.set('searchLobby', this.searchLobby); + this.handlers.set('readyUp', this.readyUp); } handleConnection(socket: WebSocket, ...args: any[]) { @@ -142,34 +144,33 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * @param password, The password for the lobby. * @returns True if the machine joined the lobby, false otherwise. */ - @SubscribeMessage('joinLobby') async joinLobby( - @ConnectedSocket() client: Socket, - @MessageBody('machine') machine: Machine, - @MessageBody('code') code: string, - @MessageBody('password') password: string, - ): Promise { - if (canJoinLobby(code, password)) { - if (client.id in LOBBYMAN.spectatorConnections) { - disconnectSpectator(client.id); - } + socketId: SocketId, + { machine, code, password }: JoinLobbyPayload, + ): Promise { + if (!canJoinLobby(code, password)) { + return { type: 'lobbyJoined', payload: { joined: false } }; + } - if (client.id in LOBBYMAN.machineConnections) { - // A machine can only join one lobby at a time. - disconnectMachine(client.id); - } + if (socketId in LOBBYMAN.spectatorConnections) { + disconnectSpectator(socketId); + } - const lobby = LOBBYMAN.lobbies[code]; - lobby.machines[client.id] = { - ...machine, - socketId: client.id, - ready: false, - }; - LOBBYMAN.machineConnections[client.id] = code; - console.log('Machine ' + `${client.id}` + 'joined ' + `${code}`); - return true; + if (socketId in LOBBYMAN.machineConnections) { + // A machine can only join one lobby at a time. + disconnectMachine(socketId); } - return false; + + const lobby = LOBBYMAN.lobbies[code]; + lobby.machines[socketId] = { + ...machine, + socketId, + ready: false, + }; + LOBBYMAN.machineConnections[socketId] = code; + console.log('Machine ' + `${socketId}` + 'joined ' + `${code}`); + + return { type: 'lobbyJoined', payload: { joined: true } }; } /** @@ -177,86 +178,90 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * @param client, The socket connection of the machine to disconnect. * @returns, True if the machine was disconnected successfully. */ - @SubscribeMessage('leaveLobby') - async leaveLobby(@ConnectedSocket() client: Socket): Promise { - return disconnectMachine(client.id); + async leaveLobby(socketId: SocketId): Promise { + const left = disconnectMachine(socketId); + return { type: 'lobbyLeft', payload: { left } }; } /** * Connects a spectator to an existing lobby. - * @param client, The socket that connected. + * @param socketId, The socket that connected. * @param spectator, The spectator to connect to a lobby. * @param code, The code for the lobby to spectate. * @param password, The password for the lobby. * @returns, The number of spectators in the lobby. */ - @SubscribeMessage('spectateLobby') async spectateLobby( - @ConnectedSocket() client: Socket, - @MessageBody('spectator') spectator: Spectator, - @MessageBody('code') code: string, - @MessageBody('password') password: string, - ): Promise { + socketId: SocketId, + { spectator, code, password }: SpectateLobbyPayload, + ): Promise { const lobby = LOBBYMAN.lobbies[code]; - if (lobby) { - if ( - !(client.id in LOBBYMAN.machineConnections) && - canJoinLobby(code, password) - ) { - if (client.id in LOBBYMAN.spectatorConnections) { - // A spectator can only spectate one lobby at a time. - disconnectSpectator(client.id); - } - lobby.spectators[client.id] = { - ...spectator, - socketId: client.id, - }; - client.join(code); - LOBBYMAN.spectatorConnections[client.id] = code; + if (!lobby) { + return { type: 'lobbySpectated', payload: { spectators: 0 } }; + } + + if ( + !(socketId in LOBBYMAN.machineConnections) && + canJoinLobby(code, password) + ) { + if (socketId in LOBBYMAN.spectatorConnections) { + // A spectator can only spectate one lobby at a time. + disconnectSpectator(socketId); } - return Object.keys(lobby.spectators).length; + + lobby.spectators[socketId] = { + ...spectator, + socketId, + }; + ROOMMAN.join(socketId, code); + LOBBYMAN.spectatorConnections[socketId] = code; } - return 0; + return { + type: 'lobbySpectated', + payload: { spectators: Object.keys(lobby.spectators).length }, + }; } /** * Searches for all active lobbies. * @returns, The list of lobbies that are currently active. */ - @SubscribeMessage('searchLobby') - async searchLobby(): Promise { - const lobbyInfo: LobbyInfo[] = []; - for (const lobby of Object.values(LOBBYMAN.lobbies)) { - lobbyInfo.push({ - code: lobby.code, - isPasswordProtected: lobby.password.length !== 0, - playerCount: getPlayerCountForLobby(lobby), - spectatorCount: Object.keys(lobby.spectators).length, - }); - } - console.log('Found ' + lobbyInfo.length + ' lobbies'); - return lobbyInfo; + async searchLobby(): Promise { + const lobbies: LobbyInfo[] = Object.values(LOBBYMAN.lobbies).map((l) => ({ + code: l.code, + isPasswordProtected: l.password.length !== 0, + playerCount: getPlayerCountForLobby(l), + spectatorCount: Object.keys(l.spectators).length, + })); + console.log('Found ' + lobbies.length + ' lobbies'); + return { type: 'lobbySearched', payload: { lobbies } }; } /** Updates the ready state of the machine. * @param client, The socket that connected. * @returns, true if we successfully readied up, false otherwise. */ - @SubscribeMessage('readyUp') - async readyUp(@ConnectedSocket() client: Socket): Promise { - const lobby = getLobbyForMachine(client.id); + async readyUp(socketId: SocketId): Promise { + const response: Message = { + type: 'readyUpResult', + payload: { ready: false }, + }; + const lobby = getLobbyForMachine(socketId); if (lobby === undefined) { - return false; + return { ...response, payload: { ready: false } }; } - const machine = lobby.machines[client.id]; + const machine = lobby.machines[socketId]; if (machine === undefined) { - return false; + return { ...response, payload: { ready: false } }; } machine.ready = true; - client.nsp.to(lobby.code).emit('state', getLobbyState(client.id)); + const stateMessage = getLobbyState(socketId); + if (stateMessage) { + CLIENTS.send(stateMessage, lobby.code); + } let allReady = true; for (const machine of Object.values(lobby.machines)) { @@ -267,8 +272,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } if (allReady) { - client.nsp.to(lobby.code).emit('startSong'); + CLIENTS.send({ type: 'startSong', payload: { start: true } }, lobby.code); } - return true; + return { type: 'readyUpResult', payload: { ready: allReady } }; } } diff --git a/src/events/events.types.ts b/src/events/events.types.ts index 36b751a..bf110ee 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -1,17 +1,50 @@ -import { Machine } from '../types/models.types'; +import { + LobbyCode, + LobbyInfo, + Machine, + Spectator, +} from '../types/models.types'; -export type MessageType = 'createLobby' | 'lobbyCreated' | 'clientDisconnected'; +export type MessageType = + | 'createLobby' + | 'lobbyCreated' + | 'joinLobby' + | 'lobbyJoined' + | 'leaveLobby' + | 'lobbyLeft' + | 'spectateLobby' + | 'lobbySpectated' + | 'searchLobby' + | 'lobbySearched' + | 'clientDisconnected' + | 'readyUp' + | 'readyUpResult' + | 'lobbyState' + | 'startSong'; export type MessagePayload = | CreateLobbyPayload | LobbyCreatedPayload - | ClientDisconnectedPayload; + | JoinLobbyPayload + | LobbyJoinedPayload + | LeaveLobbyPayload + | LobbyLeftPayload + | SearchLobbyPayload + | LobbySearchedPayload + | ClientDisconnectedPayload + | ReadyUpPayload + | ReadyUpResultPayload + | LobbyStatePayload + | StartSongPayload; export interface Message { type: MessageType; payload: MessagePayload; } +// TODO: We can tighten types here, extend Message with the specific type/payload +// Then our handler signatures can return the correct Message + export interface CreateLobbyPayload { machine: Machine; password: string; @@ -21,6 +54,58 @@ export interface LobbyCreatedPayload { code: Machine; } +export interface JoinLobbyPayload { + machine: Machine; + code: LobbyCode; + password: string; +} + +export interface LobbyJoinedPayload { + joined: boolean; +} + +export interface SpectateLobbyPayload { + spectator: Spectator; + code: LobbyCode; + password: string; +} + +export interface LobbySpectatedPayload { + spectators: number; +} + +export interface SearchLobbyPayload { + spectator: Spectator; + code: LobbyCode; + password: string; +} + +export interface LobbySearchedPayload { + lobbies: LobbyInfo[]; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface LeaveLobbyPayload {} + +export interface LobbyLeftPayload { + left: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ReadyUpPayload {} + +export interface ReadyUpResultPayload { + ready: boolean; +} + export interface ClientDisconnectedPayload { reason: string; } + +export interface LobbyStatePayload { + machines: Machine[]; +} + +export interface StartSongPayload { + start: boolean; +} diff --git a/src/events/utils.ts b/src/events/utils.ts index d61caed..94dc6b3 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -6,6 +6,7 @@ import { Machine, ROOMMAN, } from '../types/models.types'; +import { Message } from './events.types'; /** * Determines if the correct credentials are provided to join a lobby. @@ -152,16 +153,17 @@ export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { return LOBBYMAN.lobbies[code]; } -export function getLobbyState(socketId: SocketId): Machine[] | null { +export function getLobbyState(socketId: SocketId): Message | null { const lobby = getLobbyForMachine(socketId); if (lobby === undefined) { return null; } // Send back the machine state with the socket ids omitted - const machineState = Object.entries(lobby.machines).map((m) => ({ + const machines = Object.entries(lobby.machines).map((m) => ({ socketId, ...m, })); - return machineState; + + return { type: 'lobbyState', payload: { machines } }; } diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 9820e78..8c2573d 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -130,15 +130,23 @@ export class ROOMMAN { ); console.log(this.rooms); } + + static isJoined(socketId: SocketId, code: LobbyCode): boolean { + if (!this.rooms.has(code)) return false; + return Boolean(this.rooms.get(code)?.includes(socketId)); + } } export class CLIENTS { private static clients: Map = new Map(); - static send(response: Message) { - this.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(response)); + static send(response: Message, code?: LobbyCode) { + this.clients.forEach((socket, socketId) => { + // if a lobby code is provided, ensure the client is actually in that lobby + if (code && !ROOMMAN.isJoined(socketId, code)) return; + + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(response)); } }); } From cda638a03ba14cd63505cb7dceee1db5f2802f59 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Fri, 1 Nov 2024 09:07:12 -0700 Subject: [PATCH 09/32] Correct a double disconnect, pass down the expected Socket instead of socketId --- src/events/events.gateway.ts | 30 +++++++++++++++++++++++------- src/types/models.types.ts | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 70f27ab..cbbd5e2 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -54,6 +54,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { this.handlers.set('readyUp', this.readyUp); } + /** + * Listener to handle new websocket connections. Responsible for notifying our CLIENTS manager + * and setting up callbacks to handle incoming messages */ handleConnection(socket: WebSocket, ...args: any[]) { const socketId = CLIENTS.connect(socket); @@ -70,17 +73,27 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { throw new Error('Missing handler'); // Should not happen, but makes TS happy } const response = await handler(socketId, message.payload); - - CLIENTS.send(response); + if (response) { + CLIENTS.sendSocket(response, socketId); + } }); - socket.on('close', () => this.handleDisconnect(socketId)); } /** * Cleans up the lobby manager when a client disconnects. - * @param socketId, The socket id that disconnected. + * @param socket, The socket id that disconnected. + * @override OnGatewayDisconnect */ - handleDisconnect(socketId: string) { + handleDisconnect(socket: WebSocket) { + let socketId: SocketId; + try { + socketId = CLIENTS.getSocketId(socket); + } catch (e) { + console.error('Disconnect not handled, socketId not found for socket'); + return; + } + console.info('Disconnecting socket ' + socketId); + CLIENTS.disconnect(socketId); if (socketId in LOBBYMAN.machineConnections) { @@ -260,7 +273,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { machine.ready = true; const stateMessage = getLobbyState(socketId); if (stateMessage) { - CLIENTS.send(stateMessage, lobby.code); + CLIENTS.sendLobby(stateMessage, lobby.code); } let allReady = true; @@ -272,7 +285,10 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } if (allReady) { - CLIENTS.send({ type: 'startSong', payload: { start: true } }, lobby.code); + CLIENTS.sendLobby( + { type: 'startSong', payload: { start: true } }, + lobby.code, + ); } return { type: 'readyUpResult', payload: { ready: allReady } }; } diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 8c2573d..c8c3ea7 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -138,12 +138,40 @@ export class ROOMMAN { } export class CLIENTS { + // Mapping from socketId to the lobby code for the spectators. private static clients: Map = new Map(); - static send(response: Message, code?: LobbyCode) { + static getSocketId(targetSocket: WebSocket): SocketId { + for (const [socketId, socket] of this.clients.entries()) { + if (socket === targetSocket) return socketId; + } + throw new Error('Socket not found'); + } + + /** Sends a message to all connected clients */ + static sendAll(response: Message) { + this.clients.forEach((socket) => { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(response)); + } + }); + } + + /** Sends a message to a specific socket */ + static sendSocket(response: Message, socketId: SocketId) { + const socket = this.clients.get(socketId); + if (!socket || socket.readyState !== WebSocket.OPEN) { + console.warn('Cannot send to socket, socket is not connected'); + return; + } + socket.send(JSON.stringify(response)); + } + + /** Sends a message to all clients in a particular lobby */ + static sendLobby(response: Message, code: LobbyCode) { this.clients.forEach((socket, socketId) => { - // if a lobby code is provided, ensure the client is actually in that lobby - if (code && !ROOMMAN.isJoined(socketId, code)) return; + // skip clients not in the lobby + if (!ROOMMAN.isJoined(socketId, code)) return; if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(response)); From 561248658ade874047b93e704a43b1cb6e4c2304 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Fri, 1 Nov 2024 09:15:33 -0700 Subject: [PATCH 10/32] Remove unused socket.io server field --- src/events/events.gateway.ts | 11 ++++------- src/events/events.types.ts | 4 ++-- src/types/models.types.ts | 2 -- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index cbbd5e2..8f6f946 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -2,9 +2,7 @@ import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, - WebSocketServer, } from '@nestjs/websockets'; -import { Server } from 'socket.io'; import { WebSocket } from 'ws'; import { CLIENTS, @@ -37,15 +35,14 @@ import { }, }) export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { - @WebSocketServer() - server: Server; - + /** Maps received message types to a callback function to handle those message. + * The callback function may return a message to send to the calling socket */ private handlers: Map< MessageType, - (socketId: SocketId, payload: any) => Promise + (socketId: SocketId, payload: any) => Promise > = new Map(); - afterInit(server: Server) { + afterInit() { this.handlers.set('createLobby', this.createLobby); this.handlers.set('joinLobby', this.joinLobby); this.handlers.set('leaveLobby', this.leaveLobby); diff --git a/src/events/events.types.ts b/src/events/events.types.ts index bf110ee..544ca0f 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -37,9 +37,9 @@ export type MessagePayload = | LobbyStatePayload | StartSongPayload; -export interface Message { +export interface Message { type: MessageType; - payload: MessagePayload; + payload: T; } // TODO: We can tighten types here, extend Message with the specific type/payload diff --git a/src/types/models.types.ts b/src/types/models.types.ts index c8c3ea7..3aea487 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -110,7 +110,6 @@ export class ROOMMAN { } console.info(`Socket ${socketId} is joining room ${code}`); sockets.push(socketId); - console.log(this.rooms); } static leave(socketId: SocketId, code: LobbyCode) { @@ -128,7 +127,6 @@ export class ROOMMAN { code, sockets.filter((s) => s !== socketId), ); - console.log(this.rooms); } static isJoined(socketId: SocketId, code: LobbyCode): boolean { From 88e27277f539b4d6b95400444afe7ee50600f718 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Fri, 1 Nov 2024 09:19:28 -0700 Subject: [PATCH 11/32] Tighten up handler callbacks --- src/events/events.gateway.ts | 18 ++++++++++++------ src/events/events.types.ts | 10 ++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 8f6f946..8624b0c 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -24,8 +24,14 @@ import { CreateLobbyPayload, JoinLobbyPayload, LobbyCreatedPayload, + LobbyJoinedPayload, + LobbyLeftPayload, + LobbySearchedPayload, + LobbySpectatedPayload, Message, MessageType, + ReadyUpResultPayload, + SearchLobbyPayload, SpectateLobbyPayload, } from './events.types'; @@ -112,7 +118,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async createLobby( socketId: string, { machine, password }: CreateLobbyPayload, - ): Promise { + ): Promise> { if (socketId in LOBBYMAN.spectatorConnections) { disconnectSpectator(socketId); } @@ -157,7 +163,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async joinLobby( socketId: SocketId, { machine, code, password }: JoinLobbyPayload, - ): Promise { + ): Promise> { if (!canJoinLobby(code, password)) { return { type: 'lobbyJoined', payload: { joined: false } }; } @@ -188,7 +194,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * @param client, The socket connection of the machine to disconnect. * @returns, True if the machine was disconnected successfully. */ - async leaveLobby(socketId: SocketId): Promise { + async leaveLobby(socketId: SocketId): Promise> { const left = disconnectMachine(socketId); return { type: 'lobbyLeft', payload: { left } }; } @@ -204,7 +210,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async spectateLobby( socketId: SocketId, { spectator, code, password }: SpectateLobbyPayload, - ): Promise { + ): Promise> { const lobby = LOBBYMAN.lobbies[code]; if (!lobby) { @@ -237,7 +243,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * Searches for all active lobbies. * @returns, The list of lobbies that are currently active. */ - async searchLobby(): Promise { + async searchLobby(): Promise> { const lobbies: LobbyInfo[] = Object.values(LOBBYMAN.lobbies).map((l) => ({ code: l.code, isPasswordProtected: l.password.length !== 0, @@ -252,7 +258,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * @param client, The socket that connected. * @returns, true if we successfully readied up, false otherwise. */ - async readyUp(socketId: SocketId): Promise { + async readyUp(socketId: SocketId): Promise> { const response: Message = { type: 'readyUpResult', payload: { ready: false }, diff --git a/src/events/events.types.ts b/src/events/events.types.ts index 544ca0f..a2e9ec6 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -42,9 +42,6 @@ export interface Message { payload: T; } -// TODO: We can tighten types here, extend Message with the specific type/payload -// Then our handler signatures can return the correct Message - export interface CreateLobbyPayload { machine: Machine; password: string; @@ -74,11 +71,8 @@ export interface LobbySpectatedPayload { spectators: number; } -export interface SearchLobbyPayload { - spectator: Spectator; - code: LobbyCode; - password: string; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchLobbyPayload {} export interface LobbySearchedPayload { lobbies: LobbyInfo[]; From 447e45a16bb4f625e2e11bfe70f7ab233265c137 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Fri, 1 Nov 2024 09:26:43 -0700 Subject: [PATCH 12/32] Catch any malformed messages so the server doesn't crash --- src/events/events.gateway.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 8624b0c..4424a26 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -64,20 +64,24 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const socketId = CLIENTS.connect(socket); socket.on('message', async (messageBuffer: Buffer) => { - const message: Message = JSON.parse(messageBuffer.toString()); - if (!message.type || !message.payload) { - throw new Error('Message requires a type and a payload'); - } - if (!this.handlers.has(message.type)) { - throw new Error(`No handler for message type "${message.type}"`); - } - const handler = this.handlers.get(message.type); - if (!handler) { - throw new Error('Missing handler'); // Should not happen, but makes TS happy - } - const response = await handler(socketId, message.payload); - if (response) { - CLIENTS.sendSocket(response, socketId); + try { + const message: Message = JSON.parse(messageBuffer.toString()); + if (!message.type || !message.payload) { + throw new Error('Message requires a type and a payload'); + } + if (!this.handlers.has(message.type)) { + throw new Error(`No handler for message type "${message.type}"`); + } + const handler = this.handlers.get(message.type); + if (!handler) { + throw new Error('Missing handler'); // Should not happen, but makes TS happy + } + const response = await handler(socketId, message.payload); + if (response) { + CLIENTS.sendSocket(response, socketId); + } + } catch (e) { + console.error('Error handling message', e); } }); } From 1c28412c926c8ee919f5585f6330b6a3be7b75fc Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 2 Nov 2024 10:56:58 -0700 Subject: [PATCH 13/32] Update tests, remove socket.io --- package-lock.json | 186 ++++++++++---------------- package.json | 2 - src/events/events.gateway.spec.ts | 209 +++++++++++++++++++----------- src/events/events.types.ts | 2 +- src/events/utils.ts | 3 +- src/types/models.types.ts | 14 +- 6 files changed, 211 insertions(+), 205 deletions(-) diff --git a/package-lock.json b/package-lock.json index 225c9e7..b52e7a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,11 @@ "@nestjs/common": "^10.3.7", "@nestjs/core": "^10.3.7", "@nestjs/platform-express": "^10.3.7", - "@nestjs/platform-socket.io": "^10.3.7", "@nestjs/platform-ws": "^10.3.7", "@nestjs/websockets": "^10.3.7", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", - "socket.io-client": "^4.6.1", "uuid": "^11.0.2", "ws": "^8.18.0" }, @@ -1607,6 +1605,8 @@ "version": "10.3.7", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", + "optional": true, + "peer": true, "dependencies": { "socket.io": "4.7.5", "tslib": "2.6.2" @@ -1802,7 +1802,9 @@ "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "optional": true, + "peer": true }, "node_modules/@tsconfig/node10": { "version": "1.0.9", @@ -1891,7 +1893,9 @@ "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "optional": true, + "peer": true }, "node_modules/@types/cookiejar": { "version": "2.1.2", @@ -1903,6 +1907,8 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "optional": true, + "peer": true, "dependencies": { "@types/node": "*" } @@ -2020,7 +2026,8 @@ "node_modules/@types/node": { "version": "16.18.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.23.tgz", - "integrity": "sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==" + "integrity": "sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==", + "devOptional": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", @@ -2926,6 +2933,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "optional": true, + "peer": true, "engines": { "node": "^4.5.0 || >= 5.9" } @@ -3527,6 +3536,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, "dependencies": { "ms": "2.1.2" }, @@ -3743,6 +3753,8 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "optional": true, + "peer": true, "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -3759,51 +3771,12 @@ "node": ">=10.2.0" } }, - "node_modules/engine.io-client": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", - "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", - "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/engine.io/node_modules/cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -3812,6 +3785,8 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" } @@ -3821,6 +3796,8 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -6831,7 +6808,8 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true }, "node_modules/multer": { "version": "1.4.4-lts.1", @@ -8175,6 +8153,8 @@ "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "optional": true, + "peer": true, "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -8192,6 +8172,8 @@ "version": "2.5.4", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "optional": true, + "peer": true, "dependencies": { "debug": "~4.3.4", "ws": "~8.11.0" @@ -8202,6 +8184,8 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -8218,24 +8202,12 @@ } } }, - "node_modules/socket.io-client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", - "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.4.0", - "socket.io-parser": "~4.2.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "optional": true, + "peer": true, "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -9405,14 +9377,6 @@ } } }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -10660,6 +10624,8 @@ "version": "10.3.7", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", + "optional": true, + "peer": true, "requires": { "socket.io": "4.7.5", "tslib": "2.6.2" @@ -10776,7 +10742,9 @@ "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "optional": true, + "peer": true }, "@tsconfig/node10": { "version": "1.0.9", @@ -10865,7 +10833,9 @@ "@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "optional": true, + "peer": true }, "@types/cookiejar": { "version": "2.1.2", @@ -10877,6 +10847,8 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "optional": true, + "peer": true, "requires": { "@types/node": "*" } @@ -10994,7 +10966,8 @@ "@types/node": { "version": "16.18.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.23.tgz", - "integrity": "sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==" + "integrity": "sha512-XAMpaw1s1+6zM+jn2tmw8MyaRDIJfXxqmIQIS0HfoGYPuf7dUWeiUKopwq13KFX9lEp1+THGtlaaYx39Nxr58g==", + "devOptional": true }, "@types/normalize-package-data": { "version": "2.4.1", @@ -11659,7 +11632,9 @@ "base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "optional": true, + "peer": true }, "binary-extensions": { "version": "2.2.0", @@ -12092,6 +12067,7 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "devOptional": true, "requires": { "ms": "2.1.2" } @@ -12250,6 +12226,8 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "optional": true, + "peer": true, "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -12266,46 +12244,27 @@ "cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "optional": true, + "peer": true }, "engine.io-parser": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==" + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "optional": true, + "peer": true }, "ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "optional": true, + "peer": true, "requires": {} } } }, - "engine.io-client": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz", - "integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.0.3", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - }, - "dependencies": { - "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", - "requires": {} - } - } - }, - "engine.io-parser": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", - "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==" - }, "enhanced-resolve": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", @@ -14540,7 +14499,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true }, "multer": { "version": "1.4.4-lts.1", @@ -15527,6 +15487,8 @@ "version": "4.7.5", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "optional": true, + "peer": true, "requires": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -15541,6 +15503,8 @@ "version": "2.5.4", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "optional": true, + "peer": true, "requires": { "debug": "~4.3.4", "ws": "~8.11.0" @@ -15550,25 +15514,18 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "optional": true, + "peer": true, "requires": {} } } }, - "socket.io-client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz", - "integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.4.0", - "socket.io-parser": "~4.2.1" - } - }, "socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "optional": true, + "peer": true, "requires": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -16398,11 +16355,6 @@ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "requires": {} }, - "xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 9ac627d..90a7162 100644 --- a/package.json +++ b/package.json @@ -30,13 +30,11 @@ "@nestjs/common": "^10.3.7", "@nestjs/core": "^10.3.7", "@nestjs/platform-express": "^10.3.7", - "@nestjs/platform-socket.io": "^10.3.7", "@nestjs/platform-ws": "^10.3.7", "@nestjs/websockets": "^10.3.7", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", - "socket.io-client": "^4.6.1", "uuid": "^11.0.2", "ws": "^8.18.0" }, diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index a66c606..a9ff453 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -1,116 +1,173 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { AppModule } from '../app.module'; -import { io, Socket } from 'socket.io-client'; -import { LobbyInfo, LOBBYMAN } from '../types/models.types'; +import { LOBBYMAN } from '../types/models.types'; +import { + CreateLobbyPayload, + LeaveLobbyPayload, + LobbyCreatedPayload, + LobbyLeftPayload, + LobbySearchedPayload, + LobbySpectatedPayload, + Message, + SearchLobbyPayload, + SpectateLobbyPayload, +} from './events.types'; +import { WebSocket } from 'ws'; +import { WsAdapter } from '@nestjs/platform-ws'; + +const port = 3001; describe('EventsGateway', () => { let app: INestApplication; - let socket: Socket; + let client: WebSocket; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile(); - app = moduleRef.createNestApplication(); - // Run tests on a different port than 3000 (the default for this app). - await app.listen(3001); + app.useWebSocketAdapter(new WsAdapter(app)); + await app.init(); + await app.listen(port); }); - beforeEach(() => { + beforeEach(async () => { // Clear out data before every test. LOBBYMAN.lobbies = {}; LOBBYMAN.machineConnections = {}; LOBBYMAN.spectatorConnections = {}; - socket = io('http://localhost:3001'); - socket.connect(); + // Create a new client + client = new WebSocket('ws://localhost:' + port); + client.on('error', (error) => { + console.error('WebSocket Error:', error); + }); + await new Promise((resolve) => { + client.on('open', resolve); + }); }); describe('generalLobbyUsage', () => { it('createLobby', async () => { - const code = await new Promise((resolve) => { - socket.emit( - 'createLobby', - { machine: { player1: { playerName: 'teejusb' } } }, - (data: string) => { - expect(data.length).toEqual(4); - resolve(data); - }, - ); - }); - - await new Promise((resolve) => { - socket.emit('searchLobby', (data: LobbyInfo[]) => { - expect(data.length).toEqual(1); - expect(data[0].code).toEqual(code); - expect(data[0].playerCount).toEqual(1); - expect(data[0].spectatorCount).toEqual(0); - resolve(undefined); - }); - }); - - await new Promise((resolve) => { - socket.emit( - 'spectateLobby', - { code: code, password: '' }, - (spectatorCount: number) => { - // Spectate should fail as a player can't also be a spectator. - expect(spectatorCount).toEqual(0); - resolve(undefined); + console.log('Create Lobby'); + + const create = await send( + client, + { + type: 'createLobby', + payload: { + machine: { player1: { playerId: 'id', profileName: 'teejusb' } }, + password: '', }, - ); - }); - - const socket2 = io('http://localhost:3001'); - socket2.connect(); - - await new Promise((resolve) => { - socket2.emit( - 'spectateLobby', - { code: code, password: '' }, - (spectatorCount: number) => { - // socket2 is a different connection, so we can spectate now. - expect(spectatorCount).toEqual(1); - resolve(undefined); + }, + ); + expect(create.type).toBe('lobbyCreated'); + expect(create.payload).toHaveProperty('code'); + expect(typeof create.payload.code).toBe('string'); + expect(create.payload.code.length).toBe(4); + + const search = await send( + client, + { + type: 'searchLobby', + payload: {}, + }, + ); + expect(search.type).toBe('lobbySearched'); + expect(search.payload.lobbies.length).toBe(1); + expect(search.payload.lobbies[0].code).toBe(create.payload.code); + expect(search.payload.lobbies[0].playerCount).toBe(1); + expect(search.payload.lobbies[0].spectatorCount).toBe(0); + + const spectate = await send( + client, + { + type: 'spectateLobby', + payload: { + spectator: { + profileName: 'E.Norma', + }, + code: search.payload.lobbies[0].code, + password: '', }, - ); - }); - - await new Promise((resolve) => { - socket.emit('searchLobby', (data: LobbyInfo[]) => { - expect(data.length).toEqual(1); - expect(data[0].code).toEqual(code); - expect(data[0].playerCount).toEqual(1); - expect(data[0].spectatorCount).toEqual(1); - resolve(undefined); - }); - }); + }, + ); + expect(spectate.type).toBe('lobbySpectated'); + expect(spectate.payload.spectators).toBe(0); // Spectate should fail as a player can't also be a spectator. + const client2 = new WebSocket('ws://localhost:' + port); await new Promise((resolve) => { - socket.emit('leaveLobby', {}, (didLeave: boolean) => { - expect(didLeave).toEqual(true); - resolve(undefined); - }); + client2.on('open', resolve); }); - await new Promise((resolve) => { - socket.emit('searchLobby', (data: LobbyInfo[]) => { - expect(data.length).toEqual(0); - resolve(undefined); - }); + const spectate2 = await send( + client2, + { + type: 'spectateLobby', + payload: { + spectator: { + profileName: 'Brat', + }, + code: search.payload.lobbies[0].code, + password: '', + }, + }, + ); + expect(spectate2.type).toBe('lobbySpectated'); + expect(spectate2.payload.spectators).toBe(1); // socket2 is a different connection, so we can spectate now. + + const search2 = await send( + client, + { + type: 'searchLobby', + payload: {}, + }, + ); + expect(search2.type).toBe('lobbySearched'); + expect(search2.payload.lobbies.length).toBe(1); + expect(search2.payload.lobbies[0].code).toBe(create.payload.code); + expect(search2.payload.lobbies[0].playerCount).toBe(1); + expect(search2.payload.lobbies[0].spectatorCount).toBe(1); + + const leave = await send(client, { + type: 'leaveLobby', + payload: {}, }); - - socket2.disconnect(); + expect(leave.type).toBe('lobbyLeft'); + expect(leave.payload.left).toBeTruthy(); + + const search3 = await send( + client, + { + type: 'searchLobby', + payload: {}, + }, + ); + expect(search3.type).toBe('lobbySearched'); + expect(search3.payload.lobbies.length).toBe(0); + + client2.close(); }); }); afterEach(() => { - socket.disconnect(); + client.close(); }); afterAll(async () => { await app.close(); }); }); + +function send( + client: WebSocket, + message: Message, +): Promise> { + return new Promise((resolve) => { + client.on('message', (response: Message) => { + resolve(JSON.parse(response.toString())); + }); + client.send(JSON.stringify(message)); + }); +} diff --git a/src/events/events.types.ts b/src/events/events.types.ts index a2e9ec6..c6f792d 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -48,7 +48,7 @@ export interface CreateLobbyPayload { } export interface LobbyCreatedPayload { - code: Machine; + code: LobbyCode; } export interface JoinLobbyPayload { diff --git a/src/events/utils.ts b/src/events/utils.ts index 94dc6b3..e24b12d 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -1,10 +1,9 @@ -import { SocketId } from 'socket.io-adapter'; import { CLIENTS, LOBBYMAN, Lobby, - Machine, ROOMMAN, + SocketId, } from '../types/models.types'; import { Message } from './events.types'; diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 3aea487..46317af 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -6,7 +6,7 @@ export type SocketId = string; export type LobbyCode = string; -export class Judgments { +export interface Judgments { fantasticPlus: number; fantastics: number; excellents: number; @@ -23,12 +23,12 @@ export class Judgments { totalRolls: number; } -export class Spectator { +export interface Spectator { profileName: string; socketId?: SocketId; } -export class SongInfo { +export interface SongInfo { // The path for the song on a player's filesystem. // We'll use this as a key for other players to play. // e.g. 5guys1pack/Earthquake @@ -50,7 +50,7 @@ export class SongInfo { songLength: number; } -export class Player { +export interface Player { playerId: string; profileName: string; @@ -59,14 +59,14 @@ export class Player { exScore?: number; } -export class Machine { +export interface Machine { player1?: Player; player2?: Player; socketId?: SocketId; ready?: boolean; } -export class Lobby { +export interface Lobby { code: LobbyCode; // Empty string here is equivalent to "no password". We could use undefined // but we can consider them the same. @@ -77,7 +77,7 @@ export class Lobby { songInfo?: SongInfo; } -export class LobbyInfo { +export interface LobbyInfo { code: LobbyCode; isPasswordProtected: boolean; playerCount: number; From 6544462b93e4c5483d12395e5b4ea86118dfd1ea Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 2 Nov 2024 11:35:48 -0700 Subject: [PATCH 14/32] Create a service for handling clients --- src/app.module.ts | 3 +- src/clients/client.module.ts | 8 +++ src/clients/client.service.ts | 85 +++++++++++++++++++++++++++++++ src/events/events.gateway.ts | 59 +++++++++++++--------- src/events/events.module.ts | 3 +- src/events/utils.ts | 16 +++--- src/types/models.types.ts | 94 +++-------------------------------- 7 files changed, 148 insertions(+), 120 deletions(-) create mode 100644 src/clients/client.module.ts create mode 100644 src/clients/client.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index a93eadf..f8eac6f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { EventsModule } from './events/events.module'; +import { ClientModule } from './clients/client.module'; @Module({ - imports: [EventsModule], + imports: [EventsModule, ClientModule], controllers: [AppController], providers: [AppService], }) diff --git a/src/clients/client.module.ts b/src/clients/client.module.ts new file mode 100644 index 0000000..caf38f5 --- /dev/null +++ b/src/clients/client.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ClientService } from './client.service'; + +@Module({ + providers: [ClientService], + exports: [ClientService], +}) +export class ClientModule {} diff --git a/src/clients/client.service.ts b/src/clients/client.service.ts new file mode 100644 index 0000000..28d8e99 --- /dev/null +++ b/src/clients/client.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; +import WebSocket = require('ws'); +import { Message } from '../events/events.types'; +import { SocketId, LobbyCode, ROOMMAN } from '../types/models.types'; +import { v4 as uuid } from 'uuid'; +@Injectable() +export class ClientService { + // Mapping from socketId to the lobby code for the spectators. + private clients: Map = new Map(); + + getSocketId(targetSocket: WebSocket): SocketId { + for (const [socketId, socket] of this.clients.entries()) { + if (socket === targetSocket) return socketId; + } + throw new Error('Socket not found'); + } + + /** Sends a message to all connected clients */ + sendAll(response: Message) { + this.clients.forEach((socket) => { + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(response)); + } + }); + } + + /** Sends a message to a specific socket */ + sendSocket(response: Message, socketId: SocketId) { + const socket = this.clients.get(socketId); + if (!socket || socket.readyState !== WebSocket.OPEN) { + console.warn('Cannot send to socket, socket is not connected'); + return; + } + socket.send(JSON.stringify(response)); + } + + /** Sends a message to all clients in a particular lobby */ + sendLobby(response: Message, code: LobbyCode) { + this.clients.forEach((socket, socketId) => { + // skip clients not in the lobby + if (!ROOMMAN.isJoined(socketId, code)) return; + + if (socket.readyState === WebSocket.OPEN) { + socket.send(JSON.stringify(response)); + } + }); + } + + disconnect(socketId: SocketId, reason?: string) { + if (!this.clients.has(socketId)) { + console.warn(`Client ${socketId} not connected`); + return; + } + + const message: Message = { + type: 'clientDisconnected', + payload: { reason: reason || 'Just because' }, + }; + + const client = this.clients.get(socketId); + if (!client) return; + + if (client.readyState === WebSocket.OPEN) { + this.clients.get(socketId)?.close(1000, JSON.stringify(message)); + } + this.clients.delete(socketId); + } + + connect(socket: WebSocket): string { + // Assert we're not already connected + const entry = Object.entries(this.clients).find( + ([, value]) => socket === value, + ); + if (entry) { + console.warn(`Socket ${entry[0]} is already connected`); + return entry[0]; + } + + // Generate an id for the entry, set and return it + const socketId = uuid(); + this.clients.set(socketId, socket); + console.log('Socket connected: ', socketId); + return socketId; + } +} diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 4424a26..69c9e74 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -4,13 +4,7 @@ import { WebSocketGateway, } from '@nestjs/websockets'; import { WebSocket } from 'ws'; -import { - CLIENTS, - LOBBYMAN, - LobbyInfo, - ROOMMAN, - SocketId, -} from '../types/models.types'; +import { LOBBYMAN, LobbyInfo, ROOMMAN, SocketId } from '../types/models.types'; import { disconnectMachine, disconnectSpectator, @@ -31,9 +25,9 @@ import { Message, MessageType, ReadyUpResultPayload, - SearchLobbyPayload, SpectateLobbyPayload, } from './events.types'; +import { ClientService } from '../clients/client.service'; @WebSocketGateway({ cors: { @@ -45,9 +39,15 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * The callback function may return a message to send to the calling socket */ private handlers: Map< MessageType, - (socketId: SocketId, payload: any) => Promise + ( + socketId: SocketId, + payload: any, + clients: ClientService, + ) => Promise > = new Map(); + constructor(private readonly clients: ClientService) {} + afterInit() { this.handlers.set('createLobby', this.createLobby); this.handlers.set('joinLobby', this.joinLobby); @@ -60,8 +60,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { /** * Listener to handle new websocket connections. Responsible for notifying our CLIENTS manager * and setting up callbacks to handle incoming messages */ - handleConnection(socket: WebSocket, ...args: any[]) { - const socketId = CLIENTS.connect(socket); + handleConnection(socket: WebSocket) { + const socketId = this.clients.connect(socket); socket.on('message', async (messageBuffer: Buffer) => { try { @@ -76,9 +76,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (!handler) { throw new Error('Missing handler'); // Should not happen, but makes TS happy } - const response = await handler(socketId, message.payload); + const response = await handler(socketId, message.payload, this.clients); if (response) { - CLIENTS.sendSocket(response, socketId); + this.clients.sendSocket(response, socketId); } } catch (e) { console.error('Error handling message', e); @@ -94,17 +94,17 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { handleDisconnect(socket: WebSocket) { let socketId: SocketId; try { - socketId = CLIENTS.getSocketId(socket); + socketId = this.clients.getSocketId(socket); } catch (e) { console.error('Disconnect not handled, socketId not found for socket'); return; } console.info('Disconnecting socket ' + socketId); - CLIENTS.disconnect(socketId); + this.clients.disconnect(socketId); if (socketId in LOBBYMAN.machineConnections) { - disconnectMachine(socketId); + disconnectMachine(socketId, this.clients); } if (socketId in LOBBYMAN.spectatorConnections) { @@ -122,6 +122,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async createLobby( socketId: string, { machine, password }: CreateLobbyPayload, + clients: ClientService, ): Promise> { if (socketId in LOBBYMAN.spectatorConnections) { disconnectSpectator(socketId); @@ -129,7 +130,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (socketId in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - disconnectMachine(socketId); + disconnectMachine(socketId, clients); } let code = generateLobbyCode(); @@ -167,6 +168,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async joinLobby( socketId: SocketId, { machine, code, password }: JoinLobbyPayload, + clients: ClientService, ): Promise> { if (!canJoinLobby(code, password)) { return { type: 'lobbyJoined', payload: { joined: false } }; @@ -178,7 +180,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (socketId in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - disconnectMachine(socketId); + disconnectMachine(socketId, clients); } const lobby = LOBBYMAN.lobbies[code]; @@ -198,8 +200,15 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * @param client, The socket connection of the machine to disconnect. * @returns, True if the machine was disconnected successfully. */ - async leaveLobby(socketId: SocketId): Promise> { - const left = disconnectMachine(socketId); + async leaveLobby( + socketId: SocketId, + {}, + clients?: ClientService, + ): Promise> { + let left = false; + if (clients) { + left = disconnectMachine(socketId, clients); + } return { type: 'lobbyLeft', payload: { left } }; } @@ -262,7 +271,11 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * @param client, The socket that connected. * @returns, true if we successfully readied up, false otherwise. */ - async readyUp(socketId: SocketId): Promise> { + async readyUp( + socketId: SocketId, + {}, + clients: ClientService, + ): Promise> { const response: Message = { type: 'readyUpResult', payload: { ready: false }, @@ -280,7 +293,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { machine.ready = true; const stateMessage = getLobbyState(socketId); if (stateMessage) { - CLIENTS.sendLobby(stateMessage, lobby.code); + clients.sendLobby(stateMessage, lobby.code); } let allReady = true; @@ -292,7 +305,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } if (allReady) { - CLIENTS.sendLobby( + clients.sendLobby( { type: 'startSong', payload: { start: true } }, lobby.code, ); diff --git a/src/events/events.module.ts b/src/events/events.module.ts index 2b2d1cb..74bd8d1 100644 --- a/src/events/events.module.ts +++ b/src/events/events.module.ts @@ -1,7 +1,8 @@ import { Module } from '@nestjs/common'; import { EventsGateway } from './events.gateway'; +import { ClientService } from '../clients/client.service'; @Module({ - providers: [EventsGateway], + providers: [EventsGateway, ClientService], }) export class EventsModule {} diff --git a/src/events/utils.ts b/src/events/utils.ts index e24b12d..7dd2dbd 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -1,10 +1,5 @@ -import { - CLIENTS, - LOBBYMAN, - Lobby, - ROOMMAN, - SocketId, -} from '../types/models.types'; +import { ClientService } from '../clients/client.service'; +import { LOBBYMAN, Lobby, ROOMMAN, SocketId } from '../types/models.types'; import { Message } from './events.types'; /** @@ -67,7 +62,10 @@ export function getPlayerCountForLobby(lobby: Lobby): number { * @param socketId, The socket ID of the machine to disconnect. * @returns True if the machine left the lobby, false otherwise. */ -export function disconnectMachine(socketId: SocketId): boolean { +export function disconnectMachine( + socketId: SocketId, + clients: ClientService, +): boolean { const code = LOBBYMAN.machineConnections[socketId]; if (code === undefined) { return false; @@ -101,7 +99,7 @@ export function disconnectMachine(socketId: SocketId): boolean { ROOMMAN.leave(spectator.socketId, code); // Force a disconnect. If there are no more players in the lobby, // we should remove the spectators as well. - CLIENTS.disconnect(spectator.socketId); + clients.disconnect(spectator.socketId); delete LOBBYMAN.spectatorConnections[spectator.socketId]; } } diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 46317af..083f28b 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -1,7 +1,3 @@ -import WebSocket = require('ws'); -import { v4 as uuidv4 } from 'uuid'; -import { Message } from '../events/events.types'; - export type SocketId = string; export type LobbyCode = string; @@ -103,7 +99,10 @@ export class ROOMMAN { if (!this.rooms.has(code)) { this.rooms.set(code, []); } - const sockets = this.rooms.get(code)!; + const sockets = this.rooms.get(code); + if (!sockets) { + throw new Error('No socket with code ' + code); // Shouldn't happen, since we set the code right before this + } if (sockets.includes(socketId)) { console.warn(`Socket ${socketId} is already in room ${code}`); return; @@ -117,7 +116,10 @@ export class ROOMMAN { console.warn(`No room for code ${code}`); return; } - const sockets = this.rooms.get(code)!; + const sockets = this.rooms.get(code); + if (!sockets) { + throw new Error('No socket with code ' + code); // Shouldn't happen, since we set the code right before this + } if (!sockets.includes(socketId)) { console.warn(`Socket ${socketId} is not in room ${code}`); return; @@ -134,83 +136,3 @@ export class ROOMMAN { return Boolean(this.rooms.get(code)?.includes(socketId)); } } - -export class CLIENTS { - // Mapping from socketId to the lobby code for the spectators. - private static clients: Map = new Map(); - - static getSocketId(targetSocket: WebSocket): SocketId { - for (const [socketId, socket] of this.clients.entries()) { - if (socket === targetSocket) return socketId; - } - throw new Error('Socket not found'); - } - - /** Sends a message to all connected clients */ - static sendAll(response: Message) { - this.clients.forEach((socket) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify(response)); - } - }); - } - - /** Sends a message to a specific socket */ - static sendSocket(response: Message, socketId: SocketId) { - const socket = this.clients.get(socketId); - if (!socket || socket.readyState !== WebSocket.OPEN) { - console.warn('Cannot send to socket, socket is not connected'); - return; - } - socket.send(JSON.stringify(response)); - } - - /** Sends a message to all clients in a particular lobby */ - static sendLobby(response: Message, code: LobbyCode) { - this.clients.forEach((socket, socketId) => { - // skip clients not in the lobby - if (!ROOMMAN.isJoined(socketId, code)) return; - - if (socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify(response)); - } - }); - } - - static disconnect(socketId: SocketId, reason?: string) { - if (!this.clients.has(socketId)) { - console.warn(`Client ${socketId} not connected`); - return; - } - - const message: Message = { - type: 'clientDisconnected', - payload: { reason: reason || 'Just because' }, - }; - - const client = this.clients.get(socketId); - if (!client) return; - - if (client.readyState === WebSocket.OPEN) { - this.clients.get(socketId)?.close(1000, JSON.stringify(message)); - } - this.clients.delete(socketId); - } - - static connect(socket: WebSocket): string { - // Assert we're not already connected - const entry = Object.entries(this.clients).find( - ([, value]) => socket === value, - ); - if (entry) { - console.warn(`Socket ${entry[0]} is already connected`); - return entry[0]; - } - - // Generate an id for the entry, set and return it - const socketId = uuidv4(); - this.clients.set(socketId, socket); - console.log('Socket connected: ', socketId); - return socketId; - } -} From 62dd07d54a5bce15893ca38288cd4f5e782ac726 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sun, 3 Nov 2024 07:45:37 -0800 Subject: [PATCH 15/32] Bind message listeners --- src/events/events.gateway.ts | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 69c9e74..ecb6e04 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -39,11 +39,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * The callback function may return a message to send to the calling socket */ private handlers: Map< MessageType, - ( - socketId: SocketId, - payload: any, - clients: ClientService, - ) => Promise + (socketId: SocketId, payload: any) => Promise > = new Map(); constructor(private readonly clients: ClientService) {} @@ -76,7 +72,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (!handler) { throw new Error('Missing handler'); // Should not happen, but makes TS happy } - const response = await handler(socketId, message.payload, this.clients); + // Retain "this" context within the handler callbacks (otherwise we lose this.clients) + const handlerBinded = handler.bind(this); + const response = await handlerBinded(socketId, message.payload); if (response) { this.clients.sendSocket(response, socketId); } @@ -122,7 +120,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async createLobby( socketId: string, { machine, password }: CreateLobbyPayload, - clients: ClientService, ): Promise> { if (socketId in LOBBYMAN.spectatorConnections) { disconnectSpectator(socketId); @@ -130,7 +127,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (socketId in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - disconnectMachine(socketId, clients); + disconnectMachine(socketId, this.clients); } let code = generateLobbyCode(); @@ -168,7 +165,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async joinLobby( socketId: SocketId, { machine, code, password }: JoinLobbyPayload, - clients: ClientService, ): Promise> { if (!canJoinLobby(code, password)) { return { type: 'lobbyJoined', payload: { joined: false } }; @@ -180,7 +176,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (socketId in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - disconnectMachine(socketId, clients); + disconnectMachine(socketId, this.clients); } const lobby = LOBBYMAN.lobbies[code]; @@ -200,15 +196,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * @param client, The socket connection of the machine to disconnect. * @returns, True if the machine was disconnected successfully. */ - async leaveLobby( - socketId: SocketId, - {}, - clients?: ClientService, - ): Promise> { + async leaveLobby(socketId: SocketId, {}): Promise> { let left = false; - if (clients) { - left = disconnectMachine(socketId, clients); - } + left = disconnectMachine(socketId, this.clients); return { type: 'lobbyLeft', payload: { left } }; } @@ -274,7 +264,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async readyUp( socketId: SocketId, {}, - clients: ClientService, ): Promise> { const response: Message = { type: 'readyUpResult', @@ -293,7 +282,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { machine.ready = true; const stateMessage = getLobbyState(socketId); if (stateMessage) { - clients.sendLobby(stateMessage, lobby.code); + this.clients.sendLobby(stateMessage, lobby.code); } let allReady = true; @@ -305,7 +294,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } if (allReady) { - clients.sendLobby( + this.clients.sendLobby( { type: 'startSong', payload: { start: true } }, lobby.code, ); From 64bff869bbd3da167c883096d76e9b45ca894e34 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 12:20:28 -0800 Subject: [PATCH 16/32] Swap to Records --- src/clients/client.service.ts | 28 ++++++++++++++-------------- src/events/events.gateway.ts | 28 ++++++++++++++++------------ src/types/models.types.ts | 24 +++++++++--------------- 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/src/clients/client.service.ts b/src/clients/client.service.ts index 28d8e99..69b4ead 100644 --- a/src/clients/client.service.ts +++ b/src/clients/client.service.ts @@ -6,10 +6,10 @@ import { v4 as uuid } from 'uuid'; @Injectable() export class ClientService { // Mapping from socketId to the lobby code for the spectators. - private clients: Map = new Map(); + private clients: Record = {}; getSocketId(targetSocket: WebSocket): SocketId { - for (const [socketId, socket] of this.clients.entries()) { + for (const [socketId, socket] of Object.entries(this.clients)) { if (socket === targetSocket) return socketId; } throw new Error('Socket not found'); @@ -17,16 +17,16 @@ export class ClientService { /** Sends a message to all connected clients */ sendAll(response: Message) { - this.clients.forEach((socket) => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify(response)); + for (const client of Object.values(this.clients)) { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(response)); } - }); + } } /** Sends a message to a specific socket */ sendSocket(response: Message, socketId: SocketId) { - const socket = this.clients.get(socketId); + const socket = this.clients[socketId]; if (!socket || socket.readyState !== WebSocket.OPEN) { console.warn('Cannot send to socket, socket is not connected'); return; @@ -36,18 +36,18 @@ export class ClientService { /** Sends a message to all clients in a particular lobby */ sendLobby(response: Message, code: LobbyCode) { - this.clients.forEach((socket, socketId) => { + for (const [socketId, socket] of Object.entries(this.clients)) { // skip clients not in the lobby if (!ROOMMAN.isJoined(socketId, code)) return; if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(response)); } - }); + } } disconnect(socketId: SocketId, reason?: string) { - if (!this.clients.has(socketId)) { + if (!this.clients[socketId]) { console.warn(`Client ${socketId} not connected`); return; } @@ -57,13 +57,13 @@ export class ClientService { payload: { reason: reason || 'Just because' }, }; - const client = this.clients.get(socketId); + const client = this.clients[socketId]; if (!client) return; if (client.readyState === WebSocket.OPEN) { - this.clients.get(socketId)?.close(1000, JSON.stringify(message)); + client.close(1000, JSON.stringify(message)); } - this.clients.delete(socketId); + delete this.clients[socketId]; } connect(socket: WebSocket): string { @@ -78,7 +78,7 @@ export class ClientService { // Generate an id for the entry, set and return it const socketId = uuid(); - this.clients.set(socketId, socket); + this.clients[socketId] = socket; console.log('Socket connected: ', socketId); return socketId; } diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index ecb6e04..59d20d4 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -37,20 +37,24 @@ import { ClientService } from '../clients/client.service'; export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { /** Maps received message types to a callback function to handle those message. * The callback function may return a message to send to the calling socket */ - private handlers: Map< - MessageType, - (socketId: SocketId, payload: any) => Promise - > = new Map(); + private handlers: Partial< + Record< + MessageType, + (socketId: SocketId, payload: any) => Promise + > + >; constructor(private readonly clients: ClientService) {} afterInit() { - this.handlers.set('createLobby', this.createLobby); - this.handlers.set('joinLobby', this.joinLobby); - this.handlers.set('leaveLobby', this.leaveLobby); - this.handlers.set('spectateLobby', this.spectateLobby); - this.handlers.set('searchLobby', this.searchLobby); - this.handlers.set('readyUp', this.readyUp); + this.handlers = { + createLobby: this.createLobby, + joinLobby: this.joinLobby, + leaveLobby: this.leaveLobby, + spectateLobby: this.spectateLobby, + searchLobby: this.searchLobby, + readyUp: this.readyUp, + }; } /** @@ -65,10 +69,10 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (!message.type || !message.payload) { throw new Error('Message requires a type and a payload'); } - if (!this.handlers.has(message.type)) { + if (!this.handlers[message.type]) { throw new Error(`No handler for message type "${message.type}"`); } - const handler = this.handlers.get(message.type); + const handler = this.handlers[message.type]; if (!handler) { throw new Error('Missing handler'); // Should not happen, but makes TS happy } diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 083f28b..9828821 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -93,16 +93,13 @@ export class LOBBYMAN { export class ROOMMAN { // Mapping of lobby ids (rooms) to the socketIds in that room - private static rooms: Map> = new Map(); + private static rooms: Record> = {}; static join(socketId: SocketId, code: LobbyCode) { - if (!this.rooms.has(code)) { - this.rooms.set(code, []); - } - const sockets = this.rooms.get(code); - if (!sockets) { - throw new Error('No socket with code ' + code); // Shouldn't happen, since we set the code right before this + if (!this.rooms[code]) { + this.rooms[code] = []; } + const sockets = this.rooms[code]; if (sockets.includes(socketId)) { console.warn(`Socket ${socketId} is already in room ${code}`); return; @@ -112,11 +109,11 @@ export class ROOMMAN { } static leave(socketId: SocketId, code: LobbyCode) { - if (!this.rooms.has(code)) { + if (!this.rooms[code]) { console.warn(`No room for code ${code}`); return; } - const sockets = this.rooms.get(code); + const sockets = this.rooms[code]; if (!sockets) { throw new Error('No socket with code ' + code); // Shouldn't happen, since we set the code right before this } @@ -125,14 +122,11 @@ export class ROOMMAN { return; } console.info(`Socket ${socketId} is leaving room ${code}`); - this.rooms.set( - code, - sockets.filter((s) => s !== socketId), - ); + this.rooms[code] = sockets.filter((s) => s !== socketId); } static isJoined(socketId: SocketId, code: LobbyCode): boolean { - if (!this.rooms.has(code)) return false; - return Boolean(this.rooms.get(code)?.includes(socketId)); + if (!this.rooms[code]) return false; + return Boolean(this.rooms[code].includes(socketId)); } } From 5ef647e1fbf8179beaba906bd4f5f5a6aeee5d34 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 15:52:48 -0800 Subject: [PATCH 17/32] Add updateMachine payloads --- package.json | 2 +- src/events/events.gateway.spec.ts | 31 +++++++++++++++- src/events/events.gateway.ts | 60 +++++++++++++++++++++++++++---- src/events/events.types.ts | 28 +++++++++++---- src/events/utils.ts | 2 +- src/types/models.types.ts | 23 ++++++++++-- 6 files changed, 128 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 90a7162..30da317 100644 --- a/package.json +++ b/package.json @@ -80,4 +80,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} +} \ No newline at end of file diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index a9ff453..1f5fc6d 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -9,9 +9,11 @@ import { LobbyLeftPayload, LobbySearchedPayload, LobbySpectatedPayload, + MachineUpdatedPayload, Message, SearchLobbyPayload, SpectateLobbyPayload, + UpdateMachinePayload, } from './events.types'; import { WebSocket } from 'ws'; import { WsAdapter } from '@nestjs/platform-ws'; @@ -57,7 +59,7 @@ describe('EventsGateway', () => { { type: 'createLobby', payload: { - machine: { player1: { playerId: 'id', profileName: 'teejusb' } }, + machine: { player1: { playerId: 'P1', profileName: 'teejusb' } }, password: '', }, }, @@ -149,6 +151,33 @@ describe('EventsGateway', () => { client2.close(); }); + + it('updateMachine', async () => { + const create = await send( + client, + { + type: 'createLobby', + payload: { + machine: { player1: { playerId: 'P1', profileName: 'teejusb' } }, + password: '', + }, + }, + ); + const payload: UpdateMachinePayload = { + machine: { + player1: { playerId: 'P1', profileName: 'teejusb' }, + player2: { playerId: 'P2', profileName: 'Moistbruh' }, + }, + }; + await send(client, { + type: 'updateMachine', + payload, + }); + + const lobby = LOBBYMAN.lobbies[create.payload.code]; + const machine = Object.values(lobby.machines)[0]; + expect(machine).toEqual(payload.machine); + }); }); afterEach(() => { diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 59d20d4..0c7db58 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -4,7 +4,13 @@ import { WebSocketGateway, } from '@nestjs/websockets'; import { WebSocket } from 'ws'; -import { LOBBYMAN, LobbyInfo, ROOMMAN, SocketId } from '../types/models.types'; +import { + LOBBYMAN, + LobbyInfo, + PlayerId, + ROOMMAN, + SocketId, +} from '../types/models.types'; import { disconnectMachine, disconnectSpectator, @@ -22,10 +28,13 @@ import { LobbyLeftPayload, LobbySearchedPayload, LobbySpectatedPayload, + MachineUpdatedPayload, Message, MessageType, + ReadyUpPayload, ReadyUpResultPayload, SpectateLobbyPayload, + UpdateMachinePayload, } from './events.types'; import { ClientService } from '../clients/client.service'; @@ -54,6 +63,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { spectateLobby: this.spectateLobby, searchLobby: this.searchLobby, readyUp: this.readyUp, + updateMachine: this.updateMachine, }; } @@ -146,7 +156,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { [socketId]: { ...machine, socketId, - ready: false, + // ready: false, }, }, spectators: {}, @@ -184,10 +194,17 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } const lobby = LOBBYMAN.lobbies[code]; + if (Object.keys(lobby.machines).length >= 4) { + return { + type: 'lobbyJoined', + payload: { joined: false, message: 'Too many machines in lobby' }, + }; + } + lobby.machines[socketId] = { ...machine, socketId, - ready: false, + // ready: false, }; LOBBYMAN.machineConnections[socketId] = code; console.log('Machine ' + `${socketId}` + 'joined ' + `${code}`); @@ -195,6 +212,26 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { return { type: 'lobbyJoined', payload: { joined: true } }; } + /** + * Updates a machine + */ + async updateMachine( + socketId: SocketId, + { machine }: UpdateMachinePayload, + ): Promise> { + const code = LOBBYMAN.machineConnections[socketId]; + if (!code) { + return { + type: 'machineUpdated', + payload: { updated: false, message: 'Code not found' }, + }; + } + const lobby = LOBBYMAN.lobbies[code]; + lobby.machines[socketId] = machine; + + return { type: 'machineUpdated', payload: { updated: true } }; + } + /** * Removes a machine from a lobby. * @param client, The socket connection of the machine to disconnect. @@ -267,7 +304,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { */ async readyUp( socketId: SocketId, - {}, + { playerId }: ReadyUpPayload, ): Promise> { const response: Message = { type: 'readyUpResult', @@ -283,7 +320,13 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { return { ...response, payload: { ready: false } }; } - machine.ready = true; + if (machine.player1?.playerId === playerId) { + machine.player1.ready = true; + } + if (machine.player2?.playerId === playerId) { + machine.player2.ready = true; + } + const stateMessage = getLobbyState(socketId); if (stateMessage) { this.clients.sendLobby(stateMessage, lobby.code); @@ -291,7 +334,12 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { let allReady = true; for (const machine of Object.values(lobby.machines)) { - if (!machine.ready) { + const { player1, player2 } = machine; + if (player1 && !player1.ready) { + allReady = false; + break; + } + if (player2 && !player2.ready) { allReady = false; break; } diff --git a/src/events/events.types.ts b/src/events/events.types.ts index c6f792d..9d86843 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -2,6 +2,7 @@ import { LobbyCode, LobbyInfo, Machine, + PlayerId, Spectator, } from '../types/models.types'; @@ -10,6 +11,8 @@ export type MessageType = | 'lobbyCreated' | 'joinLobby' | 'lobbyJoined' + | 'updateMachine' + | 'machineUpdated' | 'leaveLobby' | 'lobbyLeft' | 'spectateLobby' @@ -19,7 +22,7 @@ export type MessageType = | 'clientDisconnected' | 'readyUp' | 'readyUpResult' - | 'lobbyState' + | 'sendLobby' | 'startSong'; export type MessagePayload = @@ -27,6 +30,7 @@ export type MessagePayload = | LobbyCreatedPayload | JoinLobbyPayload | LobbyJoinedPayload + | UpdateMachinePayload | LeaveLobbyPayload | LobbyLeftPayload | SearchLobbyPayload @@ -34,7 +38,7 @@ export type MessagePayload = | ClientDisconnectedPayload | ReadyUpPayload | ReadyUpResultPayload - | LobbyStatePayload + | SendLobbyPayload | StartSongPayload; export interface Message { @@ -52,13 +56,24 @@ export interface LobbyCreatedPayload { } export interface JoinLobbyPayload { - machine: Machine; + machine: Omit; code: LobbyCode; password: string; } +export interface UpdateMachinePayload { + machine: Omit; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MachineUpdatedPayload { + updated: boolean; + message?: string; +} + export interface LobbyJoinedPayload { joined: boolean; + message?: string; } export interface SpectateLobbyPayload { @@ -85,8 +100,9 @@ export interface LobbyLeftPayload { left: boolean; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ReadyUpPayload {} +export interface ReadyUpPayload { + playerId: PlayerId; +} export interface ReadyUpResultPayload { ready: boolean; @@ -96,7 +112,7 @@ export interface ClientDisconnectedPayload { reason: string; } -export interface LobbyStatePayload { +export interface SendLobbyPayload { machines: Machine[]; } diff --git a/src/events/utils.ts b/src/events/utils.ts index 7dd2dbd..cbd8987 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -162,5 +162,5 @@ export function getLobbyState(socketId: SocketId): Message | null { ...m, })); - return { type: 'lobbyState', payload: { machines } }; + return { type: 'sendLobby', payload: { machines } }; } diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 9828821..c98ddf4 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -2,6 +2,8 @@ export type SocketId = string; export type LobbyCode = string; +export type PlayerId = 'P1' | 'P2'; + export interface Judgments { fantasticPlus: number; fantastics: number; @@ -47,19 +49,31 @@ export interface SongInfo { } export interface Player { - playerId: string; + playerId: PlayerId; profileName: string; judgments?: Judgments; score?: number; exScore?: number; + + songProgression?: { + currentTime: number; + totalTime: number; + }; + + ready?: boolean; + screen?: + | 'screenSelectMusic' + | 'screenGameplay' + | 'screenPlayerOptions' + | 'screenEvaluation'; } export interface Machine { player1?: Player; player2?: Player; socketId?: SocketId; - ready?: boolean; + // ready?: boolean; } export interface Lobby { @@ -70,6 +84,9 @@ export interface Lobby { machines: Record; spectators: Record; + // song start ? all players ready ? + // state: "song_select" | "waiting_to_start" | "playing_song" | "waiting_results" + songInfo?: SongInfo; } @@ -82,7 +99,7 @@ export interface LobbyInfo { export class LOBBYMAN { // Mapping from lobby code to a Lobby - static lobbies: Record; + static lobbies: Record; // Mapping from socketId to the lobby code of the lobby it's connected to. static machineConnections: Record; From 91df72c9e817dd0971c75b19635ae60b93b0c746 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 16:09:09 -0800 Subject: [PATCH 18/32] Remove unused import --- src/events/events.gateway.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 0c7db58..0e71bed 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -4,13 +4,7 @@ import { WebSocketGateway, } from '@nestjs/websockets'; import { WebSocket } from 'ws'; -import { - LOBBYMAN, - LobbyInfo, - PlayerId, - ROOMMAN, - SocketId, -} from '../types/models.types'; +import { LOBBYMAN, LobbyInfo, ROOMMAN, SocketId } from '../types/models.types'; import { disconnectMachine, disconnectSpectator, From ecbb2bd9084fd5df0413e37b1d42d2a962440e24 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 16:33:37 -0800 Subject: [PATCH 19/32] Move towards some generic ResponseStatus messaging --- src/events/events.gateway.spec.ts | 4 +- src/events/events.gateway.ts | 64 ++++++++++++++++++++++--------- src/events/events.types.ts | 20 +++++++--- 3 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index 1f5fc6d..b86bbc7 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -9,7 +9,7 @@ import { LobbyLeftPayload, LobbySearchedPayload, LobbySpectatedPayload, - MachineUpdatedPayload, + ResponseStatus, Message, SearchLobbyPayload, SpectateLobbyPayload, @@ -169,7 +169,7 @@ describe('EventsGateway', () => { player2: { playerId: 'P2', profileName: 'Moistbruh' }, }, }; - await send(client, { + await send(client, { type: 'updateMachine', payload, }); diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 0e71bed..e28c30d 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -18,15 +18,13 @@ import { CreateLobbyPayload, JoinLobbyPayload, LobbyCreatedPayload, - LobbyJoinedPayload, LobbyLeftPayload, LobbySearchedPayload, LobbySpectatedPayload, - MachineUpdatedPayload, + ResponseStatus, Message, MessageType, ReadyUpPayload, - ReadyUpResultPayload, SpectateLobbyPayload, UpdateMachinePayload, } from './events.types'; @@ -173,9 +171,17 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async joinLobby( socketId: SocketId, { machine, code, password }: JoinLobbyPayload, - ): Promise> { + ): Promise> { if (!canJoinLobby(code, password)) { - return { type: 'lobbyJoined', payload: { joined: false } }; + return { + type: 'responseStatus', + payload: { + event: 'joinLobby', + success: false, + message: + 'Cannot join lobby. Check the code + password and try again.', + }, + }; } if (socketId in LOBBYMAN.spectatorConnections) { @@ -190,8 +196,12 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const lobby = LOBBYMAN.lobbies[code]; if (Object.keys(lobby.machines).length >= 4) { return { - type: 'lobbyJoined', - payload: { joined: false, message: 'Too many machines in lobby' }, + type: 'responseStatus', + payload: { + event: 'joinLobby', + success: false, + message: 'Too many machines in the lobby.', + }, }; } @@ -203,7 +213,13 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { LOBBYMAN.machineConnections[socketId] = code; console.log('Machine ' + `${socketId}` + 'joined ' + `${code}`); - return { type: 'lobbyJoined', payload: { joined: true } }; + return { + type: 'responseStatus', + payload: { + event: 'joinLobby', + success: true, + }, + }; } /** @@ -212,18 +228,25 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async updateMachine( socketId: SocketId, { machine }: UpdateMachinePayload, - ): Promise> { + ): Promise> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { return { - type: 'machineUpdated', - payload: { updated: false, message: 'Code not found' }, + type: 'responseStatus', + payload: { + event: 'updateMachine', + success: false, + message: 'Code not found', + }, }; } const lobby = LOBBYMAN.lobbies[code]; lobby.machines[socketId] = machine; - return { type: 'machineUpdated', payload: { updated: true } }; + return { + type: 'responseStatus', + payload: { event: 'updateMachine', success: true }, + }; } /** @@ -299,19 +322,21 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async readyUp( socketId: SocketId, { playerId }: ReadyUpPayload, - ): Promise> { - const response: Message = { - type: 'readyUpResult', - payload: { ready: false }, + ): Promise> { + const response: Message = { + type: 'responseStatus', + payload: { event: 'readyUp', success: false }, }; const lobby = getLobbyForMachine(socketId); if (lobby === undefined) { - return { ...response, payload: { ready: false } }; + response.payload.message = 'Lobby not found'; + return response; } const machine = lobby.machines[socketId]; if (machine === undefined) { - return { ...response, payload: { ready: false } }; + response.payload.message = 'Machine not found'; + return response; } if (machine.player1?.playerId === playerId) { @@ -345,6 +370,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { lobby.code, ); } - return { type: 'readyUpResult', payload: { ready: allReady } }; + response.payload.success = true; + return response; } } diff --git a/src/events/events.types.ts b/src/events/events.types.ts index 9d86843..d2969b1 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -3,6 +3,7 @@ import { LobbyInfo, Machine, PlayerId, + SongInfo, Spectator, } from '../types/models.types'; @@ -23,6 +24,8 @@ export type MessageType = | 'readyUp' | 'readyUpResult' | 'sendLobby' + | 'selectSong' + | 'responseStatus' | 'startSong'; export type MessagePayload = @@ -39,6 +42,7 @@ export type MessagePayload = | ReadyUpPayload | ReadyUpResultPayload | SendLobbyPayload + | SelectSongPayload | StartSongPayload; export interface Message { @@ -61,19 +65,23 @@ export interface JoinLobbyPayload { password: string; } +export interface LobbyJoinedPayload { + joined: boolean; + message?: string; +} + export interface UpdateMachinePayload { machine: Omit; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MachineUpdatedPayload { - updated: boolean; +export interface ResponseStatus { + event: MessageType; + success: boolean; message?: string; } -export interface LobbyJoinedPayload { - joined: boolean; - message?: string; +export interface SelectSongPayload { + songInfo: SongInfo; } export interface SpectateLobbyPayload { From 07124207948ea02ff784e179e6645ce98d9bdd93 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 16:42:57 -0800 Subject: [PATCH 20/32] Add response helpers --- src/events/events.gateway.ts | 82 ++++++++++++++++++++++++------------ 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index e28c30d..c1dc391 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -27,6 +27,7 @@ import { ReadyUpPayload, SpectateLobbyPayload, UpdateMachinePayload, + SelectSongPayload, } from './events.types'; import { ClientService } from '../clients/client.service'; @@ -195,14 +196,14 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const lobby = LOBBYMAN.lobbies[code]; if (Object.keys(lobby.machines).length >= 4) { - return { - type: 'responseStatus', - payload: { - event: 'joinLobby', - success: false, - message: 'Too many machines in the lobby.', - }, - }; + responseStatusFailure('joinLobby', 'Too many machines in the lobby'); + } + + if (lobby.songInfo) { + responseStatusFailure( + 'joinLobby', + 'A song is already selected, please try later.', + ); } lobby.machines[socketId] = { @@ -213,13 +214,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { LOBBYMAN.machineConnections[socketId] = code; console.log('Machine ' + `${socketId}` + 'joined ' + `${code}`); - return { - type: 'responseStatus', - payload: { - event: 'joinLobby', - success: true, - }, - }; + return responseStatusSuccess('joinLobby'); } /** @@ -231,22 +226,29 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { ): Promise> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { - return { - type: 'responseStatus', - payload: { - event: 'updateMachine', - success: false, - message: 'Code not found', - }, - }; + return responseStatusFailure('updateMachine', 'Machine not found'); } const lobby = LOBBYMAN.lobbies[code]; lobby.machines[socketId] = machine; - return { - type: 'responseStatus', - payload: { event: 'updateMachine', success: true }, - }; + return responseStatusSuccess('updateMachine'); + } + + async selectSong( + socketId: SocketId, + { songInfo }: SelectSongPayload, + ): Promise> { + const code = LOBBYMAN.machineConnections[socketId]; + if (!code) { + return responseStatusFailure('selectSong', 'Machine not found'); + } + const lobby = LOBBYMAN.lobbies[code]; + if (lobby.songInfo) { + return responseStatusFailure('selectSong', 'Song already selected'); + } + lobby.songInfo = songInfo; + + return responseStatusSuccess('selectSong'); } /** @@ -374,3 +376,29 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { return response; } } + +function responseStatus( + event: MessageType, + success: boolean, + message?: string, +): Message { + return { + type: 'responseStatus', + payload: { + event, + success, + message, + }, + }; +} + +function responseStatusSuccess(event: MessageType): Message { + return responseStatus(event, true); +} + +function responseStatusFailure( + event: MessageType, + message: string, +): Message { + return responseStatus(event, false, message); +} From 8a966f62613e926ee35e17cb3ce0ad007930a9fc Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 17:24:39 -0800 Subject: [PATCH 21/32] Add song select, machine updates --- src/events/events.gateway.spec.ts | 4 +- src/events/events.gateway.ts | 68 ++++++++++++++++++++++--------- src/events/events.types.ts | 13 +++--- src/events/utils.ts | 31 ++++++++++---- 4 files changed, 82 insertions(+), 34 deletions(-) diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index b86bbc7..fcd898d 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -9,7 +9,7 @@ import { LobbyLeftPayload, LobbySearchedPayload, LobbySpectatedPayload, - ResponseStatus, + ResponseStatusPayload, Message, SearchLobbyPayload, SpectateLobbyPayload, @@ -169,7 +169,7 @@ describe('EventsGateway', () => { player2: { playerId: 'P2', profileName: 'Moistbruh' }, }, }; - await send(client, { + await send(client, { type: 'updateMachine', payload, }); diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index c1dc391..70605a2 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -21,7 +21,7 @@ import { LobbyLeftPayload, LobbySearchedPayload, LobbySpectatedPayload, - ResponseStatus, + ResponseStatusPayload, Message, MessageType, ReadyUpPayload, @@ -57,6 +57,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { searchLobby: this.searchLobby, readyUp: this.readyUp, updateMachine: this.updateMachine, + lobbyState: this.lobbyState, + selectSong: this.selectSong, }; } @@ -69,7 +71,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { socket.on('message', async (messageBuffer: Buffer) => { try { const message: Message = JSON.parse(messageBuffer.toString()); - if (!message.type || !message.payload) { + if (!message.type) { throw new Error('Message requires a type and a payload'); } if (!this.handlers[message.type]) { @@ -172,7 +174,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async joinLobby( socketId: SocketId, { machine, code, password }: JoinLobbyPayload, - ): Promise> { + ): Promise> { if (!canJoinLobby(code, password)) { return { type: 'responseStatus', @@ -223,7 +225,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async updateMachine( socketId: SocketId, { machine }: UpdateMachinePayload, - ): Promise> { + ): Promise> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { return responseStatusFailure('updateMachine', 'Machine not found'); @@ -231,13 +233,37 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const lobby = LOBBYMAN.lobbies[code]; lobby.machines[socketId] = machine; + const stateMessage = getLobbyState(socketId); + console.log('Got state', stateMessage); + if (stateMessage) { + this.clients.sendLobby(stateMessage, lobby.code); + } + return responseStatusSuccess('updateMachine'); } + /** + * Updates a machine + */ + async lobbyState( + socketId: SocketId, + ): Promise | undefined> { + const code = LOBBYMAN.machineConnections[socketId]; + if (!code) { + return responseStatusFailure('lobbyState', 'Machine not found'); + } + const stateMessage = getLobbyState(socketId); + if (stateMessage) { + this.clients.sendLobby(stateMessage, code); + } + + return undefined; + } + async selectSong( socketId: SocketId, { songInfo }: SelectSongPayload, - ): Promise> { + ): Promise> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { return responseStatusFailure('selectSong', 'Machine not found'); @@ -319,26 +345,28 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { /** Updates the ready state of the machine. * @param client, The socket that connected. + * @deprecated use updateMachine, kill this * @returns, true if we successfully readied up, false otherwise. */ async readyUp( socketId: SocketId, { playerId }: ReadyUpPayload, - ): Promise> { - const response: Message = { - type: 'responseStatus', - payload: { event: 'readyUp', success: false }, - }; + ): Promise> { + if (!playerId) { + return responseStatusFailure('readyUp', 'Missing player id'); + } + const lobby = getLobbyForMachine(socketId); if (lobby === undefined) { - response.payload.message = 'Lobby not found'; - return response; + return responseStatusFailure('readyUp', 'Lobby not found'); + } + if (!lobby.songInfo) { + return responseStatusFailure('readyUp', 'No song selected'); } const machine = lobby.machines[socketId]; if (machine === undefined) { - response.payload.message = 'Machine not found'; - return response; + return responseStatusFailure('readyUp', 'Machine not found'); } if (machine.player1?.playerId === playerId) { @@ -349,6 +377,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } const stateMessage = getLobbyState(socketId); + if (stateMessage) { this.clients.sendLobby(stateMessage, lobby.code); } @@ -372,8 +401,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { lobby.code, ); } - response.payload.success = true; - return response; + return responseStatusSuccess('readyUp'); } } @@ -381,7 +409,7 @@ function responseStatus( event: MessageType, success: boolean, message?: string, -): Message { +): Message { return { type: 'responseStatus', payload: { @@ -392,13 +420,15 @@ function responseStatus( }; } -function responseStatusSuccess(event: MessageType): Message { +function responseStatusSuccess( + event: MessageType, +): Message { return responseStatus(event, true); } function responseStatusFailure( event: MessageType, message: string, -): Message { +): Message { return responseStatus(event, false, message); } diff --git a/src/events/events.types.ts b/src/events/events.types.ts index d2969b1..3451cf7 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -2,6 +2,7 @@ import { LobbyCode, LobbyInfo, Machine, + Player, PlayerId, SongInfo, Spectator, @@ -23,7 +24,7 @@ export type MessageType = | 'clientDisconnected' | 'readyUp' | 'readyUpResult' - | 'sendLobby' + | 'lobbyState' | 'selectSong' | 'responseStatus' | 'startSong'; @@ -41,7 +42,7 @@ export type MessagePayload = | ClientDisconnectedPayload | ReadyUpPayload | ReadyUpResultPayload - | SendLobbyPayload + | LobbyStatePayload | SelectSongPayload | StartSongPayload; @@ -74,7 +75,7 @@ export interface UpdateMachinePayload { machine: Omit; } -export interface ResponseStatus { +export interface ResponseStatusPayload { event: MessageType; success: boolean; message?: string; @@ -120,8 +121,10 @@ export interface ClientDisconnectedPayload { reason: string; } -export interface SendLobbyPayload { - machines: Machine[]; +export interface LobbyStatePayload { + players: Array; + code: LobbyCode; + songInfo?: SongInfo; } export interface StartSongPayload { diff --git a/src/events/utils.ts b/src/events/utils.ts index cbd8987..3f1bded 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -1,6 +1,12 @@ import { ClientService } from '../clients/client.service'; -import { LOBBYMAN, Lobby, ROOMMAN, SocketId } from '../types/models.types'; -import { Message } from './events.types'; +import { + LOBBYMAN, + Lobby, + Player, + ROOMMAN, + SocketId, +} from '../types/models.types'; +import { LobbyStatePayload, Message } from './events.types'; /** * Determines if the correct credentials are provided to join a lobby. @@ -150,17 +156,26 @@ export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { return LOBBYMAN.lobbies[code]; } -export function getLobbyState(socketId: SocketId): Message | null { +export function getLobbyState( + socketId: SocketId, +): Message | null { const lobby = getLobbyForMachine(socketId); if (lobby === undefined) { return null; } // Send back the machine state with the socket ids omitted - const machines = Object.entries(lobby.machines).map((m) => ({ - socketId, - ...m, - })); + const players: Player[] = []; + Object.values(lobby.machines).forEach((machine) => { + const { player1, player2 } = machine; + if (player1) { + players.push(player1); + } + if (player2) { + players.push(player2); + } + }); + const { songInfo, code } = lobby; - return { type: 'sendLobby', payload: { machines } }; + return { type: 'lobbyState', payload: { players, songInfo, code } }; } From 60ddccc057e17d92142a03845f2221b44559da8c Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 17:56:33 -0800 Subject: [PATCH 22/32] Remove redundant response messages --- src/events/events.gateway.spec.ts | 79 +++++++++++++++++++++++++++++-- src/events/events.gateway.ts | 35 ++++++++++---- src/types/models.types.ts | 15 +++--- 3 files changed, 105 insertions(+), 24 deletions(-) diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index fcd898d..96fbaeb 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -14,6 +14,7 @@ import { SearchLobbyPayload, SpectateLobbyPayload, UpdateMachinePayload, + SelectSongPayload, } from './events.types'; import { WebSocket } from 'ws'; import { WsAdapter } from '@nestjs/platform-ws'; @@ -59,12 +60,18 @@ describe('EventsGateway', () => { { type: 'createLobby', payload: { - machine: { player1: { playerId: 'P1', profileName: 'teejusb' } }, + machine: { + player1: { + playerId: 'P1', + profileName: 'teejusb', + screen: 'screenSelectMusic', + }, + }, password: '', }, }, ); - expect(create.type).toBe('lobbyCreated'); + expect(create.type).toBe('lobbyState'); expect(create.payload).toHaveProperty('code'); expect(typeof create.payload.code).toBe('string'); expect(create.payload.code.length).toBe(4); @@ -158,15 +165,29 @@ describe('EventsGateway', () => { { type: 'createLobby', payload: { - machine: { player1: { playerId: 'P1', profileName: 'teejusb' } }, + machine: { + player1: { + playerId: 'P1', + profileName: 'teejusb', + screen: 'screenSelectMusic', + }, + }, password: '', }, }, ); const payload: UpdateMachinePayload = { machine: { - player1: { playerId: 'P1', profileName: 'teejusb' }, - player2: { playerId: 'P2', profileName: 'Moistbruh' }, + player1: { + playerId: 'P1', + profileName: 'teejusb', + screen: 'screenGameplay', + }, + player2: { + playerId: 'P2', + profileName: 'Moistbruh', + screen: 'screenGameplay', + }, }, }; await send(client, { @@ -180,6 +201,54 @@ describe('EventsGateway', () => { }); }); + it('selectSong', async () => { + const create = await send(client, { + type: 'createLobby', + payload: { + machine: { + player1: { + playerId: 'P1', + profileName: 'teejusb', + screen: 'screenSelectMusic', + }, + }, + password: '', + }, + }); + + // Initially no song + const lobby = LOBBYMAN.lobbies[create.payload.code]; + expect(lobby.songInfo).toBeUndefined(); + + const payload: SelectSongPayload = { + songInfo: { + songPath: '11 guys/wowie', + songLength: 42069, + title: 'WOWIE', + artist: 'the guys', + }, + }; + + // First song sets song info + await send(client, { + type: 'selectSong', + payload, + }); + expect(lobby.songInfo).toEqual(payload.songInfo); + + // Second one will fail + payload.songInfo.title = 'Updated'; + const second = await send( + client, + { + type: 'selectSong', + payload, + }, + ); + expect(second.payload.success).toBe(false); + expect(lobby.songInfo?.title).toEqual('WOWIE'); + }); + afterEach(() => { client.close(); }); diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 70605a2..3f1149d 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -129,7 +129,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async createLobby( socketId: string, { machine, password }: CreateLobbyPayload, - ): Promise> { + ): Promise | undefined> { if (socketId in LOBBYMAN.spectatorConnections) { disconnectSpectator(socketId); } @@ -160,7 +160,12 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { LOBBYMAN.machineConnections[socketId] = code; console.log('Created lobby ' + code); - return { type: 'lobbyCreated', payload: { code } as LobbyCreatedPayload }; + const stateMessage = getLobbyState(socketId); + if (stateMessage) { + this.clients.sendLobby(stateMessage, code); + } + return undefined; + // return { type: 'lobbyCreated', payload: { code } as LobbyCreatedPayload }; } /** @@ -174,7 +179,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async joinLobby( socketId: SocketId, { machine, code, password }: JoinLobbyPayload, - ): Promise> { + ): Promise | undefined> { if (!canJoinLobby(code, password)) { return { type: 'responseStatus', @@ -211,12 +216,16 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { lobby.machines[socketId] = { ...machine, socketId, - // ready: false, }; LOBBYMAN.machineConnections[socketId] = code; console.log('Machine ' + `${socketId}` + 'joined ' + `${code}`); - return responseStatusSuccess('joinLobby'); + const stateMessage = getLobbyState(socketId); + if (stateMessage) { + this.clients.sendLobby(stateMessage, lobby.code); + } + return undefined; + // return responseStatusSuccess('joinLobby'); } /** @@ -225,7 +234,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async updateMachine( socketId: SocketId, { machine }: UpdateMachinePayload, - ): Promise> { + ): Promise | undefined> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { return responseStatusFailure('updateMachine', 'Machine not found'); @@ -234,12 +243,12 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { lobby.machines[socketId] = machine; const stateMessage = getLobbyState(socketId); - console.log('Got state', stateMessage); if (stateMessage) { this.clients.sendLobby(stateMessage, lobby.code); } - return responseStatusSuccess('updateMachine'); + return undefined; + // return responseStatusSuccess('updateMachine'); } /** @@ -263,7 +272,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async selectSong( socketId: SocketId, { songInfo }: SelectSongPayload, - ): Promise> { + ): Promise | undefined> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { return responseStatusFailure('selectSong', 'Machine not found'); @@ -274,7 +283,13 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } lobby.songInfo = songInfo; - return responseStatusSuccess('selectSong'); + const stateMessage = getLobbyState(socketId); + if (stateMessage) { + this.clients.sendLobby(stateMessage, code); + } + + return undefined; + // return responseStatusSuccess('selectSong'); } /** diff --git a/src/types/models.types.ts b/src/types/models.types.ts index c98ddf4..77af493 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -44,29 +44,26 @@ export interface SongInfo { songPath: string; title: string; artist: string; - stepartist: string; songLength: number; } export interface Player { playerId: PlayerId; profileName: string; + screen: + | 'screenSelectMusic' + | 'screenGameplay' + | 'screenPlayerOptions' + | 'screenEvaluation'; judgments?: Judgments; score?: number; exScore?: number; - + ready?: boolean; songProgression?: { currentTime: number; totalTime: number; }; - - ready?: boolean; - screen?: - | 'screenSelectMusic' - | 'screenGameplay' - | 'screenPlayerOptions' - | 'screenEvaluation'; } export interface Machine { From 635912d895f3bdcdc1f403ce40b7bfbc3b33a031 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 18:08:40 -0800 Subject: [PATCH 23/32] Rename to event and data --- src/clients/client.service.ts | 4 +- src/events/events.gateway.spec.ts | 102 +++++++++++++++--------------- src/events/events.gateway.ts | 32 +++++----- src/events/events.types.ts | 4 +- src/events/utils.ts | 2 +- 5 files changed, 72 insertions(+), 72 deletions(-) diff --git a/src/clients/client.service.ts b/src/clients/client.service.ts index 69b4ead..62d812e 100644 --- a/src/clients/client.service.ts +++ b/src/clients/client.service.ts @@ -53,8 +53,8 @@ export class ClientService { } const message: Message = { - type: 'clientDisconnected', - payload: { reason: reason || 'Just because' }, + event: 'clientDisconnected', + data: { reason: reason || 'Just because' }, }; const client = this.clients[socketId]; diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index 96fbaeb..f135029 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -58,8 +58,8 @@ describe('EventsGateway', () => { const create = await send( client, { - type: 'createLobby', - payload: { + event: 'createLobby', + data: { machine: { player1: { playerId: 'P1', @@ -71,39 +71,39 @@ describe('EventsGateway', () => { }, }, ); - expect(create.type).toBe('lobbyState'); - expect(create.payload).toHaveProperty('code'); - expect(typeof create.payload.code).toBe('string'); - expect(create.payload.code.length).toBe(4); + expect(create.event).toBe('lobbyState'); + expect(create.data).toHaveProperty('code'); + expect(typeof create.data.code).toBe('string'); + expect(create.data.code.length).toBe(4); const search = await send( client, { - type: 'searchLobby', - payload: {}, + event: 'searchLobby', + data: {}, }, ); - expect(search.type).toBe('lobbySearched'); - expect(search.payload.lobbies.length).toBe(1); - expect(search.payload.lobbies[0].code).toBe(create.payload.code); - expect(search.payload.lobbies[0].playerCount).toBe(1); - expect(search.payload.lobbies[0].spectatorCount).toBe(0); + expect(search.event).toBe('lobbySearched'); + expect(search.data.lobbies.length).toBe(1); + expect(search.data.lobbies[0].code).toBe(create.data.code); + expect(search.data.lobbies[0].playerCount).toBe(1); + expect(search.data.lobbies[0].spectatorCount).toBe(0); const spectate = await send( client, { - type: 'spectateLobby', - payload: { + event: 'spectateLobby', + data: { spectator: { profileName: 'E.Norma', }, - code: search.payload.lobbies[0].code, + code: search.data.lobbies[0].code, password: '', }, }, ); - expect(spectate.type).toBe('lobbySpectated'); - expect(spectate.payload.spectators).toBe(0); // Spectate should fail as a player can't also be a spectator. + expect(spectate.event).toBe('lobbySpectated'); + expect(spectate.data.spectators).toBe(0); // Spectate should fail as a player can't also be a spectator. const client2 = new WebSocket('ws://localhost:' + port); await new Promise((resolve) => { @@ -113,48 +113,48 @@ describe('EventsGateway', () => { const spectate2 = await send( client2, { - type: 'spectateLobby', - payload: { + event: 'spectateLobby', + data: { spectator: { profileName: 'Brat', }, - code: search.payload.lobbies[0].code, + code: search.data.lobbies[0].code, password: '', }, }, ); - expect(spectate2.type).toBe('lobbySpectated'); - expect(spectate2.payload.spectators).toBe(1); // socket2 is a different connection, so we can spectate now. + expect(spectate2.event).toBe('lobbySpectated'); + expect(spectate2.data.spectators).toBe(1); // socket2 is a different connection, so we can spectate now. const search2 = await send( client, { - type: 'searchLobby', - payload: {}, + event: 'searchLobby', + data: {}, }, ); - expect(search2.type).toBe('lobbySearched'); - expect(search2.payload.lobbies.length).toBe(1); - expect(search2.payload.lobbies[0].code).toBe(create.payload.code); - expect(search2.payload.lobbies[0].playerCount).toBe(1); - expect(search2.payload.lobbies[0].spectatorCount).toBe(1); + expect(search2.event).toBe('lobbySearched'); + expect(search2.data.lobbies.length).toBe(1); + expect(search2.data.lobbies[0].code).toBe(create.data.code); + expect(search2.data.lobbies[0].playerCount).toBe(1); + expect(search2.data.lobbies[0].spectatorCount).toBe(1); const leave = await send(client, { - type: 'leaveLobby', - payload: {}, + event: 'leaveLobby', + data: {}, }); - expect(leave.type).toBe('lobbyLeft'); - expect(leave.payload.left).toBeTruthy(); + expect(leave.event).toBe('lobbyLeft'); + expect(leave.data.left).toBeTruthy(); const search3 = await send( client, { - type: 'searchLobby', - payload: {}, + event: 'searchLobby', + data: {}, }, ); - expect(search3.type).toBe('lobbySearched'); - expect(search3.payload.lobbies.length).toBe(0); + expect(search3.event).toBe('lobbySearched'); + expect(search3.data.lobbies.length).toBe(0); client2.close(); }); @@ -163,8 +163,8 @@ describe('EventsGateway', () => { const create = await send( client, { - type: 'createLobby', - payload: { + event: 'createLobby', + data: { machine: { player1: { playerId: 'P1', @@ -191,11 +191,11 @@ describe('EventsGateway', () => { }, }; await send(client, { - type: 'updateMachine', - payload, + event: 'updateMachine', + data: payload, }); - const lobby = LOBBYMAN.lobbies[create.payload.code]; + const lobby = LOBBYMAN.lobbies[create.data.code]; const machine = Object.values(lobby.machines)[0]; expect(machine).toEqual(payload.machine); }); @@ -203,8 +203,8 @@ describe('EventsGateway', () => { it('selectSong', async () => { const create = await send(client, { - type: 'createLobby', - payload: { + event: 'createLobby', + data: { machine: { player1: { playerId: 'P1', @@ -217,7 +217,7 @@ describe('EventsGateway', () => { }); // Initially no song - const lobby = LOBBYMAN.lobbies[create.payload.code]; + const lobby = LOBBYMAN.lobbies[create.data.code]; expect(lobby.songInfo).toBeUndefined(); const payload: SelectSongPayload = { @@ -231,8 +231,8 @@ describe('EventsGateway', () => { // First song sets song info await send(client, { - type: 'selectSong', - payload, + event: 'selectSong', + data: payload, }); expect(lobby.songInfo).toEqual(payload.songInfo); @@ -241,11 +241,11 @@ describe('EventsGateway', () => { const second = await send( client, { - type: 'selectSong', - payload, + event: 'selectSong', + data: payload, }, ); - expect(second.payload.success).toBe(false); + expect(second.data.success).toBe(false); expect(lobby.songInfo?.title).toEqual('WOWIE'); }); diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 3f1149d..3e49424 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -71,19 +71,19 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { socket.on('message', async (messageBuffer: Buffer) => { try { const message: Message = JSON.parse(messageBuffer.toString()); - if (!message.type) { - throw new Error('Message requires a type and a payload'); + if (!message.event) { + throw new Error('Message requires an event'); } - if (!this.handlers[message.type]) { - throw new Error(`No handler for message type "${message.type}"`); + if (!this.handlers[message.event]) { + throw new Error(`No handler for message type "${message.event}"`); } - const handler = this.handlers[message.type]; + const handler = this.handlers[message.event]; if (!handler) { throw new Error('Missing handler'); // Should not happen, but makes TS happy } // Retain "this" context within the handler callbacks (otherwise we lose this.clients) const handlerBinded = handler.bind(this); - const response = await handlerBinded(socketId, message.payload); + const response = await handlerBinded(socketId, message.data); if (response) { this.clients.sendSocket(response, socketId); } @@ -182,8 +182,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { ): Promise | undefined> { if (!canJoinLobby(code, password)) { return { - type: 'responseStatus', - payload: { + event: 'responseStatus', + data: { event: 'joinLobby', success: false, message: @@ -300,7 +300,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async leaveLobby(socketId: SocketId, {}): Promise> { let left = false; left = disconnectMachine(socketId, this.clients); - return { type: 'lobbyLeft', payload: { left } }; + return { event: 'lobbyLeft', data: { left } }; } /** @@ -318,7 +318,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const lobby = LOBBYMAN.lobbies[code]; if (!lobby) { - return { type: 'lobbySpectated', payload: { spectators: 0 } }; + return { event: 'lobbySpectated', data: { spectators: 0 } }; } if ( @@ -338,8 +338,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { LOBBYMAN.spectatorConnections[socketId] = code; } return { - type: 'lobbySpectated', - payload: { spectators: Object.keys(lobby.spectators).length }, + event: 'lobbySpectated', + data: { spectators: Object.keys(lobby.spectators).length }, }; } @@ -355,7 +355,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { spectatorCount: Object.keys(l.spectators).length, })); console.log('Found ' + lobbies.length + ' lobbies'); - return { type: 'lobbySearched', payload: { lobbies } }; + return { event: 'lobbySearched', data: { lobbies } }; } /** Updates the ready state of the machine. @@ -412,7 +412,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (allReady) { this.clients.sendLobby( - { type: 'startSong', payload: { start: true } }, + { event: 'startSong', data: { start: true } }, lobby.code, ); } @@ -426,8 +426,8 @@ function responseStatus( message?: string, ): Message { return { - type: 'responseStatus', - payload: { + event: 'responseStatus', + data: { event, success, message, diff --git a/src/events/events.types.ts b/src/events/events.types.ts index 3451cf7..2c113bb 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -47,8 +47,8 @@ export type MessagePayload = | StartSongPayload; export interface Message { - type: MessageType; - payload: T; + event: MessageType; + data: T; } export interface CreateLobbyPayload { diff --git a/src/events/utils.ts b/src/events/utils.ts index 3f1bded..820df82 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -177,5 +177,5 @@ export function getLobbyState( }); const { songInfo, code } = lobby; - return { type: 'lobbyState', payload: { players, songInfo, code } }; + return { event: 'lobbyState', data: { players, songInfo, code } }; } From b75ee7382df35b586cd4a23f4fabe3de66ab3f85 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 18:12:34 -0800 Subject: [PATCH 24/32] Log failures --- src/events/events.gateway.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 3e49424..123f58a 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -72,7 +72,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { try { const message: Message = JSON.parse(messageBuffer.toString()); if (!message.event) { - throw new Error('Message requires an event'); + console.log('No event, ignoring: ', JSON.stringify(message, null, 2)); + return; } if (!this.handlers[message.event]) { throw new Error(`No handler for message type "${message.event}"`); From ce6679cd565a712d9c43ff08699682a53e0b4065 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 18:30:46 -0800 Subject: [PATCH 25/32] Log the messages --- src/events/events.gateway.ts | 3 ++- src/events/events.types.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 123f58a..0700a2a 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -71,8 +71,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { socket.on('message', async (messageBuffer: Buffer) => { try { const message: Message = JSON.parse(messageBuffer.toString()); + console.log('Received message:', JSON.stringify(message, null, 2)); if (!message.event) { - console.log('No event, ignoring: ', JSON.stringify(message, null, 2)); + console.log('No event, ignoring'); return; } if (!this.handlers[message.event]) { diff --git a/src/events/events.types.ts b/src/events/events.types.ts index 2c113bb..8e40029 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -52,7 +52,7 @@ export interface Message { } export interface CreateLobbyPayload { - machine: Machine; + machine: Omit; password: string; } From 93daffe060275c9c9f5a47c696e794b95021737a Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 19:36:04 -0800 Subject: [PATCH 26/32] Clear song info --- src/clients/client.service.ts | 10 +- src/events/events.gateway.spec.ts | 73 +++++++-------- src/events/events.gateway.ts | 149 +++++++++++------------------- src/events/events.types.ts | 14 +-- src/events/utils.ts | 4 +- 5 files changed, 101 insertions(+), 149 deletions(-) diff --git a/src/clients/client.service.ts b/src/clients/client.service.ts index 62d812e..e42ca97 100644 --- a/src/clients/client.service.ts +++ b/src/clients/client.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import WebSocket = require('ws'); -import { Message } from '../events/events.types'; +import { EventMessage } from '../events/events.types'; import { SocketId, LobbyCode, ROOMMAN } from '../types/models.types'; import { v4 as uuid } from 'uuid'; @Injectable() @@ -16,7 +16,7 @@ export class ClientService { } /** Sends a message to all connected clients */ - sendAll(response: Message) { + sendAll(response: EventMessage) { for (const client of Object.values(this.clients)) { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(response)); @@ -25,7 +25,7 @@ export class ClientService { } /** Sends a message to a specific socket */ - sendSocket(response: Message, socketId: SocketId) { + sendSocket(response: EventMessage, socketId: SocketId) { const socket = this.clients[socketId]; if (!socket || socket.readyState !== WebSocket.OPEN) { console.warn('Cannot send to socket, socket is not connected'); @@ -35,7 +35,7 @@ export class ClientService { } /** Sends a message to all clients in a particular lobby */ - sendLobby(response: Message, code: LobbyCode) { + sendLobby(response: EventMessage, code: LobbyCode) { for (const [socketId, socket] of Object.entries(this.clients)) { // skip clients not in the lobby if (!ROOMMAN.isJoined(socketId, code)) return; @@ -52,7 +52,7 @@ export class ClientService { return; } - const message: Message = { + const message: EventMessage = { event: 'clientDisconnected', data: { reason: reason || 'Just because' }, }; diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index f135029..f7a313d 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -3,14 +3,13 @@ import { Test } from '@nestjs/testing'; import { AppModule } from '../app.module'; import { LOBBYMAN } from '../types/models.types'; import { - CreateLobbyPayload, + CreateLobbyData, LeaveLobbyPayload, - LobbyCreatedPayload, LobbyLeftPayload, LobbySearchedPayload, LobbySpectatedPayload, ResponseStatusPayload, - Message, + EventMessage, SearchLobbyPayload, SpectateLobbyPayload, UpdateMachinePayload, @@ -55,26 +54,21 @@ describe('EventsGateway', () => { it('createLobby', async () => { console.log('Create Lobby'); - const create = await send( - client, - { - event: 'createLobby', - data: { - machine: { - player1: { - playerId: 'P1', - profileName: 'teejusb', - screen: 'screenSelectMusic', - }, + const create = await send(client, { + event: 'createLobby', + data: { + machine: { + player1: { + playerId: 'P1', + profileName: 'teejusb', + screen: 'screenSelectMusic', }, - password: '', }, + password: '', }, - ); + }); expect(create.event).toBe('lobbyState'); expect(create.data).toHaveProperty('code'); - expect(typeof create.data.code).toBe('string'); - expect(create.data.code.length).toBe(4); const search = await send( client, @@ -85,7 +79,9 @@ describe('EventsGateway', () => { ); expect(search.event).toBe('lobbySearched'); expect(search.data.lobbies.length).toBe(1); - expect(search.data.lobbies[0].code).toBe(create.data.code); + const code = Object.values(LOBBYMAN.lobbies)[0].code; + + expect(search.data.lobbies[0].code).toBe(code); expect(search.data.lobbies[0].playerCount).toBe(1); expect(search.data.lobbies[0].spectatorCount).toBe(0); @@ -135,7 +131,7 @@ describe('EventsGateway', () => { ); expect(search2.event).toBe('lobbySearched'); expect(search2.data.lobbies.length).toBe(1); - expect(search2.data.lobbies[0].code).toBe(create.data.code); + expect(search2.data.lobbies[0].code).toBe(code); expect(search2.data.lobbies[0].playerCount).toBe(1); expect(search2.data.lobbies[0].spectatorCount).toBe(1); @@ -160,22 +156,19 @@ describe('EventsGateway', () => { }); it('updateMachine', async () => { - const create = await send( - client, - { - event: 'createLobby', - data: { - machine: { - player1: { - playerId: 'P1', - profileName: 'teejusb', - screen: 'screenSelectMusic', - }, + await send(client, { + event: 'createLobby', + data: { + machine: { + player1: { + playerId: 'P1', + profileName: 'teejusb', + screen: 'screenSelectMusic', }, - password: '', }, + password: '', }, - ); + }); const payload: UpdateMachinePayload = { machine: { player1: { @@ -194,15 +187,16 @@ describe('EventsGateway', () => { event: 'updateMachine', data: payload, }); + const code = LOBBYMAN.lobbies[0].code; - const lobby = LOBBYMAN.lobbies[create.data.code]; + const lobby = LOBBYMAN.lobbies[code]; const machine = Object.values(lobby.machines)[0]; expect(machine).toEqual(payload.machine); }); }); it('selectSong', async () => { - const create = await send(client, { + const create = await send(client, { event: 'createLobby', data: { machine: { @@ -215,9 +209,10 @@ describe('EventsGateway', () => { password: '', }, }); + const code = Object.values(LOBBYMAN.lobbies)[0].code; // Initially no song - const lobby = LOBBYMAN.lobbies[create.data.code]; + const lobby = LOBBYMAN.lobbies[code]; expect(lobby.songInfo).toBeUndefined(); const payload: SelectSongPayload = { @@ -260,10 +255,10 @@ describe('EventsGateway', () => { function send( client: WebSocket, - message: Message, -): Promise> { + message: EventMessage, +): Promise> { return new Promise((resolve) => { - client.on('message', (response: Message) => { + client.on('message', (response: EventMessage) => { resolve(JSON.parse(response.toString())); }); client.send(JSON.stringify(message)); diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 0700a2a..c23b39f 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -4,27 +4,30 @@ import { WebSocketGateway, } from '@nestjs/websockets'; import { WebSocket } from 'ws'; -import { LOBBYMAN, LobbyInfo, ROOMMAN, SocketId } from '../types/models.types'; +import { + LOBBYMAN, + Lobby, + LobbyInfo, + ROOMMAN, + SocketId, +} from '../types/models.types'; import { disconnectMachine, disconnectSpectator, canJoinLobby, generateLobbyCode, getPlayerCountForLobby, - getLobbyForMachine, getLobbyState, } from './utils'; import { - CreateLobbyPayload, + CreateLobbyData, JoinLobbyPayload, - LobbyCreatedPayload, LobbyLeftPayload, LobbySearchedPayload, LobbySpectatedPayload, ResponseStatusPayload, - Message, - MessageType, - ReadyUpPayload, + EventMessage, + MessageType as EventType, SpectateLobbyPayload, UpdateMachinePayload, SelectSongPayload, @@ -41,8 +44,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * The callback function may return a message to send to the calling socket */ private handlers: Partial< Record< - MessageType, - (socketId: SocketId, payload: any) => Promise + EventType, + (socketId: SocketId, payload: any) => Promise > >; @@ -55,7 +58,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { leaveLobby: this.leaveLobby, spectateLobby: this.spectateLobby, searchLobby: this.searchLobby, - readyUp: this.readyUp, updateMachine: this.updateMachine, lobbyState: this.lobbyState, selectSong: this.selectSong, @@ -70,7 +72,15 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { socket.on('message', async (messageBuffer: Buffer) => { try { - const message: Message = JSON.parse(messageBuffer.toString()); + const messageString = messageBuffer.toString(); + let message: EventMessage; + try { + message = JSON.parse(messageString); + } catch (e) { + console.error('Error parsing message', messageString); + return; + } + console.log('Received message:', JSON.stringify(message, null, 2)); if (!message.event) { console.log('No event, ignoring'); @@ -130,8 +140,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { */ async createLobby( socketId: string, - { machine, password }: CreateLobbyPayload, - ): Promise | undefined> { + { machine, password }: CreateLobbyData, + ): Promise { if (socketId in LOBBYMAN.spectatorConnections) { disconnectSpectator(socketId); } @@ -153,7 +163,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { [socketId]: { ...machine, socketId, - // ready: false, }, }, spectators: {}, @@ -167,7 +176,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { this.clients.sendLobby(stateMessage, code); } return undefined; - // return { type: 'lobbyCreated', payload: { code } as LobbyCreatedPayload }; } /** @@ -181,7 +189,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async joinLobby( socketId: SocketId, { machine, code, password }: JoinLobbyPayload, - ): Promise | undefined> { + ): Promise | undefined> { if (!canJoinLobby(code, password)) { return { event: 'responseStatus', @@ -236,13 +244,21 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async updateMachine( socketId: SocketId, { machine }: UpdateMachinePayload, - ): Promise | undefined> { + ): Promise | undefined> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { return responseStatusFailure('updateMachine', 'Machine not found'); } const lobby = LOBBYMAN.lobbies[code]; + + const inSongSelectBefore = inSongSelect(lobby); lobby.machines[socketId] = machine; + const inSongSelectAfter = inSongSelect(lobby); + + if (!inSongSelectBefore && inSongSelectAfter) { + lobby.songInfo = undefined; + console.log('!!!! CLEARING SONG INFO !!!!'); + } const stateMessage = getLobbyState(socketId); if (stateMessage) { @@ -258,7 +274,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { */ async lobbyState( socketId: SocketId, - ): Promise | undefined> { + ): Promise | undefined> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { return responseStatusFailure('lobbyState', 'Machine not found'); @@ -274,7 +290,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async selectSong( socketId: SocketId, { songInfo }: SelectSongPayload, - ): Promise | undefined> { + ): Promise | undefined> { const code = LOBBYMAN.machineConnections[socketId]; if (!code) { return responseStatusFailure('selectSong', 'Machine not found'); @@ -299,7 +315,10 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * @param client, The socket connection of the machine to disconnect. * @returns, True if the machine was disconnected successfully. */ - async leaveLobby(socketId: SocketId, {}): Promise> { + async leaveLobby( + socketId: SocketId, + {}, + ): Promise> { let left = false; left = disconnectMachine(socketId, this.clients); return { event: 'lobbyLeft', data: { left } }; @@ -316,7 +335,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async spectateLobby( socketId: SocketId, { spectator, code, password }: SpectateLobbyPayload, - ): Promise> { + ): Promise> { const lobby = LOBBYMAN.lobbies[code]; if (!lobby) { @@ -349,7 +368,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { * Searches for all active lobbies. * @returns, The list of lobbies that are currently active. */ - async searchLobby(): Promise> { + async searchLobby(): Promise> { const lobbies: LobbyInfo[] = Object.values(LOBBYMAN.lobbies).map((l) => ({ code: l.code, isPasswordProtected: l.password.length !== 0, @@ -359,74 +378,13 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { console.log('Found ' + lobbies.length + ' lobbies'); return { event: 'lobbySearched', data: { lobbies } }; } - - /** Updates the ready state of the machine. - * @param client, The socket that connected. - * @deprecated use updateMachine, kill this - * @returns, true if we successfully readied up, false otherwise. - */ - async readyUp( - socketId: SocketId, - { playerId }: ReadyUpPayload, - ): Promise> { - if (!playerId) { - return responseStatusFailure('readyUp', 'Missing player id'); - } - - const lobby = getLobbyForMachine(socketId); - if (lobby === undefined) { - return responseStatusFailure('readyUp', 'Lobby not found'); - } - if (!lobby.songInfo) { - return responseStatusFailure('readyUp', 'No song selected'); - } - - const machine = lobby.machines[socketId]; - if (machine === undefined) { - return responseStatusFailure('readyUp', 'Machine not found'); - } - - if (machine.player1?.playerId === playerId) { - machine.player1.ready = true; - } - if (machine.player2?.playerId === playerId) { - machine.player2.ready = true; - } - - const stateMessage = getLobbyState(socketId); - - if (stateMessage) { - this.clients.sendLobby(stateMessage, lobby.code); - } - - let allReady = true; - for (const machine of Object.values(lobby.machines)) { - const { player1, player2 } = machine; - if (player1 && !player1.ready) { - allReady = false; - break; - } - if (player2 && !player2.ready) { - allReady = false; - break; - } - } - - if (allReady) { - this.clients.sendLobby( - { event: 'startSong', data: { start: true } }, - lobby.code, - ); - } - return responseStatusSuccess('readyUp'); - } } function responseStatus( - event: MessageType, + event: EventType, success: boolean, message?: string, -): Message { +): EventMessage { return { event: 'responseStatus', data: { @@ -437,15 +395,20 @@ function responseStatus( }; } -function responseStatusSuccess( - event: MessageType, -): Message { - return responseStatus(event, true); -} - function responseStatusFailure( - event: MessageType, + event: EventType, message: string, -): Message { +): EventMessage { return responseStatus(event, false, message); } + +function inSongSelect(lobby: Lobby): boolean { + let selecting = true; + Object.values(lobby.machines).forEach((machine) => { + if (machine.player1?.screen === 'screenSelectMusic') { + selecting = false; + return; + } + }); + return selecting; +} diff --git a/src/events/events.types.ts b/src/events/events.types.ts index 8e40029..bfce460 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -10,7 +10,6 @@ import { export type MessageType = | 'createLobby' - | 'lobbyCreated' | 'joinLobby' | 'lobbyJoined' | 'updateMachine' @@ -29,9 +28,8 @@ export type MessageType = | 'responseStatus' | 'startSong'; -export type MessagePayload = - | CreateLobbyPayload - | LobbyCreatedPayload +export type EventData = + | CreateLobbyData | JoinLobbyPayload | LobbyJoinedPayload | UpdateMachinePayload @@ -46,20 +44,16 @@ export type MessagePayload = | SelectSongPayload | StartSongPayload; -export interface Message { +export interface EventMessage { event: MessageType; data: T; } -export interface CreateLobbyPayload { +export interface CreateLobbyData { machine: Omit; password: string; } -export interface LobbyCreatedPayload { - code: LobbyCode; -} - export interface JoinLobbyPayload { machine: Omit; code: LobbyCode; diff --git a/src/events/utils.ts b/src/events/utils.ts index 820df82..543d745 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -6,7 +6,7 @@ import { ROOMMAN, SocketId, } from '../types/models.types'; -import { LobbyStatePayload, Message } from './events.types'; +import { LobbyStatePayload, EventMessage } from './events.types'; /** * Determines if the correct credentials are provided to join a lobby. @@ -158,7 +158,7 @@ export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { export function getLobbyState( socketId: SocketId, -): Message | null { +): EventMessage | null { const lobby = getLobbyForMachine(socketId); if (lobby === undefined) { return null; From 2878bd4053c47414f4552e15a98a5918ed24879b Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 19:37:11 -0800 Subject: [PATCH 27/32] Look at p1 and p2 --- src/events/events.gateway.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index c23b39f..120b14e 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -409,6 +409,10 @@ function inSongSelect(lobby: Lobby): boolean { selecting = false; return; } + if (machine.player2?.screen === 'screenSelectMusic') { + selecting = false; + return; + } }); return selecting; } From e66e628c1ca1dea4ab46e5c2491e4741c52e5863 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 23:01:18 -0800 Subject: [PATCH 28/32] Correctly merge machine updates --- package-lock.json | 20 +++++++++++++++++--- package.json | 4 +++- src/clients/client.service.ts | 2 ++ src/events/events.gateway.ts | 25 ++++++++++++++++++++++--- src/events/utils.ts | 19 ++++++++++++++++++- src/types/models.types.ts | 12 ++++++------ 6 files changed, 68 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index b52e7a0..ec6a3ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@nestjs/platform-express": "^10.3.7", "@nestjs/platform-ws": "^10.3.7", "@nestjs/websockets": "^10.3.7", + "lodash": "^4.17.21", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", @@ -26,6 +27,7 @@ "@nestjs/testing": "^10.3.7", "@types/express": "^4.17.13", "@types/jest": "28.1.8", + "@types/lodash": "^4.17.13", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@types/ws": "^8.5.12", @@ -2011,6 +2013,13 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -6476,7 +6485,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "license": "MIT" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -10951,6 +10960,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -14255,8 +14270,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.memoize": { "version": "4.1.2", diff --git a/package.json b/package.json index 30da317..d32fd2b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@nestjs/platform-express": "^10.3.7", "@nestjs/platform-ws": "^10.3.7", "@nestjs/websockets": "^10.3.7", + "lodash": "^4.17.21", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", @@ -44,6 +45,7 @@ "@nestjs/testing": "^10.3.7", "@types/express": "^4.17.13", "@types/jest": "28.1.8", + "@types/lodash": "^4.17.13", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@types/ws": "^8.5.12", @@ -80,4 +82,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} diff --git a/src/clients/client.service.ts b/src/clients/client.service.ts index e42ca97..271de56 100644 --- a/src/clients/client.service.ts +++ b/src/clients/client.service.ts @@ -47,6 +47,7 @@ export class ClientService { } disconnect(socketId: SocketId, reason?: string) { + console.log('Client disconnecting', socketId); if (!this.clients[socketId]) { console.warn(`Client ${socketId} not connected`); return; @@ -68,6 +69,7 @@ export class ClientService { connect(socket: WebSocket): string { // Assert we're not already connected + console.log('Client connecting'); const entry = Object.entries(this.clients).find( ([, value]) => socket === value, ); diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 120b14e..190a2b9 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -32,6 +32,8 @@ import { UpdateMachinePayload, SelectSongPayload, } from './events.types'; +import { merge } from 'lodash'; + import { ClientService } from '../clients/client.service'; @WebSocketGateway({ @@ -96,6 +98,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { // Retain "this" context within the handler callbacks (otherwise we lose this.clients) const handlerBinded = handler.bind(this); const response = await handlerBinded(socketId, message.data); + console.log('Sending response', JSON.stringify(response, null, 2)); if (response) { this.clients.sendSocket(response, socketId); } @@ -227,6 +230,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { ...machine, socketId, }; + ROOMMAN.join(socketId, code); LOBBYMAN.machineConnections[socketId] = code; console.log('Machine ' + `${socketId}` + 'joined ' + `${code}`); @@ -252,11 +256,25 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const lobby = LOBBYMAN.lobbies[code]; const inSongSelectBefore = inSongSelect(lobby); - lobby.machines[socketId] = machine; + console.log('In song select before?', inSongSelectBefore); + // lobby.machines[socketId] = { ...lobby.machines[socketId], ...machine }; + merge(lobby.machines[socketId], machine); const inSongSelectAfter = inSongSelect(lobby); + console.log('In song select after?', inSongSelectAfter); if (!inSongSelectBefore && inSongSelectAfter) { lobby.songInfo = undefined; + Object.values(lobby.machines).forEach((machine) => { + // Only retain relevant fields + if (machine.player1) { + const { playerId, profileName, screenName } = machine.player1; + machine.player1 = { playerId, profileName, screenName }; + } + if (machine.player2) { + const { playerId, profileName, screenName } = machine.player2; + machine.player2 = { playerId, profileName, screenName }; + } + }); console.log('!!!! CLEARING SONG INFO !!!!'); } @@ -405,11 +423,12 @@ function responseStatusFailure( function inSongSelect(lobby: Lobby): boolean { let selecting = true; Object.values(lobby.machines).forEach((machine) => { - if (machine.player1?.screen === 'screenSelectMusic') { + if (machine.player1 && machine.player1.screenName !== 'ScreenSelectMusic') { + console.log(machine.player1.profileName, ' ', machine.player1.screenName); selecting = false; return; } - if (machine.player2?.screen === 'screenSelectMusic') { + if (machine.player2 && machine.player2.screenName !== 'ScreenSelectMusic') { selecting = false; return; } diff --git a/src/events/utils.ts b/src/events/utils.ts index 543d745..3ae9056 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -2,6 +2,7 @@ import { ClientService } from '../clients/client.service'; import { LOBBYMAN, Lobby, + LobbyCode, Player, ROOMMAN, SocketId, @@ -93,6 +94,7 @@ export function disconnectMachine( } ROOMMAN.leave(machine.socketId, code); + // Don't disconnect here, as we may be re-using the connection. // In the case of `leaveLobby`, the client can manually disconnect. } @@ -110,6 +112,12 @@ export function disconnectMachine( } } delete LOBBYMAN.lobbies[code]; + } else { + // When a client disconnects, notify other clients + const stateMessage = getLobbyStateForCode(code); + if (stateMessage) { + clients.sendLobby(stateMessage, code); + } } return true; } @@ -149,6 +157,9 @@ export function disconnectSpectator(socketId: SocketId): boolean { */ export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { const code = LOBBYMAN.machineConnections[socketId]; + + console.log('Getting lobby state', code, socketId); + if (code === undefined) { return undefined; } @@ -163,9 +174,15 @@ export function getLobbyState( if (lobby === undefined) { return null; } + return getLobbyStateForCode(lobby.code); +} +export function getLobbyStateForCode( + code: LobbyCode, +): EventMessage | null { // Send back the machine state with the socket ids omitted const players: Player[] = []; + const lobby = LOBBYMAN.lobbies[code]; Object.values(lobby.machines).forEach((machine) => { const { player1, player2 } = machine; if (player1) { @@ -175,7 +192,7 @@ export function getLobbyState( players.push(player2); } }); - const { songInfo, code } = lobby; + const { songInfo } = lobby; return { event: 'lobbyState', data: { players, songInfo, code } }; } diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 77af493..21fdaa4 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -50,11 +50,11 @@ export interface SongInfo { export interface Player { playerId: PlayerId; profileName: string; - screen: - | 'screenSelectMusic' - | 'screenGameplay' - | 'screenPlayerOptions' - | 'screenEvaluation'; + screenName: + | 'ScreenSelectMusic' + | 'ScreenGameplay' + | 'ScreenPlayerOptions' + | 'ScreenEvaluation'; judgments?: Judgments; score?: number; @@ -107,7 +107,7 @@ export class LOBBYMAN { export class ROOMMAN { // Mapping of lobby ids (rooms) to the socketIds in that room - private static rooms: Record> = {}; + public static rooms: Record> = {}; static join(socketId: SocketId, code: LobbyCode) { if (!this.rooms[code]) { From 78eb4b2934c08afba5bfe10c9e9041b60820a309 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sat, 7 Dec 2024 23:15:28 -0800 Subject: [PATCH 29/32] Clean up some logging --- src/events/events.gateway.spec.ts | 10 +++++----- src/events/events.gateway.ts | 7 +------ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index f7a313d..7db29fa 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -61,7 +61,7 @@ describe('EventsGateway', () => { player1: { playerId: 'P1', profileName: 'teejusb', - screen: 'screenSelectMusic', + screenName: 'ScreenSelectMusic', }, }, password: '', @@ -163,7 +163,7 @@ describe('EventsGateway', () => { player1: { playerId: 'P1', profileName: 'teejusb', - screen: 'screenSelectMusic', + screenName: 'ScreenSelectMusic', }, }, password: '', @@ -174,12 +174,12 @@ describe('EventsGateway', () => { player1: { playerId: 'P1', profileName: 'teejusb', - screen: 'screenGameplay', + screenName: 'ScreenGameplay', }, player2: { playerId: 'P2', profileName: 'Moistbruh', - screen: 'screenGameplay', + screenName: 'ScreenGameplay', }, }, }; @@ -203,7 +203,7 @@ describe('EventsGateway', () => { player1: { playerId: 'P1', profileName: 'teejusb', - screen: 'screenSelectMusic', + screenName: 'ScreenSelectMusic', }, }, password: '', diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 190a2b9..83fe079 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -98,8 +98,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { // Retain "this" context within the handler callbacks (otherwise we lose this.clients) const handlerBinded = handler.bind(this); const response = await handlerBinded(socketId, message.data); - console.log('Sending response', JSON.stringify(response, null, 2)); if (response) { + console.log('Sending response', JSON.stringify(response, null, 2)); this.clients.sendSocket(response, socketId); } } catch (e) { @@ -256,11 +256,8 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const lobby = LOBBYMAN.lobbies[code]; const inSongSelectBefore = inSongSelect(lobby); - console.log('In song select before?', inSongSelectBefore); - // lobby.machines[socketId] = { ...lobby.machines[socketId], ...machine }; merge(lobby.machines[socketId], machine); const inSongSelectAfter = inSongSelect(lobby); - console.log('In song select after?', inSongSelectAfter); if (!inSongSelectBefore && inSongSelectAfter) { lobby.songInfo = undefined; @@ -275,7 +272,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { machine.player2 = { playerId, profileName, screenName }; } }); - console.log('!!!! CLEARING SONG INFO !!!!'); } const stateMessage = getLobbyState(socketId); @@ -424,7 +420,6 @@ function inSongSelect(lobby: Lobby): boolean { let selecting = true; Object.values(lobby.machines).forEach((machine) => { if (machine.player1 && machine.player1.screenName !== 'ScreenSelectMusic') { - console.log(machine.player1.profileName, ' ', machine.player1.screenName); selecting = false; return; } From 026fd951b65a6dbeca97e2127eb018a9f7f77529 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Sun, 8 Dec 2024 09:22:11 -0800 Subject: [PATCH 30/32] Make ready mandatory. Add new screen state. --- src/types/models.types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 21fdaa4..3e8acf4 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -51,15 +51,16 @@ export interface Player { playerId: PlayerId; profileName: string; screenName: + | 'NoScreen' | 'ScreenSelectMusic' | 'ScreenGameplay' | 'ScreenPlayerOptions' | 'ScreenEvaluation'; + ready: boolean; judgments?: Judgments; score?: number; exScore?: number; - ready?: boolean; songProgression?: { currentTime: number; totalTime: number; From 4c1889d23d224b26b0d74a49a2362a09e3530ad9 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Mon, 9 Dec 2024 07:01:41 -0800 Subject: [PATCH 31/32] Tidy up the machine state broadcasts, update tests --- src/events/events.gateway.spec.ts | 85 +++++++++++++-- src/events/events.gateway.ts | 176 ++++++++++++++++++------------ src/events/events.types.ts | 6 +- src/events/utils.ts | 134 +++++++++-------------- src/types/models.types.ts | 4 - 5 files changed, 231 insertions(+), 174 deletions(-) diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index 7db29fa..0331481 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -17,6 +17,7 @@ import { } from './events.types'; import { WebSocket } from 'ws'; import { WsAdapter } from '@nestjs/platform-ws'; +import { omit } from 'lodash'; const port = 3001; @@ -62,6 +63,7 @@ describe('EventsGateway', () => { playerId: 'P1', profileName: 'teejusb', screenName: 'ScreenSelectMusic', + ready: false, }, }, password: '', @@ -79,7 +81,7 @@ describe('EventsGateway', () => { ); expect(search.event).toBe('lobbySearched'); expect(search.data.lobbies.length).toBe(1); - const code = Object.values(LOBBYMAN.lobbies)[0].code; + const code = Object.keys(LOBBYMAN.lobbies)[0]; expect(search.data.lobbies[0].code).toBe(code); expect(search.data.lobbies[0].playerCount).toBe(1); @@ -164,39 +166,102 @@ describe('EventsGateway', () => { playerId: 'P1', profileName: 'teejusb', screenName: 'ScreenSelectMusic', + ready: false, }, }, password: '', }, }); - const payload: UpdateMachinePayload = { + const update1: UpdateMachinePayload = { machine: { player1: { playerId: 'P1', profileName: 'teejusb', screenName: 'ScreenGameplay', + ready: false, + score: 0.99, + songProgression: { + currentTime: 1, + totalTime: 2, + }, }, player2: { playerId: 'P2', profileName: 'Moistbruh', screenName: 'ScreenGameplay', + ready: false, + score: 0.99, }, }, }; await send(client, { event: 'updateMachine', - data: payload, + data: update1, }); - const code = LOBBYMAN.lobbies[0].code; - - const lobby = LOBBYMAN.lobbies[code]; + const lobby = Object.values(LOBBYMAN.lobbies)[0]; const machine = Object.values(lobby.machines)[0]; - expect(machine).toEqual(payload.machine); + expect(omit(machine, 'socketId')).toEqual(update1.machine); + + // If one player goes back to SongSelect (I know, technically not possible for a single machine) + // The songInfo/scores should persist + const update2: UpdateMachinePayload = { + machine: { + player1: { + playerId: 'P1', + profileName: 'teejusb', + screenName: 'ScreenSelectMusic', + ready: false, + }, + player2: { + playerId: 'P2', + profileName: 'Moistbruh', + screenName: 'ScreenGameplay', + ready: false, + score: 0.99, + }, + }, + }; + await send(client, { + event: 'updateMachine', + data: update2, + }); + + expect(machine.player1).toBeDefined(); + expect(machine.player1?.screenName).toEqual('ScreenSelectMusic'); + expect(machine.player1?.score).toBeDefined(); + expect(machine.player1?.songProgression).toBeDefined(); + + // Now we go back to select music, it should wipe songInfo/scores + const update3: UpdateMachinePayload = { + machine: { + player1: { + playerId: 'P1', + profileName: 'teejusb', + screenName: 'ScreenSelectMusic', + ready: false, + }, + player2: { + playerId: 'P2', + profileName: 'Moistbruh', + screenName: 'ScreenSelectMusic', + ready: false, + }, + }, + }; + await send(client, { + event: 'updateMachine', + data: update3, + }); + + expect(machine.player1).toBeDefined(); + expect(machine.player1?.screenName).toEqual('ScreenSelectMusic'); + expect(machine.player1?.score).toBeUndefined(); + expect(machine.player1?.songProgression).toBeUndefined(); }); }); it('selectSong', async () => { - const create = await send(client, { + await send(client, { event: 'createLobby', data: { machine: { @@ -204,15 +269,15 @@ describe('EventsGateway', () => { playerId: 'P1', profileName: 'teejusb', screenName: 'ScreenSelectMusic', + ready: false, }, }, password: '', }, }); - const code = Object.values(LOBBYMAN.lobbies)[0].code; // Initially no song - const lobby = LOBBYMAN.lobbies[code]; + const [, lobby] = Object.entries(LOBBYMAN.lobbies)[0]; expect(lobby.songInfo).toBeUndefined(); const payload: SelectSongPayload = { diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 83fe079..641db66 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -6,18 +6,20 @@ import { import { WebSocket } from 'ws'; import { LOBBYMAN, - Lobby, + LobbyCode, LobbyInfo, + Player, ROOMMAN, SocketId, } from '../types/models.types'; import { - disconnectMachine, disconnectSpectator, canJoinLobby, generateLobbyCode, getPlayerCountForLobby, - getLobbyState, + RETAINED_PLAYER_KEYS, + inSongSelect, + responseStatusFailure, } from './utils'; import { CreateLobbyData, @@ -27,12 +29,13 @@ import { LobbySpectatedPayload, ResponseStatusPayload, EventMessage, - MessageType as EventType, + EventType, SpectateLobbyPayload, UpdateMachinePayload, SelectSongPayload, + LobbyStatePayload, } from './events.types'; -import { merge } from 'lodash'; +import { merge, pick } from 'lodash'; import { ClientService } from '../clients/client.service'; @@ -126,7 +129,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { this.clients.disconnect(socketId); if (socketId in LOBBYMAN.machineConnections) { - disconnectMachine(socketId, this.clients); + this.disconnectMachine(socketId); } if (socketId in LOBBYMAN.spectatorConnections) { @@ -151,7 +154,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (socketId in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - disconnectMachine(socketId, this.clients); + this.disconnectMachine(socketId); } let code = generateLobbyCode(); @@ -174,10 +177,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { LOBBYMAN.machineConnections[socketId] = code; console.log('Created lobby ' + code); - const stateMessage = getLobbyState(socketId); - if (stateMessage) { - this.clients.sendLobby(stateMessage, code); - } + this.broadcastLobbyState(code); return undefined; } @@ -211,7 +211,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (socketId in LOBBYMAN.machineConnections) { // A machine can only join one lobby at a time. - disconnectMachine(socketId, this.clients); + this.disconnectMachine(socketId); } const lobby = LOBBYMAN.lobbies[code]; @@ -234,12 +234,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { LOBBYMAN.machineConnections[socketId] = code; console.log('Machine ' + `${socketId}` + 'joined ' + `${code}`); - const stateMessage = getLobbyState(socketId); - if (stateMessage) { - this.clients.sendLobby(stateMessage, lobby.code); - } + this.broadcastLobbyState(code); + return undefined; - // return responseStatusSuccess('joinLobby'); } /** @@ -255,32 +252,28 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } const lobby = LOBBYMAN.lobbies[code]; - const inSongSelectBefore = inSongSelect(lobby); + // Merge the incoming machine data with the respective lobby's machine + const playersInSongSelectBefore = inSongSelect(lobby); merge(lobby.machines[socketId], machine); - const inSongSelectAfter = inSongSelect(lobby); + const playersInSongSelectAfter = inSongSelect(lobby); - if (!inSongSelectBefore && inSongSelectAfter) { + // If all players have transitioned back to song select, + // Ensure the scores and currently-selected song get reset + if (!playersInSongSelectBefore && playersInSongSelectAfter) { lobby.songInfo = undefined; Object.values(lobby.machines).forEach((machine) => { // Only retain relevant fields if (machine.player1) { - const { playerId, profileName, screenName } = machine.player1; - machine.player1 = { playerId, profileName, screenName }; + machine.player1 = pick(machine.player1, RETAINED_PLAYER_KEYS); } if (machine.player2) { - const { playerId, profileName, screenName } = machine.player2; - machine.player2 = { playerId, profileName, screenName }; + machine.player2 = pick(machine.player2, RETAINED_PLAYER_KEYS); } }); } - const stateMessage = getLobbyState(socketId); - if (stateMessage) { - this.clients.sendLobby(stateMessage, lobby.code); - } - + this.broadcastLobbyState(lobby.code); return undefined; - // return responseStatusSuccess('updateMachine'); } /** @@ -293,10 +286,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { if (!code) { return responseStatusFailure('lobbyState', 'Machine not found'); } - const stateMessage = getLobbyState(socketId); - if (stateMessage) { - this.clients.sendLobby(stateMessage, code); - } + this.broadcastLobbyState(code); return undefined; } @@ -315,13 +305,9 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { } lobby.songInfo = songInfo; - const stateMessage = getLobbyState(socketId); - if (stateMessage) { - this.clients.sendLobby(stateMessage, code); - } + this.broadcastLobbyState(code); return undefined; - // return responseStatusSuccess('selectSong'); } /** @@ -334,7 +320,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { {}, ): Promise> { let left = false; - left = disconnectMachine(socketId, this.clients); + left = this.disconnectMachine(socketId); return { event: 'lobbyLeft', data: { left } }; } @@ -392,41 +378,87 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { console.log('Found ' + lobbies.length + ' lobbies'); return { event: 'lobbySearched', data: { lobbies } }; } -} -function responseStatus( - event: EventType, - success: boolean, - message?: string, -): EventMessage { - return { - event: 'responseStatus', - data: { - event, - success, - message, - }, - }; -} + private broadcastLobbyState(code: LobbyCode) { + const lobby = this.getLobbyState(code); + if (lobby) { + this.clients.sendLobby(lobby, code); + } + } -function responseStatusFailure( - event: EventType, - message: string, -): EventMessage { - return responseStatus(event, false, message); -} + private getLobbyState( + code: LobbyCode, + ): EventMessage | null { + // Send back the machine state with the socket ids omitted + const players: Player[] = []; + const lobby = LOBBYMAN.lobbies[code]; + Object.values(lobby.machines).forEach((machine) => { + const { player1, player2 } = machine; + if (player1) { + players.push(player1); + } + if (player2) { + players.push(player2); + } + }); + const { songInfo } = lobby; -function inSongSelect(lobby: Lobby): boolean { - let selecting = true; - Object.values(lobby.machines).forEach((machine) => { - if (machine.player1 && machine.player1.screenName !== 'ScreenSelectMusic') { - selecting = false; - return; + return { event: 'lobbyState', data: { players, songInfo, code } }; + } + + /** + * Makes a machine leave a lobby. If the machine was the last player in the + * lobby, the lobby will be deleted and all spectators will be disconnected. + * @param socketId, The socket ID of the machine to disconnect. + * @returns True if the machine left the lobby, false otherwise. + */ + private disconnectMachine(socketId: SocketId): boolean { + const code = LOBBYMAN.machineConnections[socketId]; + if (code === undefined) { + return false; } - if (machine.player2 && machine.player2.screenName !== 'ScreenSelectMusic') { - selecting = false; - return; + + const lobby = LOBBYMAN.lobbies[code]; + if (lobby === undefined) { + return false; + } + + const machine = lobby.machines[socketId]; + if (machine === undefined) { + return false; + } + + if (machine.socketId) { + if (machine.socketId in LOBBYMAN.machineConnections) { + delete LOBBYMAN.machineConnections[machine.socketId]; + } + + ROOMMAN.leave(machine.socketId, code); + + // Don't disconnect here, as we may be re-using the connection. + // In the case of `leaveLobby`, the client can manually disconnect. } - }); - return selecting; + delete lobby.machines[socketId]; + delete LOBBYMAN.machineConnections[socketId]; + + if (getPlayerCountForLobby(lobby) === 0) { + for (const spectator of Object.values(lobby.spectators)) { + if (spectator.socketId) { + ROOMMAN.leave(spectator.socketId, code); + // Force a disconnect. If there are no more players in the lobby, + // we should remove the spectators as well. + this.clients.disconnect(spectator.socketId); + delete LOBBYMAN.spectatorConnections[spectator.socketId]; + } + } + delete LOBBYMAN.lobbies[code]; + } else { + // When a client disconnects, notify other clients + const stateMessage = this.getLobbyState(code); + if (stateMessage) { + this.clients.sendLobby(stateMessage, code); + } + } + return true; + } } diff --git a/src/events/events.types.ts b/src/events/events.types.ts index bfce460..a7c37ad 100644 --- a/src/events/events.types.ts +++ b/src/events/events.types.ts @@ -8,7 +8,7 @@ import { Spectator, } from '../types/models.types'; -export type MessageType = +export type EventType = | 'createLobby' | 'joinLobby' | 'lobbyJoined' @@ -45,7 +45,7 @@ export type EventData = | StartSongPayload; export interface EventMessage { - event: MessageType; + event: EventType; data: T; } @@ -70,7 +70,7 @@ export interface UpdateMachinePayload { } export interface ResponseStatusPayload { - event: MessageType; + event: EventType; success: boolean; message?: string; } diff --git a/src/events/utils.ts b/src/events/utils.ts index 3ae9056..8763933 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -1,13 +1,20 @@ -import { ClientService } from '../clients/client.service'; import { LOBBYMAN, Lobby, - LobbyCode, Player, ROOMMAN, SocketId, } from '../types/models.types'; -import { LobbyStatePayload, EventMessage } from './events.types'; +import { EventMessage, EventType, ResponseStatusPayload } from './events.types'; + +/** Keys retained when the game state is reset. + * @see updateMachine */ +export const RETAINED_PLAYER_KEYS: Array = [ + 'playerId', + 'profileName', + 'screenName', + 'ready', +]; /** * Determines if the correct credentials are provided to join a lobby. @@ -63,65 +70,6 @@ export function getPlayerCountForLobby(lobby: Lobby): number { return playerCount; } -/** - * Makes a machine leave a lobby. If the machine was the last player in the - * lobby, the lobby will be deleted and all spectators will be disconnected. - * @param socketId, The socket ID of the machine to disconnect. - * @returns True if the machine left the lobby, false otherwise. - */ -export function disconnectMachine( - socketId: SocketId, - clients: ClientService, -): boolean { - const code = LOBBYMAN.machineConnections[socketId]; - if (code === undefined) { - return false; - } - - const lobby = LOBBYMAN.lobbies[code]; - if (lobby === undefined) { - return false; - } - - const machine = lobby.machines[socketId]; - if (machine === undefined) { - return false; - } - - if (machine.socketId) { - if (machine.socketId in LOBBYMAN.machineConnections) { - delete LOBBYMAN.machineConnections[machine.socketId]; - } - - ROOMMAN.leave(machine.socketId, code); - - // Don't disconnect here, as we may be re-using the connection. - // In the case of `leaveLobby`, the client can manually disconnect. - } - delete lobby.machines[socketId]; - delete LOBBYMAN.machineConnections[socketId]; - - if (getPlayerCountForLobby(lobby) === 0) { - for (const spectator of Object.values(lobby.spectators)) { - if (spectator.socketId) { - ROOMMAN.leave(spectator.socketId, code); - // Force a disconnect. If there are no more players in the lobby, - // we should remove the spectators as well. - clients.disconnect(spectator.socketId); - delete LOBBYMAN.spectatorConnections[spectator.socketId]; - } - } - delete LOBBYMAN.lobbies[code]; - } else { - // When a client disconnects, notify other clients - const stateMessage = getLobbyStateForCode(code); - if (stateMessage) { - clients.sendLobby(stateMessage, code); - } - } - return true; -} - /** * Makes a spectator leave a lobby. * @param socketId, The socket ID of the spectator to disconnect. @@ -167,32 +115,48 @@ export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { return LOBBYMAN.lobbies[code]; } -export function getLobbyState( - socketId: SocketId, -): EventMessage | null { - const lobby = getLobbyForMachine(socketId); - if (lobby === undefined) { - return null; - } - return getLobbyStateForCode(lobby.code); +/** + * Constructs a ResponseStatus event with a success or fail. + */ +export function responseStatus( + event: EventType, + success: boolean, + message?: string, +): EventMessage { + return { + event: 'responseStatus', + data: { + event, + success, + message, + }, + }; } -export function getLobbyStateForCode( - code: LobbyCode, -): EventMessage | null { - // Send back the machine state with the socket ids omitted - const players: Player[] = []; - const lobby = LOBBYMAN.lobbies[code]; - Object.values(lobby.machines).forEach((machine) => { - const { player1, player2 } = machine; - if (player1) { - players.push(player1); +/** + * Constructs a Response status event with a failure. + * @param event + * @param message + * @returns + */ +export function responseStatusFailure( + event: EventType, + message: string, +): EventMessage { + return responseStatus(event, false, message); +} + +export function inSongSelect(lobby: Lobby): boolean { + let selecting = true; + Object.values(lobby.machines).forEach(({ player1, player2 }) => { + if (player1 && player1.screenName !== 'ScreenSelectMusic') { + selecting = false; + return; } - if (player2) { - players.push(player2); + if (player2 && player2.screenName !== 'ScreenSelectMusic') { + selecting = false; + return; } }); - const { songInfo } = lobby; - - return { event: 'lobbyState', data: { players, songInfo, code } }; + return selecting; } diff --git a/src/types/models.types.ts b/src/types/models.types.ts index 3e8acf4..0b7a6be 100644 --- a/src/types/models.types.ts +++ b/src/types/models.types.ts @@ -71,7 +71,6 @@ export interface Machine { player1?: Player; player2?: Player; socketId?: SocketId; - // ready?: boolean; } export interface Lobby { @@ -82,9 +81,6 @@ export interface Lobby { machines: Record; spectators: Record; - // song start ? all players ready ? - // state: "song_select" | "waiting_to_start" | "playing_song" | "waiting_results" - songInfo?: SongInfo; } From dc601a02edf74f7964d7ce907fff15e1f4abc713 Mon Sep 17 00:00:00 2001 From: Andrew Keturi Date: Thu, 12 Dec 2024 22:21:35 -0800 Subject: [PATCH 32/32] Remove room if the last player left, remove extra logging --- src/clients/client.service.ts | 1 - src/events/events.gateway.spec.ts | 2 - src/events/events.gateway.ts | 68 +++++++++++++++++-------------- src/events/utils.ts | 5 +-- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/clients/client.service.ts b/src/clients/client.service.ts index 271de56..daa32b3 100644 --- a/src/clients/client.service.ts +++ b/src/clients/client.service.ts @@ -81,7 +81,6 @@ export class ClientService { // Generate an id for the entry, set and return it const socketId = uuid(); this.clients[socketId] = socket; - console.log('Socket connected: ', socketId); return socketId; } } diff --git a/src/events/events.gateway.spec.ts b/src/events/events.gateway.spec.ts index 0331481..b3aaf8a 100644 --- a/src/events/events.gateway.spec.ts +++ b/src/events/events.gateway.spec.ts @@ -53,8 +53,6 @@ describe('EventsGateway', () => { describe('generalLobbyUsage', () => { it('createLobby', async () => { - console.log('Create Lobby'); - const create = await send(client, { event: 'createLobby', data: { diff --git a/src/events/events.gateway.ts b/src/events/events.gateway.ts index 641db66..c390cc9 100644 --- a/src/events/events.gateway.ts +++ b/src/events/events.gateway.ts @@ -26,7 +26,6 @@ import { JoinLobbyPayload, LobbyLeftPayload, LobbySearchedPayload, - LobbySpectatedPayload, ResponseStatusPayload, EventMessage, EventType, @@ -86,9 +85,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { return; } - console.log('Received message:', JSON.stringify(message, null, 2)); if (!message.event) { - console.log('No event, ignoring'); return; } if (!this.handlers[message.event]) { @@ -102,7 +99,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { const handlerBinded = handler.bind(this); const response = await handlerBinded(socketId, message.data); if (response) { - console.log('Sending response', JSON.stringify(response, null, 2)); this.clients.sendSocket(response, socketId); } } catch (e) { @@ -173,9 +169,10 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { }, spectators: {}, }; + console.log('Created lobby', { code }); + ROOMMAN.join(socketId, code); LOBBYMAN.machineConnections[socketId] = code; - console.log('Created lobby ' + code); this.broadcastLobbyState(code); return undefined; @@ -232,7 +229,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { }; ROOMMAN.join(socketId, code); LOBBYMAN.machineConnections[socketId] = code; - console.log('Machine ' + `${socketId}` + 'joined ' + `${code}`); this.broadcastLobbyState(code); @@ -335,33 +331,31 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { async spectateLobby( socketId: SocketId, { spectator, code, password }: SpectateLobbyPayload, - ): Promise> { - const lobby = LOBBYMAN.lobbies[code]; - - if (!lobby) { - return { event: 'lobbySpectated', data: { spectators: 0 } }; + ): Promise | undefined> { + if (!canJoinLobby(code, password)) { + return responseStatusFailure( + 'spectateLobby', + 'No lobby found with code ' + code, + ); } - if ( - !(socketId in LOBBYMAN.machineConnections) && - canJoinLobby(code, password) - ) { - if (socketId in LOBBYMAN.spectatorConnections) { - // A spectator can only spectate one lobby at a time. - disconnectSpectator(socketId); - } - - lobby.spectators[socketId] = { - ...spectator, - socketId, - }; - ROOMMAN.join(socketId, code); - LOBBYMAN.spectatorConnections[socketId] = code; + if (socketId in LOBBYMAN.spectatorConnections) { + // A spectator can only spectate one lobby at a time. + disconnectSpectator(socketId); } - return { - event: 'lobbySpectated', - data: { spectators: Object.keys(lobby.spectators).length }, + const lobby = LOBBYMAN.lobbies[code.toUpperCase()]; + lobby.spectators[socketId] = { + ...spectator, + socketId, }; + ROOMMAN.join(socketId, code); + LOBBYMAN.spectatorConnections[socketId] = code; + + // Broadcasts an updated spectator count to all machines + // and the initial lobby state for the newly-added spectator + this.broadcastLobbyState(code); + + return undefined; } /** @@ -375,7 +369,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { playerCount: getPlayerCountForLobby(l), spectatorCount: Object.keys(l.spectators).length, })); - console.log('Found ' + lobbies.length + ' lobbies'); return { event: 'lobbySearched', data: { lobbies } }; } @@ -403,7 +396,19 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { }); const { songInfo } = lobby; - return { event: 'lobbyState', data: { players, songInfo, code } }; + return { + event: 'lobbyState', + data: { + players: players.sort((p1, p2) => { + if (p1.exScore && p2.exScore) { + return p2.exScore - p1.exScore; + } + return p1.profileName > p2.profileName ? 1 : -1; + }), + songInfo, + code, + }, + }; } /** @@ -451,6 +456,7 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { delete LOBBYMAN.spectatorConnections[spectator.socketId]; } } + delete ROOMMAN.rooms[code]; delete LOBBYMAN.lobbies[code]; } else { // When a client disconnects, notify other clients diff --git a/src/events/utils.ts b/src/events/utils.ts index 8763933..8f5c5ff 100644 --- a/src/events/utils.ts +++ b/src/events/utils.ts @@ -25,7 +25,7 @@ export const RETAINED_PLAYER_KEYS: Array = [ */ export function canJoinLobby(code: string, password: string) { // Does the lobby we're trying to join exist? - const lobby = LOBBYMAN.lobbies[code]; + const lobby = LOBBYMAN.lobbies[code.toUpperCase()]; if (lobby === undefined) { return false; } @@ -105,9 +105,6 @@ export function disconnectSpectator(socketId: SocketId): boolean { */ export function getLobbyForMachine(socketId: SocketId): Lobby | undefined { const code = LOBBYMAN.machineConnections[socketId]; - - console.log('Getting lobby state', code, socketId); - if (code === undefined) { return undefined; }