From 63cffa3bcf7c2f30ad57e01f81b0b9e39670442d Mon Sep 17 00:00:00 2001 From: Conor Date: Wed, 27 Dec 2023 02:07:03 -0500 Subject: [PATCH] * add types to Socket.io Server and Socket --- server/src/index.ts | 6 +-- server/src/routes/gameHandlers.ts | 27 +++++----- server/src/routes/registerHandlers.ts | 4 +- server/src/routes/roomHandlers.ts | 17 +++--- server/src/state/gameState.ts | 30 +---------- server/src/state/pollService.ts | 3 +- server/src/types/socketServerTypes.ts | 69 ++++++++++++++++++++++++ server/src/types/stateTypes.ts | 43 +++++++++++++++ server/tests/models/autoMatch.ts | 3 +- server/tests/models/completeCallbacks.ts | 10 ++-- server/tests/models/selectionAccepts.ts | 3 +- server/tests/models/sikeDispute.ts | 3 +- 12 files changed, 157 insertions(+), 61 deletions(-) create mode 100644 server/src/types/stateTypes.ts diff --git a/server/src/index.ts b/server/src/index.ts index c66ab2a..941209f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -46,8 +46,8 @@ const server = http.createServer(app); * Create socket server */ -import { Server, Socket } from 'socket.io'; -const io = new Server(server, { +import { TypedServer, TypedSocket } from './types/socketServerTypes'; +const io = new TypedServer(server, { cors: { origin: ['http://localhost:8080', 'http://localhost:5001'], methods: ['GET', 'POST'], @@ -60,7 +60,7 @@ const io = new Server(server, { * Listen on socket server */ import { registerHandlers } from './routes/registerHandlers'; -io.on('connection', (socket: Socket) => registerHandlers(io, socket)); +io.on('connection', (socket: TypedSocket) => registerHandlers(io, socket)); /** * Start room service diff --git a/server/src/routes/gameHandlers.ts b/server/src/routes/gameHandlers.ts index bc13188..ae4791a 100644 --- a/server/src/routes/gameHandlers.ts +++ b/server/src/routes/gameHandlers.ts @@ -1,15 +1,16 @@ import { getRoomById, Room } from '../state/rooms'; -import { GameState, Responses, Stage } from '../state/gameState'; +import { GameState } from '../state/gameState'; import { z } from 'zod'; import logger from '../logger/logger'; -import { Server, Socket } from 'socket.io'; /*** handler validation schemas ***/ import { ApiResult, isErr, isOk, isSuccess } from '../types/result'; import { ConfigurableOptions, getConfigurableOptionsSchema } from '../state/options'; import { PollName } from '../state/pollService'; +import { TypedServer, TypedSocket } from '../types/socketServerTypes'; +import { Responses, Stage } from '../types/stateTypes'; -const registerGameHandlers = (io: Server, socket: Socket) => { +const registerGameHandlers = (io: TypedServer, socket: TypedSocket) => { /*** GAME STATE ENDPOINTS ***/ socket.on('setOptions', (options: ConfigurableOptions, callback?: (p: { success: boolean }) => void) => { const room = roomIfLeader(socket.id); @@ -152,7 +153,7 @@ const registerGameHandlers = (io: Server, socket: Socket) => { const state = room.state!; const result = state.acceptMatch(socket.id, match); if (isSuccess(result)) { - io.to(room.name).emit('matchesFound', [{ player: socket.id, response: match }]); + io.to(room.name).emit('matchesFound', [{ player: socket.id, response: match, exact: false }]); } else { logger.log(result.wrap('(gameHandlers) selectMatch failed due to %1$s')); } @@ -174,7 +175,7 @@ const registerGameHandlers = (io: Server, socket: Socket) => { }); // todo: change client side to accept Result type instead of this dumb ApiResult - socket.on('getResponses', (id, callback: (result: ApiResult) => void) => { + socket.on('getResponses', (id: string, callback: (result: ApiResult) => void) => { const validationResult = z.object({ id: z.string(), callback: z.function() }).safeParse({ id, callback }); if (!validationResult.success) { logger.error('(gameHandlers) getResponses attempted with invalid arguments'); @@ -197,7 +198,7 @@ const registerGameHandlers = (io: Server, socket: Socket) => { }); }; -function registerCallbacks(io: Server, room: Room) { +function registerCallbacks(io: TypedServer, room: Room) { const state = room.state!; state.registerStartNextPromptCb(() => { @@ -226,7 +227,7 @@ function registerCallbacks(io: Server, room: Room) { }); } -function beginPrompt(io: Server, room: Room) { +function beginPrompt(io: TypedServer, room: Room) { const state = room.state!; if (state.beginNewPrompt()) { io.to(room.name).emit('beginPrompt', state.prompt); @@ -241,7 +242,7 @@ function beginPrompt(io: Server, room: Room) { } } -function skipPrompt(io: Server, room: Room) { +function skipPrompt(io: TypedServer, room: Room) { const state = room.state!; if (state.promptTimeout) { clearTimeout(state.promptTimeout); @@ -250,7 +251,7 @@ function skipPrompt(io: Server, room: Room) { beginPrompt(io, room); } -function beginSelection(io: Server, room: Room) { +function beginSelection(io: TypedServer, room: Room) { const state = room.state!; if (state.beginSelection()) { io.to(room.name).emit('nextSelection', { @@ -262,7 +263,7 @@ function beginSelection(io: Server, room: Room) { } } -function continueSelection(io: Server, room: Room) { +function continueSelection(io: TypedServer, room: Room) { const state = room.state!; if (state.nextSelection()) { io.to(room.name).emit('nextSelection', { @@ -276,7 +277,7 @@ function continueSelection(io: Server, room: Room) { } } -function applyDisputeAction(io: Server, room: Room, action: string) { +function applyDisputeAction(io: TypedServer, room: Room, action: string) { if (action === 'reSelect') { io.to(room.name).emit('nextSelection', { selector: room.state!.selectorId(), @@ -287,7 +288,7 @@ function applyDisputeAction(io: Server, room: Room, action: string) { } } -function beginMatching(io: Server, room: Room) { +function beginMatching(io: TypedServer, room: Room) { const state = room.state!; io.to(room.name).emit('beginMatching', state.selectedResponse()); const matches = state.matches(); @@ -305,7 +306,7 @@ function roomIfLeader(id: string): Room | undefined { return room; } -function midgameJoin(socket: Socket, room: Room, oldId?: string) { +function midgameJoin(socket: TypedSocket, room: Room, oldId?: string) { socket.emit('midgameConnect', room.state!.midgameConnect(socket.id, oldId)); if (room.state!.stage === Stage.Matching) { const match = room.state!.getMatch(socket.id); diff --git a/server/src/routes/registerHandlers.ts b/server/src/routes/registerHandlers.ts index 24681f7..04b38b5 100644 --- a/server/src/routes/registerHandlers.ts +++ b/server/src/routes/registerHandlers.ts @@ -1,8 +1,8 @@ -import { Server, Socket } from 'socket.io'; import { registerRoomHandlers } from './roomHandlers'; import { registerGameHandlers } from './gameHandlers'; +import { TypedServer, TypedSocket } from '../types/socketServerTypes'; -export function registerHandlers(io: Server, socket: Socket) { +export function registerHandlers(io: TypedServer, socket: TypedSocket) { registerRoomHandlers(io, socket); registerGameHandlers(io, socket); } diff --git a/server/src/routes/roomHandlers.ts b/server/src/routes/roomHandlers.ts index a73ee9c..7c57ce5 100644 --- a/server/src/routes/roomHandlers.ts +++ b/server/src/routes/roomHandlers.ts @@ -1,10 +1,10 @@ import { createRoom, disconnectPlayer, getRoomById, getRoomByName, joinRoom } from '../state/rooms'; import logger from '../logger/logger'; import { midgameJoin } from './gameHandlers'; -import { Server, Socket } from 'socket.io'; import { isErr } from '../types/result'; -import { Stage } from '../state/gameState'; import { z } from 'zod'; +import { TypedServer, TypedSocket } from '../types/socketServerTypes'; +import { Stage } from '../types/stateTypes'; /*** handler validation schemas ***/ const roomSchema = z.object({ @@ -12,7 +12,8 @@ const roomSchema = z.object({ roomName: z.string(), langs: z.array(z.string().min(2).max(5)).optional() }); -export function registerRoomHandlers(io: Server, socket: Socket): void { + +export function registerRoomHandlers(io: TypedServer, socket: TypedSocket): void { socket.onAny(() => { // update activity const room = getRoomById(socket.id); @@ -66,7 +67,7 @@ export function registerRoomHandlers(io: Server, socket: Socket): void { socket.emit('joinRoom', { success: true, roomName: room.name }); socket.emit('updatePlayers', { modifies: room.players, deletes: [] }); socket.to(room.name).emit('updatePlayers', { - modifies: [room.players.find((p) => p.name === name)], + modifies: [room.players.find((p) => p.name === name)!], deletes: [] }); socket.emit('setOptions', room.state!.getOptions()); @@ -81,7 +82,7 @@ export function registerRoomHandlers(io: Server, socket: Socket): void { }); } -function disconnect(socket: Socket): void { +function disconnect(socket: TypedSocket): void { const roomName = getRoomById(socket.id)?.name; disconnectPlayer(socket.id); // remove socket from room @@ -92,9 +93,11 @@ function disconnect(socket: Socket): void { const room = getRoomByName(roomName); if (room) { - const player = room.players.find((p) => p.id === socket.id); + // safe because we know this player exists + const player = room.players.find((p) => p.id === socket.id)!; // could be modified - const leader = room.players.find((p) => p.leader); + // safe because if there was no leader then there would be no room + const leader = room.players.find((p) => p.leader)!; socket.to(room.name).emit('updatePlayers', { modifies: [player, leader], deletes: [] }); } } diff --git a/server/src/state/gameState.ts b/server/src/state/gameState.ts index f79556c..3fd8a48 100644 --- a/server/src/state/gameState.ts +++ b/server/src/state/gameState.ts @@ -5,19 +5,7 @@ import logger from '../logger/logger'; import { Player as RoomPlayer, Room } from './rooms'; import { Err, Info, Ok, Result, Success, VoidResult, Warning } from '../types/result'; import { ConfigurableOptions, defaultOptions, getConfigurableOptionsSchema, Options } from './options'; - -export enum Stage { - Lobby, - Response, - Selection, - Matching, - EndRound -} -export enum SelectionType { - Strike = 'strike', - Sike = 'sike', - Choice = 'choice' -} +import { Match, MidgameConnectData, Responses, SelectionType, Stage } from '../types/stateTypes'; type Player = { id: string; @@ -31,20 +19,6 @@ type Player = { matchingComplete: boolean; // set to true if explicitly no match was found or a match was found }; -type Match = { - player: string; - response: string; - exact: boolean; -}; - -export type Responses = { - id: string; - all: string[]; - used: string[]; - selectedStrike: string; - selectedSike: string; -}; - export class GameState { stage: Stage; public options: Options; @@ -547,7 +521,7 @@ export class GameState { return Math.ceil((timeout._idleStart + timeout._idleTimeout) / 1000 - process.uptime()); } - midgameConnect(id: string, oldId?: string) { + midgameConnect(id: string, oldId?: string): MidgameConnectData { let player = this.players.find((player) => player.id === oldId); if (!player) { logger.info('(gameState) midgame join'); diff --git a/server/src/state/pollService.ts b/server/src/state/pollService.ts index 23d38bb..6c5fc38 100644 --- a/server/src/state/pollService.ts +++ b/server/src/state/pollService.ts @@ -1,5 +1,6 @@ -import { GameState, Stage } from './gameState'; +import { GameState } from './gameState'; import { Err, Ok, Result } from '../types/result'; +import { Stage } from '../types/stateTypes'; export enum PollName { SkipPrompt = 'skipPrompt', diff --git a/server/src/types/socketServerTypes.ts b/server/src/types/socketServerTypes.ts index e69de29..a1f3509 100644 --- a/server/src/types/socketServerTypes.ts +++ b/server/src/types/socketServerTypes.ts @@ -0,0 +1,69 @@ +import { Server, Socket } from 'socket.io'; +import { ConfigurableOptions } from '../state/options'; +import { PollName } from '../state/pollService'; +import { Match, MidgameConnectData, Responses, SelectionType } from './stateTypes'; +import { ApiResult } from './result'; +import { Player } from '../state/rooms'; + +interface ServerToClientRoomEvents { + joinRoom(args: { error: string } | { success: boolean; roomName: string }): void; + + updatePlayers(args: { modifies: Player[]; deletes: Player[] }): void; +} + +interface ClientToServerRoomEvents { + createRoom(name: string, roomName: string, langs?: string[]): void; + + joinRoom(name: string, roomName: string): void; +} + +interface ServerToClientGameEvents { + setOptions(options: ConfigurableOptions): void; + + beginPrompt(prompt: string): void; + + promptResponse(response: string): void; + + nextSelection(args: { selector: string; selectionType: SelectionType }): void; + + setVoteCount(args: { pollName: PollName; count: number; next: boolean }): void; + + selectionTypeChosen(selectionType: SelectionType): void; + + beginMatching(selectedResponse: string): void; + + matchesFound(matches: Match[]): void; + + endRound(args: { hasNextRound: boolean }): void; + + gameOver(results: { player: string; points: number }[]): void; + + midgameConnect(reconnect: MidgameConnectData): void; +} + +interface ClientToServerGameEvents { + setOptions(options: ConfigurableOptions, callback?: (p: { success: boolean }) => void): void; + + startGame(): void; + + pollVote(pollName: PollName): void; + + promptResponse(response: string): void; + + selectSelectionType(isStrike: boolean): void; + + selectResponse(response: string): void; + + selectMatch(match: string): void; + + selectionComplete(): void; + + getResponses(id: string, callback: (result: ApiResult) => void): void; +} + +type ServerToClientEvents = ServerToClientRoomEvents & ServerToClientGameEvents; +type ClientToServerEvents = ClientToServerRoomEvents & ClientToServerGameEvents; + +export class TypedServer extends Server {} + +export class TypedSocket extends Socket {} diff --git a/server/src/types/stateTypes.ts b/server/src/types/stateTypes.ts new file mode 100644 index 0000000..73996fe --- /dev/null +++ b/server/src/types/stateTypes.ts @@ -0,0 +1,43 @@ +import { ConfigurableOptions } from '../state/options'; +import { PollName } from '../state/pollService'; + +export enum Stage { + Lobby, + Response, + Selection, + Matching, + EndRound +} + +export enum SelectionType { + Strike = 'strike', + Sike = 'sike', + Choice = 'choice' +} + +export type Match = { + player: string; + response: string; + exact: boolean; +}; + +export type Responses = { + id: string; + all: string[]; + used: string[]; + selectedStrike: string; + selectedSike: string; +}; + +export type MidgameConnectData = { + stage: Stage; + selectionType: SelectionType; + responses: Responses; + selector: string; + selectedResponse: string; + prompt: string; + options: ConfigurableOptions; + timer: number; + matches: Match[]; + voteCounts: Record; +}; diff --git a/server/tests/models/autoMatch.ts b/server/tests/models/autoMatch.ts index 4fcfc4f..6bcabb5 100644 --- a/server/tests/models/autoMatch.ts +++ b/server/tests/models/autoMatch.ts @@ -1,6 +1,7 @@ import { assert } from 'chai'; -import { GameState, SelectionType } from '../../src/state/gameState'; +import { GameState } from '../../src/state/gameState'; import { Player, Room } from '../../src/state/rooms'; +import { SelectionType } from '../../src/types/stateTypes'; describe('Automatch tests', () => { const selectorId = 'selector'; diff --git a/server/tests/models/completeCallbacks.ts b/server/tests/models/completeCallbacks.ts index 8b9a171..ebe6475 100644 --- a/server/tests/models/completeCallbacks.ts +++ b/server/tests/models/completeCallbacks.ts @@ -1,6 +1,8 @@ -import { GameState, SelectionType } from '../../src/state/gameState'; +import { GameState } from '../../src/state/gameState'; import { assert } from 'chai'; import { Player, Room } from '../../src/state/rooms'; +import { PollName } from '../../src/state/pollService'; +import { SelectionType } from '../../src/types/stateTypes'; describe('Complete callback tests', () => { let players: Player[]; @@ -43,8 +45,8 @@ describe('Complete callback tests', () => { }); gameState.registerPromptSkippedCb(done); gameState.beginNewPrompt(); - gameState.pollVote(selectorId, 'skipPrompt'); - gameState.pollVote(matcherId, 'skipPrompt'); + gameState.pollVote(selectorId, PollName.SkipPrompt); + gameState.pollVote(matcherId, PollName.SkipPrompt); players[matcher2Index].active = false; gameState.disconnect(matcherId); }); @@ -120,7 +122,7 @@ describe('Complete callback tests', () => { assert.strictEqual(action, 'nextSelection'); done(); }); - gameState.pollVote(matcherId, 'sikeDispute'); + gameState.pollVote(matcherId, PollName.SikeDispute); players[matcher2Index].active = false; gameState.disconnect(matcher2Id); }); diff --git a/server/tests/models/selectionAccepts.ts b/server/tests/models/selectionAccepts.ts index c8c33da..d922b87 100644 --- a/server/tests/models/selectionAccepts.ts +++ b/server/tests/models/selectionAccepts.ts @@ -1,7 +1,8 @@ import { assert } from 'chai'; -import { GameState, SelectionType } from '../../src/state/gameState'; +import { GameState } from '../../src/state/gameState'; import { Player, Room } from '../../src/state/rooms'; import { isErr, isSuccess } from '../../src/types/result'; +import { SelectionType } from '../../src/types/stateTypes'; describe('Selection Accepting tests', () => { const selectorId = 'selector'; diff --git a/server/tests/models/sikeDispute.ts b/server/tests/models/sikeDispute.ts index a206a39..407e7c5 100644 --- a/server/tests/models/sikeDispute.ts +++ b/server/tests/models/sikeDispute.ts @@ -1,7 +1,8 @@ -import { GameState, SelectionType } from '../../src/state/gameState'; +import { GameState } from '../../src/state/gameState'; import { assert } from 'chai'; import { Player } from '../../src/state/rooms'; import { isErr, isSuccess } from '../../src/types/result'; +import { SelectionType } from '../../src/types/stateTypes'; describe('Sike Dispute tests', () => { const selectorId = '0';