diff --git a/apps/server/src/logging/contracts/payloadByScope.ts b/apps/server/src/logging/contracts/payloadByScope.ts index 598845b..63b10ac 100644 --- a/apps/server/src/logging/contracts/payloadByScope.ts +++ b/apps/server/src/logging/contracts/payloadByScope.ts @@ -27,7 +27,8 @@ result: | typeof logResults.REJECTED_ROOM_FULL | typeof logResults.REJECTED_DUPLICATE - | typeof logResults.IGNORED_INVALID_PAYLOAD; + | typeof logResults.IGNORED_INVALID_PAYLOAD + | typeof logResults.IGNORED_MISSING_ROOM; socketId: string; roomId?: string; }; @@ -42,21 +43,27 @@ /** NetworkのMOVE不正ペイロードログ契約 */ type NetworkMoveLogPayload = { event: typeof protocol.SocketEvents.MOVE; - result: typeof logResults.IGNORED_INVALID_PAYLOAD; + result: + | typeof logResults.IGNORED_INVALID_PAYLOAD + | typeof logResults.IGNORED_MISSING_ROOM; socketId: string; }; /** NetworkのPLACE_BOMB不正ペイロードログ契約 */ type NetworkPlaceBombLogPayload = { event: typeof protocol.SocketEvents.PLACE_BOMB; - result: typeof logResults.IGNORED_INVALID_PAYLOAD; + result: + | typeof logResults.IGNORED_INVALID_PAYLOAD + | typeof logResults.IGNORED_MISSING_ROOM; socketId: string; }; /** NetworkのBOMB_HIT_REPORT不正ペイロードログ契約 */ type NetworkBombHitReportLogPayload = { event: typeof protocol.SocketEvents.BOMB_HIT_REPORT; - result: typeof logResults.IGNORED_INVALID_PAYLOAD; + result: + | typeof logResults.IGNORED_INVALID_PAYLOAD + | typeof logResults.IGNORED_MISSING_ROOM; socketId: string; }; diff --git a/apps/server/src/network/handlers/game/gameEventOrchestrators.ts b/apps/server/src/network/handlers/game/gameEventOrchestrators.ts index 33d0ee9..36dbb86 100644 --- a/apps/server/src/network/handlers/game/gameEventOrchestrators.ts +++ b/apps/server/src/network/handlers/game/gameEventOrchestrators.ts @@ -2,6 +2,7 @@ * gameEventOrchestrators * ゲーム受信イベントごとの調停処理を提供する * 受信ハンドラからユースケース実行責務を分離する + * ランタイム未解決時はNetworkスコープでignored_missing_roomを記録する */ import { protocol, type BombHitReportPayload, type PingPayload, type PlaceBombPayload, type playerTypes } from "@repo/shared"; import { readyForGameCoordinator } from "@server/application/coordinators/readyForGameCoordinator"; @@ -11,10 +12,11 @@ import { placeBombUseCase } from "@server/domains/game/application/useCases/placeBombUseCase"; import { reportBombHitUseCase } from "@server/domains/game/application/useCases/reportBombHitUseCase"; import { runWithRuntimeByPlayerId } from "@server/domains/room/application/services/RoomRuntimeResolver"; +import { logIgnoredMissingRoom } from "../orchestratorEventLogger"; import type { GameOutputAdapter } from "./createGameOutputAdapter"; import type { - GameHandlerRoomPort, - GameHandlerRuntimePort, + GameEventRoomUseCasePort, + GameEventRuntimeUseCasePort, } from "@server/network/types/connectionPorts"; /** START_GAMEイベントの入力ペイロード型 */ @@ -25,8 +27,8 @@ /** ゲームイベント調停で利用する依存集合 */ export type GameEventOrchestratorDeps = { socketId: string; - roomManager: GameHandlerRoomPort; - runtimeRegistry: GameHandlerRuntimePort; + roomManager: GameEventRoomUseCasePort; + runtimeRegistry: GameEventRuntimeUseCasePort; output: GameOutputAdapter; }; @@ -72,7 +74,7 @@ deps: GameEventOrchestratorDeps, move: playerTypes.MovePayload, ): void => { - runWithRuntimeByPlayerId( + const resolved = runWithRuntimeByPlayerId( deps.roomManager, deps.runtimeRegistry, deps.socketId, @@ -84,6 +86,9 @@ }); }, ); + if (!resolved) { + logIgnoredMissingRoom(protocol.SocketEvents.MOVE, deps.socketId); + } }; /** PLACE_BOMBイベントを調停して爆弾設置ユースケースを実行する */ @@ -91,7 +96,7 @@ deps: GameEventOrchestratorDeps, payload: PlaceBombPayload, ): void => { - runWithRuntimeByPlayerId( + const resolved = runWithRuntimeByPlayerId( deps.roomManager, deps.runtimeRegistry, deps.socketId, @@ -108,6 +113,9 @@ }); }, ); + if (!resolved) { + logIgnoredMissingRoom(protocol.SocketEvents.PLACE_BOMB, deps.socketId); + } }; /** BOMB_HIT_REPORTイベントを調停して被弾報告ユースケースを実行する */ @@ -115,7 +123,7 @@ deps: GameEventOrchestratorDeps, payload: BombHitReportPayload, ): void => { - runWithRuntimeByPlayerId( + const resolved = runWithRuntimeByPlayerId( deps.roomManager, deps.runtimeRegistry, deps.socketId, @@ -132,4 +140,7 @@ }); }, ); + if (!resolved) { + logIgnoredMissingRoom(protocol.SocketEvents.BOMB_HIT_REPORT, deps.socketId); + } }; diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index 8bab813..e09974d 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -5,8 +5,8 @@ import { Socket } from "socket.io"; import { protocol } from "@repo/shared"; import type { - GameHandlerRoomPort, - GameHandlerRuntimePort, + GameEventRoomUseCasePort, + GameEventRuntimeUseCasePort, } from "@server/network/types/connectionPorts"; import { isBombHitReportPayload, @@ -20,6 +20,7 @@ import type { GameOutputAdapter } from "./createGameOutputAdapter"; import { handleBombHitReportEvent, + type GameEventOrchestratorDeps, handleMoveEvent, handlePingEvent, handlePlaceBombEvent, @@ -35,123 +36,137 @@ [protocol.SocketEvents.BOMB_HIT_REPORT]: isBombHitReportPayload, } as const; +/** 検証付きゲームイベント登録定義 */ +type GuardedGameEventDefinition = { + event: keyof typeof gamePayloadValidators; + validator: (value: unknown) => value is unknown; + orchestrate: (payload: unknown) => void; +}; + +/** 自前検証付きゲームイベント登録定義 */ +type SelfValidatedGameEventDefinition = { + event: typeof protocol.SocketEvents.START_GAME; + validator: (value: unknown) => value is unknown; + orchestrate: (payload: unknown) => void; +}; + +/** 検証不要ゲームイベント登録定義 */ +type UnguardedGameEventDefinition = { + event: typeof protocol.SocketEvents.READY_FOR_GAME; + orchestrate: () => void; +}; + +/** ゲームイベント調停で利用する依存束を生成する */ +const createGameOrchestratorDeps = ( + socket: Socket, + roomManager: GameEventRoomUseCasePort, + runtimeRegistry: GameEventRuntimeUseCasePort, + gameOutputAdapter: GameOutputAdapter, +): GameEventOrchestratorDeps => { + return { + socketId: socket.id, + roomManager, + runtimeRegistry, + output: gameOutputAdapter, + }; +}; + /** ゲームイベントの購読とユースケース呼び出しを設定する */ export const registerGameHandlers = ( socket: Socket, - roomManager: GameHandlerRoomPort, - runtimeRegistry: GameHandlerRuntimePort, + roomManager: GameEventRoomUseCasePort, + runtimeRegistry: GameEventRuntimeUseCasePort, gameOutputAdapter: GameOutputAdapter, ) => { + const orchestratorDeps = createGameOrchestratorDeps( + socket, + roomManager, + runtimeRegistry, + gameOutputAdapter, + ); const { onEvent } = createServerSocketOnBridge(socket); const { guardOnEvent } = createPayloadGuard(socket.id); - const guardPingPayload = guardOnEvent( - protocol.SocketEvents.PING, - gamePayloadValidators[protocol.SocketEvents.PING], - ); - const guardMovePayload = guardOnEvent( - protocol.SocketEvents.MOVE, - gamePayloadValidators[protocol.SocketEvents.MOVE], - ); - const guardPlaceBombPayload = guardOnEvent( - protocol.SocketEvents.PLACE_BOMB, - gamePayloadValidators[protocol.SocketEvents.PLACE_BOMB], - ); - const guardBombHitReportPayload = guardOnEvent( - protocol.SocketEvents.BOMB_HIT_REPORT, - gamePayloadValidators[protocol.SocketEvents.BOMB_HIT_REPORT], - ); - // 遅延計測用のPINGを検証しPONGを返す - onEvent(protocol.SocketEvents.PING, (clientTime) => { - if (!guardPingPayload(clientTime)) { - return; - } - handlePingEvent( - { - socketId: socket.id, - roomManager, - runtimeRegistry, - output: gameOutputAdapter, + // 検証が必要なイベントを宣言的に登録する + const guardedGameEventDefinitions: GuardedGameEventDefinition[] = [ + { + event: protocol.SocketEvents.PING, + validator: gamePayloadValidators[protocol.SocketEvents.PING], + orchestrate: (payload) => { + handlePingEvent(orchestratorDeps, payload as Parameters[1]); }, - clientTime, - ); - }); - - // オーナー開始要求に応じてゲーム進行ユースケースを起動する - onEvent(protocol.SocketEvents.START_GAME, (data) => { - if (!isStartGamePayload(data)) { - return; - } - - handleStartGameEvent( - { - socketId: socket.id, - roomManager, - runtimeRegistry, - output: gameOutputAdapter, + }, + { + event: protocol.SocketEvents.MOVE, + validator: gamePayloadValidators[protocol.SocketEvents.MOVE], + orchestrate: (payload) => { + handleMoveEvent(orchestratorDeps, payload as Parameters[1]); }, - data, - ); - }); + }, + { + event: protocol.SocketEvents.PLACE_BOMB, + validator: gamePayloadValidators[protocol.SocketEvents.PLACE_BOMB], + orchestrate: (payload) => { + handlePlaceBombEvent(orchestratorDeps, payload as Parameters[1]); + }, + }, + { + event: protocol.SocketEvents.BOMB_HIT_REPORT, + validator: gamePayloadValidators[protocol.SocketEvents.BOMB_HIT_REPORT], + orchestrate: (payload) => { + handleBombHitReportEvent(orchestratorDeps, payload as Parameters[1]); + }, + }, + ]; - // 参加者の準備完了通知を受けて現在状態を返す - onEvent(protocol.SocketEvents.READY_FOR_GAME, () => { - handleReadyForGameEvent({ - socketId: socket.id, - roomManager, - runtimeRegistry, - output: gameOutputAdapter, + guardedGameEventDefinitions.forEach((definition) => { + const guard = guardOnEvent(definition.event, definition.validator); + onEvent(definition.event, (payload) => { + if (!guard(payload)) { + return; + } + + definition.orchestrate(payload); }); }); - // 移動入力を検証しプレイヤー移動ユースケースへ連携する - onEvent(protocol.SocketEvents.MOVE, (data) => { - if (!guardMovePayload(data)) { - return; - } - - handleMoveEvent( - { - socketId: socket.id, - roomManager, - runtimeRegistry, - output: gameOutputAdapter, + // payloadGuard対象外だが検証が必要なイベントを宣言的に登録する + const selfValidatedGameEventDefinitions: SelfValidatedGameEventDefinition[] = [ + { + event: protocol.SocketEvents.START_GAME, + validator: isStartGamePayload, + orchestrate: (payload) => { + handleStartGameEvent( + orchestratorDeps, + payload as Parameters[1], + ); }, - data, - ); + }, + ]; + + selfValidatedGameEventDefinitions.forEach((definition) => { + onEvent(definition.event, (payload) => { + if (!definition.validator(payload)) { + return; + } + + definition.orchestrate(payload); + }); }); - // 爆弾設置入力を検証し,所属ルームへ同期配信する - onEvent(protocol.SocketEvents.PLACE_BOMB, (data) => { - if (!guardPlaceBombPayload(data)) { - return; - } - - handlePlaceBombEvent( - { - socketId: socket.id, - roomManager, - runtimeRegistry, - output: gameOutputAdapter, + // 検証不要イベントを宣言的に登録する + const unguardedGameEventDefinitions: UnguardedGameEventDefinition[] = [ + { + event: protocol.SocketEvents.READY_FOR_GAME, + orchestrate: () => { + handleReadyForGameEvent(orchestratorDeps); }, - data, - ); - }); + }, + ]; - // 被弾報告を受信する - onEvent(protocol.SocketEvents.BOMB_HIT_REPORT, (data) => { - if (!guardBombHitReportPayload(data)) { - return; - } - - handleBombHitReportEvent( - { - socketId: socket.id, - roomManager, - runtimeRegistry, - output: gameOutputAdapter, - }, - data, - ); + unguardedGameEventDefinitions.forEach((definition) => { + onEvent(definition.event, () => { + definition.orchestrate(); + }); }); }; diff --git a/apps/server/src/network/handlers/orchestratorEventLogger.ts b/apps/server/src/network/handlers/orchestratorEventLogger.ts new file mode 100644 index 0000000..b7decd3 --- /dev/null +++ b/apps/server/src/network/handlers/orchestratorEventLogger.ts @@ -0,0 +1,25 @@ +/** + * orchestratorEventLogger + * オーケストレータ層で利用するイベントログ記録を共通化する + */ +import { protocol } from "@repo/shared"; +import { logEvent } from "@server/logging/logger"; +import { logResults, logScopes } from "@server/logging/index"; + +/** オーケストレータで未解決時に記録するイベント型 */ +export type MissingRoomNetworkEvent = + | typeof protocol.SocketEvents.MOVE + | typeof protocol.SocketEvents.PLACE_BOMB + | typeof protocol.SocketEvents.BOMB_HIT_REPORT; + +/** 未解決のルーム関連イベントをNetworkスコープで記録する */ +export const logIgnoredMissingRoom = ( + event: MissingRoomNetworkEvent, + socketId: string, +): void => { + logEvent(logScopes.NETWORK, { + event, + result: logResults.IGNORED_MISSING_ROOM, + socketId, + }); +}; diff --git a/apps/server/src/network/handlers/room/registerRoomHandlers.ts b/apps/server/src/network/handlers/room/registerRoomHandlers.ts index a893f3c..91690d1 100644 --- a/apps/server/src/network/handlers/room/registerRoomHandlers.ts +++ b/apps/server/src/network/handlers/room/registerRoomHandlers.ts @@ -5,51 +5,86 @@ import { Socket } from "socket.io"; import { protocol } from "@repo/shared"; import type { - RoomHandlerRoomPort, - RoomHandlerRuntimePort, + JoinRoomEventRoomUseCasePort, + JoinRoomEventRuntimeUseCasePort, } from "@server/network/types/connectionPorts"; import { createPayloadGuard } from "@server/network/handlers/payloadGuard"; import { createServerSocketOnBridge } from "@server/network/handlers/socketEventBridge"; import { isJoinRoomPayload } from "@server/network/validation/socketPayloadValidators"; import type { RoomOutputAdapter } from "./createRoomOutputAdapter"; -import { handleJoinRoomEvent } from "./roomEventOrchestrators"; +import { + handleJoinRoomEvent, + type JoinRoomOrchestratorDeps, +} from "./roomEventOrchestrators"; /** ルーム受信イベントごとの入力検証関数を保持するテーブル */ const roomPayloadValidators = { [protocol.SocketEvents.JOIN_ROOM]: isJoinRoomPayload, } as const; +/** 検証付きルームイベント登録定義 */ +type GuardedRoomEventDefinition = { + event: keyof typeof roomPayloadValidators; + validator: (value: unknown) => value is unknown; + orchestrate: (payload: unknown) => Promise; +}; + +/** ルームイベント調停で利用する依存束を生成する */ +const createJoinRoomOrchestratorDeps = ( + socket: Socket, + roomManager: JoinRoomEventRoomUseCasePort, + runtimeRegistry: JoinRoomEventRuntimeUseCasePort, + roomOutputAdapter: RoomOutputAdapter, +): JoinRoomOrchestratorDeps => { + return { + socketId: socket.id, + roomManager, + runtimeRegistry, + output: roomOutputAdapter, + joinRoom: async (roomId) => { + await socket.join(roomId); + }, + }; +}; + /** ルーム参加イベントを検証して参加ユースケースへ連携する */ export const registerRoomHandlers = ( socket: Socket, - roomManager: RoomHandlerRoomPort, - runtimeRegistry: RoomHandlerRuntimePort, + roomManager: JoinRoomEventRoomUseCasePort, + runtimeRegistry: JoinRoomEventRuntimeUseCasePort, roomOutputAdapter: RoomOutputAdapter, ) => { + const orchestratorDeps = createJoinRoomOrchestratorDeps( + socket, + roomManager, + runtimeRegistry, + roomOutputAdapter, + ); const { onEvent } = createServerSocketOnBridge(socket); const { guardOnEvent } = createPayloadGuard(socket.id); - const guardJoinRoomPayload = guardOnEvent( - protocol.SocketEvents.JOIN_ROOM, - roomPayloadValidators[protocol.SocketEvents.JOIN_ROOM] - ); - // 参加要求のペイロード検証と参加処理を実行する - onEvent(protocol.SocketEvents.JOIN_ROOM, async (data) => { - if (!guardJoinRoomPayload(data)) { - return; - } - - await handleJoinRoomEvent( - { - socketId: socket.id, - roomManager, - runtimeRegistry, - output: roomOutputAdapter, - joinRoom: async (roomId) => { - await socket.join(roomId); - }, + // 検証が必要なイベントを宣言的に登録する + const guardedRoomEventDefinitions: GuardedRoomEventDefinition[] = [ + { + event: protocol.SocketEvents.JOIN_ROOM, + validator: roomPayloadValidators[protocol.SocketEvents.JOIN_ROOM], + orchestrate: async (payload) => { + await handleJoinRoomEvent( + orchestratorDeps, + payload as Parameters[1], + ); }, - data, - ); + }, + ]; + + guardedRoomEventDefinitions.forEach((definition) => { + const guard = guardOnEvent(definition.event, definition.validator); + onEvent(definition.event, async (payload) => { + if (!guard(payload)) { + return; + } + + await definition.orchestrate(payload); + }); }); }; diff --git a/apps/server/src/network/handlers/room/roomEventOrchestrators.ts b/apps/server/src/network/handlers/room/roomEventOrchestrators.ts index 7c06a89..7b13c35 100644 --- a/apps/server/src/network/handlers/room/roomEventOrchestrators.ts +++ b/apps/server/src/network/handlers/room/roomEventOrchestrators.ts @@ -2,22 +2,23 @@ * roomEventOrchestrators * ルーム受信イベントごとの調停処理を提供する * 受信ハンドラからユースケース実行責務を分離する + * 本ファイルではランタイム未解決ログ対象イベントを扱わない */ import type { roomTypes } from "@repo/shared"; import { joinRoomUseCase } from "@server/domains/room/application/useCases/joinRoomUseCase"; import { logEvent } from "@server/logging/logger"; import { logResults, logScopes, roomUseCaseLogEvents } from "@server/logging/index"; import type { - RoomHandlerRoomPort, - RoomHandlerRuntimePort, + JoinRoomEventRoomUseCasePort, + JoinRoomEventRuntimeUseCasePort, } from "@server/network/types/connectionPorts"; import type { RoomOutputAdapter } from "./createRoomOutputAdapter"; /** JOIN_ROOMイベント調停で利用する依存集合 */ export type JoinRoomOrchestratorDeps = { socketId: string; - roomManager: RoomHandlerRoomPort; - runtimeRegistry: RoomHandlerRuntimePort; + roomManager: JoinRoomEventRoomUseCasePort; + runtimeRegistry: JoinRoomEventRuntimeUseCasePort; output: RoomOutputAdapter; joinRoom: (roomId: string) => Promise; }; diff --git a/apps/server/src/network/types/connectionPorts.ts b/apps/server/src/network/types/connectionPorts.ts index 79a503f..110e570 100644 --- a/apps/server/src/network/types/connectionPorts.ts +++ b/apps/server/src/network/types/connectionPorts.ts @@ -32,23 +32,23 @@ & FindGameByRoomPort & FindGameByPlayerPort; -/** ゲーム受信ハンドラで利用するルーム依存ポート */ -export type GameHandlerRoomPort = Pick< +/** ゲームイベント調停で利用するルーム依存ポート */ +export type GameEventRoomUseCasePort = Pick< ConnectionRoomPort, "getRoomByOwnerId" | "getRoomByPlayerId" | "markRoomPlaying" | "markRoomWaiting" >; -/** ゲーム受信ハンドラで利用するランタイム依存ポート */ -export type GameHandlerRuntimePort = Pick< +/** ゲームイベント調停で利用するランタイム依存ポート */ +export type GameEventRuntimeUseCasePort = Pick< ConnectionRuntimePort, "getGameManagerByRoomId" | "getGameManagerByPlayerId" >; -/** ルーム受信ハンドラで利用するルーム依存ポート */ -export type RoomHandlerRoomPort = Pick; +/** ルーム参加イベント調停で利用するルーム依存ポート */ +export type JoinRoomEventRoomUseCasePort = Pick; -/** ルーム受信ハンドラで利用するランタイム依存ポート */ -export type RoomHandlerRuntimePort = Pick< +/** ルーム参加イベント調停で利用するランタイム依存ポート */ +export type JoinRoomEventRuntimeUseCasePort = Pick< ConnectionRuntimePort, "ensureGameManagerForRoom" >;