diff --git a/apps/client/src/hooks/useAppFlow.ts b/apps/client/src/hooks/useAppFlow.ts index bb6bafc..b27d4c2 100644 --- a/apps/client/src/hooks/useAppFlow.ts +++ b/apps/client/src/hooks/useAppFlow.ts @@ -1,8 +1,15 @@ -import { useEffect, useRef, useState } from "react"; +/** + * useAppFlow + * アプリ全体の画面遷移と参加フロー状態を管理するフック + * 参加要求の成功失敗と接続状態を統合してシーンへ渡す + */ +import { useCallback, useReducer, useRef, useState } from "react"; import { socketManager } from "@client/network/SocketManager"; import { appConsts, config } from "@repo/shared"; import type { appTypes, roomTypes } from "@repo/shared"; +import { useSocketSubscriptions } from "./useSocketSubscriptions"; +/** アプリフロー管理フックの公開状態と操作を表す型 */ type AppFlowState = { scenePhase: appTypes.ScenePhase; room: roomTypes.Room | null; @@ -12,98 +19,136 @@ requestJoin: (payload: roomTypes.JoinRoomPayload) => void; }; +type JoinState = { + isJoining: boolean; + joinFailure: JoinFailure | null; +}; + +type JoinFailureReason = roomTypes.JoinRoomRejectedPayload["reason"] | "timeout"; + +type JoinFailure = { + reason: JoinFailureReason; + roomId?: string; +}; + +type JoinAction = + | { type: "start" } + | { type: "complete"; joinFailure: JoinFailure | null }; + +const initialJoinState: JoinState = { + isJoining: false, + joinFailure: null, +}; + +const joinReducer = (state: JoinState, action: JoinAction): JoinState => { + if (action.type === "start") { + return { + isJoining: true, + joinFailure: null, + }; + } + + if (action.type === "complete") { + return { + isJoining: false, + joinFailure: action.joinFailure, + }; + } + + return state; +}; + +/** アプリ全体のシーン状態と参加要求フローを管理するフック */ export const useAppFlow = (): AppFlowState => { const [scenePhase, setScenePhase] = useState(appConsts.ScenePhase.TITLE); const [room, setRoom] = useState(null); const [myId, setMyId] = useState(null); - const [joinErrorMessage, setJoinErrorMessage] = useState(null); - const [isJoining, setIsJoining] = useState(false); + const [joinState, dispatchJoin] = useReducer(joinReducer, initialJoinState); const joinTimeoutRef = useRef | null>(null); const joinRejectedHandlerRef = useRef<((payload: roomTypes.JoinRoomRejectedPayload) => void) | null>(null); - const clearJoinRejectedHandler = () => { + const clearJoinRejectedHandler = useCallback(() => { if (!joinRejectedHandlerRef.current) { return; } socketManager.title.offJoinRejected(joinRejectedHandlerRef.current); joinRejectedHandlerRef.current = null; - }; + }, []); - const clearJoinTimeout = () => { + const clearJoinTimeout = useCallback(() => { if (!joinTimeoutRef.current) { return; } clearTimeout(joinTimeoutRef.current); joinTimeoutRef.current = null; - }; + }, []); - const requestJoin = (payload: roomTypes.JoinRoomPayload) => { - if (isJoining) { + const completeJoinRequest = useCallback((joinFailure: JoinFailure | null = null) => { + clearJoinTimeout(); + clearJoinRejectedHandler(); + dispatchJoin({ type: "complete", joinFailure }); + }, [clearJoinRejectedHandler, clearJoinTimeout]); + + const getJoinErrorMessage = useCallback((joinFailure: JoinFailure | null): string | null => { + if (!joinFailure) { + return null; + } + + if (joinFailure.reason === "full") { + return `ルーム ${joinFailure.roomId ?? ""} は満員です`; + } + + if (joinFailure.reason === "duplicate") { + return `ルーム ${joinFailure.roomId ?? ""} への参加要求が重複しました`; + } + + if (joinFailure.reason === "timeout") { + return "参加要求がタイムアウトしました,もう一度お試しください"; + } + + return null; + }, []); + + const requestJoin = useCallback((payload: roomTypes.JoinRoomPayload) => { + if (joinState.isJoining) { return; } - clearJoinTimeout(); - clearJoinRejectedHandler(); - setJoinErrorMessage(null); - setIsJoining(true); + completeJoinRequest(); + dispatchJoin({ type: "start" }); const handleJoinRejected = (payload: roomTypes.JoinRoomRejectedPayload) => { - clearJoinTimeout(); - setIsJoining(false); - joinRejectedHandlerRef.current = null; - - if (payload.reason === "full") { - setJoinErrorMessage(`ルーム ${payload.roomId} は満員です`); - return; - } - - if (payload.reason === "duplicate") { - setJoinErrorMessage(`ルーム ${payload.roomId} への参加要求が重複しました`); - } + completeJoinRequest({ + reason: payload.reason, + roomId: payload.roomId, + }); }; joinRejectedHandlerRef.current = handleJoinRejected; socketManager.title.onceJoinRejected(handleJoinRejected); joinTimeoutRef.current = setTimeout(() => { - clearJoinRejectedHandler(); - setIsJoining(false); - setJoinErrorMessage("参加要求がタイムアウトしました,もう一度お試しください"); - joinTimeoutRef.current = null; + completeJoinRequest({ reason: "timeout" }); }, config.GAME_CONFIG.JOIN_REQUEST_TIMEOUT_MS); socketManager.title.joinRoom(payload); + }, [completeJoinRequest, joinState.isJoining]); + + useSocketSubscriptions({ + completeJoinRequest, + setMyId, + setRoom, + setScenePhase, + }); + + return { + scenePhase, + room, + myId, + joinErrorMessage: getJoinErrorMessage(joinState.joinFailure), + isJoining: joinState.isJoining, + requestJoin, }; - - useEffect(() => { - const handleConnect = (id: string) => { - setMyId(id); - }; - const handleRoomUpdate = (updatedRoom: roomTypes.Room) => { - clearJoinTimeout(); - clearJoinRejectedHandler(); - setRoom(updatedRoom); - setIsJoining(false); - setJoinErrorMessage(null); - setScenePhase(appConsts.ScenePhase.LOBBY); - }; - const handleGameStart = () => { - setScenePhase(appConsts.ScenePhase.PLAYING); - }; - - socketManager.common.onConnect(handleConnect); - socketManager.lobby.onRoomUpdate(handleRoomUpdate); - socketManager.game.onceGameStart(handleGameStart); - - return () => { - clearJoinTimeout(); - clearJoinRejectedHandler(); - socketManager.common.offConnect(handleConnect); - socketManager.lobby.offRoomUpdate(handleRoomUpdate); - }; - }, []); - - return { scenePhase, room, myId, joinErrorMessage, isJoining, requestJoin }; }; diff --git a/apps/client/src/hooks/useSocketSubscriptions.ts b/apps/client/src/hooks/useSocketSubscriptions.ts new file mode 100644 index 0000000..fdd75ef --- /dev/null +++ b/apps/client/src/hooks/useSocketSubscriptions.ts @@ -0,0 +1,50 @@ +/** + * useSocketSubscriptions + * アプリ共通で必要なソケット購読を登録するフック + * 接続,ルーム更新,ゲーム開始の購読と解除を一元化する + */ +import { useEffect } from "react"; +import { socketManager } from "@client/network/SocketManager"; +import { appConsts } from "@repo/shared"; +import type { appTypes, roomTypes } from "@repo/shared"; + +type UseSocketSubscriptionsParams = { + completeJoinRequest: () => void; + setMyId: (id: string | null) => void; + setRoom: (room: roomTypes.Room | null) => void; + setScenePhase: (phase: appTypes.ScenePhase) => void; +}; + +/** アプリ共通のソケット購読を登録しクリーンアップするフック */ +export const useSocketSubscriptions = ({ + completeJoinRequest, + setMyId, + setRoom, + setScenePhase, +}: UseSocketSubscriptionsParams): void => { + useEffect(() => { + const handleConnect = (id: string) => { + setMyId(id); + }; + + const handleRoomUpdate = (updatedRoom: roomTypes.Room) => { + completeJoinRequest(); + setRoom(updatedRoom); + setScenePhase(appConsts.ScenePhase.LOBBY); + }; + + const handleGameStart = () => { + setScenePhase(appConsts.ScenePhase.PLAYING); + }; + + socketManager.common.onConnect(handleConnect); + socketManager.lobby.onRoomUpdate(handleRoomUpdate); + socketManager.game.onceGameStart(handleGameStart); + + return () => { + completeJoinRequest(); + socketManager.common.offConnect(handleConnect); + socketManager.lobby.offRoomUpdate(handleRoomUpdate); + }; + }, [completeJoinRequest, setMyId, setRoom, setScenePhase]); +}; diff --git a/apps/client/src/network/handlers/CommonHandler.ts b/apps/client/src/network/handlers/CommonHandler.ts index 76b798a..9f08170 100644 --- a/apps/client/src/network/handlers/CommonHandler.ts +++ b/apps/client/src/network/handlers/CommonHandler.ts @@ -1,17 +1,24 @@ +/** + * CommonHandler + * 接続イベントの購読と解除を扱う共通ハンドラを提供する + * connect イベントをアプリ用の id 通知に変換する + */ import type { Socket } from "socket.io-client"; import { protocol } from "@repo/shared"; -import type { PayloadOf } from "@repo/shared"; +import type { ConnectionLifecyclePayloadOf } from "@repo/shared"; import { createClientSocketEventBridge } from "./socketEventBridge"; +/** 接続イベントを購読解除する共通ハンドラ契約 */ type CommonHandler = { onConnect: (callback: (id: string) => void) => void; offConnect: (callback: (id: string) => void) => void; }; +/** 接続イベント向けの共通ハンドラを生成する */ export const createCommonHandler = (socket: Socket): CommonHandler => { const connectListenerMap = new Map< (id: string) => void, - (payload: PayloadOf) => void + (payload: ConnectionLifecyclePayloadOf) => void >(); const { onEvent, offEvent } = createClientSocketEventBridge(socket); @@ -22,7 +29,7 @@ callback(socket.id || ""); } - const listener = (_payload: PayloadOf) => { + const listener = (_payload: ConnectionLifecyclePayloadOf) => { callback(socket.id || ""); }; @@ -39,4 +46,5 @@ }; }; +/** 接続イベント向けの共通ハンドラ型を再公開 */ export type { CommonHandler }; diff --git a/apps/client/src/network/handlers/LobbyHandler.ts b/apps/client/src/network/handlers/LobbyHandler.ts index 9b1c594..ca0cb41 100644 --- a/apps/client/src/network/handlers/LobbyHandler.ts +++ b/apps/client/src/network/handlers/LobbyHandler.ts @@ -1,15 +1,22 @@ +/** + * LobbyHandler + * ロビー画面で利用するソケット購読と送信を扱うハンドラ + * ルーム更新購読とゲーム開始要求送信を提供する + */ import type { Socket } from "socket.io-client"; import { protocol } from "@repo/shared"; -import type { PayloadOf } from "@repo/shared"; +import type { ServerToClientPayloadOf } from "@repo/shared"; import { createClientSocketEventBridge } from "./socketEventBridge"; +/** ロビー画面で利用する通信操作の契約 */ type LobbyHandler = { - onRoomUpdate: (callback: (room: PayloadOf) => void) => void; - onceRoomUpdate: (callback: (room: PayloadOf) => void) => void; - offRoomUpdate: (callback: (room: PayloadOf) => void) => void; + onRoomUpdate: (callback: (room: ServerToClientPayloadOf) => void) => void; + onceRoomUpdate: (callback: (room: ServerToClientPayloadOf) => void) => void; + offRoomUpdate: (callback: (room: ServerToClientPayloadOf) => void) => void; startGame: () => void; }; +/** ロビー画面向けのソケットハンドラを生成する */ export const createLobbyHandler = (socket: Socket): LobbyHandler => { const { onEvent, onceEvent, offEvent, emitEvent } = createClientSocketEventBridge(socket); @@ -29,4 +36,5 @@ }; }; +/** ロビー画面向けの通信ハンドラ型を再公開 */ export type { LobbyHandler }; diff --git a/apps/client/src/network/handlers/TitleHandler.ts b/apps/client/src/network/handlers/TitleHandler.ts index 04d3a91..06b18d8 100644 --- a/apps/client/src/network/handlers/TitleHandler.ts +++ b/apps/client/src/network/handlers/TitleHandler.ts @@ -1,15 +1,22 @@ +/** + * TitleHandler + * タイトル画面で利用する参加要求送信と拒否購読を扱うハンドラ + * ルーム参加フローの送受信イベントを集約する + */ import type { Socket } from "socket.io-client"; import { protocol } from "@repo/shared"; -import type { PayloadOf } from "@repo/shared"; +import type { ClientToServerPayloadOf, ServerToClientPayloadOf } from "@repo/shared"; import { createClientSocketEventBridge } from "./socketEventBridge"; +/** タイトル画面で利用する通信操作の契約 */ type TitleHandler = { - joinRoom: (payload: PayloadOf) => void; - onJoinRejected: (callback: (payload: PayloadOf) => void) => void; - onceJoinRejected: (callback: (payload: PayloadOf) => void) => void; - offJoinRejected: (callback: (payload: PayloadOf) => void) => void; + joinRoom: (payload: ClientToServerPayloadOf) => void; + onJoinRejected: (callback: (payload: ServerToClientPayloadOf) => void) => void; + onceJoinRejected: (callback: (payload: ServerToClientPayloadOf) => void) => void; + offJoinRejected: (callback: (payload: ServerToClientPayloadOf) => void) => void; }; +/** タイトル画面向けのソケットハンドラを生成する */ export const createTitleHandler = (socket: Socket): TitleHandler => { const { onEvent, onceEvent, offEvent, emitEvent } = createClientSocketEventBridge(socket); @@ -29,4 +36,5 @@ }; }; +/** タイトル画面向けの通信ハンドラ型を再公開 */ export type { TitleHandler }; diff --git a/apps/client/src/network/handlers/socketEventBridge.ts b/apps/client/src/network/handlers/socketEventBridge.ts index aead5bc..143fe00 100644 --- a/apps/client/src/network/handlers/socketEventBridge.ts +++ b/apps/client/src/network/handlers/socketEventBridge.ts @@ -1,40 +1,26 @@ +/** + * socketEventBridge + * クライアント向けソケットイベント bridge を生成する + * 受信イベントと送信イベントの型境界を統一する + */ import type { Socket } from "socket.io-client"; -import type { PayloadOf, SocketPayloadMap } from "@repo/shared"; +import { + createSocketEventBridge, + type ClientToServerEventPayloadMap, + type ConnectionLifecycleEventPayloadMap, + type ServerToClientEventPayloadMap, +} from "@repo/shared"; -type SocketEventName = keyof SocketPayloadMap; +type ClientInboundEventPayloadMap = + & ConnectionLifecycleEventPayloadMap + & ServerToClientEventPayloadMap; +/** クライアント向けの型付きソケットイベント bridge を生成する */ export const createClientSocketEventBridge = (socket: Socket) => { - const onEvent = ( - event: TEvent, - callback: (payload: PayloadOf) => void - ) => { - (socket as any).on(event, callback); - }; - - const onceEvent = ( - event: TEvent, - callback: (payload: PayloadOf) => void - ) => { - (socket as any).once(event, callback); - }; - - const offEvent = ( - event: TEvent, - callback: (payload: PayloadOf) => void - ) => { - (socket as any).off(event, callback); - }; - - function emitEvent(event: TEvent): void; - function emitEvent(event: TEvent, payload: PayloadOf): void; - function emitEvent(event: TEvent, payload?: PayloadOf): void { - if (payload === undefined) { - (socket as any).emit(event); - return; - } - - (socket as any).emit(event, payload); - } + const { onEvent, onceEvent, offEvent, emitEvent } = createSocketEventBridge< + ClientInboundEventPayloadMap, + ClientToServerEventPayloadMap + >(socket as any); return { onEvent, diff --git a/apps/server/src/network/adapters/socketEmitters.ts b/apps/server/src/network/adapters/socketEmitters.ts index fd5be3b..4d75643 100644 --- a/apps/server/src/network/adapters/socketEmitters.ts +++ b/apps/server/src/network/adapters/socketEmitters.ts @@ -3,25 +3,29 @@ * Socket.IOの送信処理を用途別に生成するアダプタ */ import { Server, Socket } from "socket.io"; -import type { PayloadOf, SocketPayloadMap } from "@repo/shared"; +import type { + ServerToClientEventPayloadMap, + ServerToClientPayloadOf, +} from "@repo/shared"; -type SocketEventName = keyof SocketPayloadMap; +type SocketEventName = keyof ServerToClientEventPayloadMap; type EmitToRoom = { (roomId: string, event: TEvent): void; - (roomId: string, event: TEvent, payload: PayloadOf): void; + (roomId: string, event: TEvent, payload: ServerToClientPayloadOf): void; }; type EmitToSocket = { (event: TEvent): void; - (event: TEvent, payload: PayloadOf): void; + (event: TEvent, payload: ServerToClientPayloadOf): void; }; type EmitToAll = { (event: TEvent): void; - (event: TEvent, payload: PayloadOf): void; + (event: TEvent, payload: ServerToClientPayloadOf): void; }; +/** ペイロード有無に応じて emit 呼び出しシグネチャを切り替える共通関数 */ const emitWithOptionalPayload = ( emit: (event: SocketEventName, payload?: unknown) => void, event: SocketEventName, diff --git a/apps/server/src/network/handlers/GameHandler.ts b/apps/server/src/network/handlers/GameHandler.ts index c2be16e..f317903 100644 --- a/apps/server/src/network/handlers/GameHandler.ts +++ b/apps/server/src/network/handlers/GameHandler.ts @@ -1,6 +1,7 @@ /** * GameHandler - * ゲーム関連ハンドラを外部公開する再エクスポート定義 + * ゲーム関連ハンドラの公開窓口を提供する再エクスポートファイル + * ネットワーク層の import 経路を統一する */ -/** ゲームイベント受信ハンドラ登録関数を再公開する */ +/** ゲームイベント受信ハンドラ登録関数を外部参照向けに再公開 */ export { registerGameHandlers } from "./game/registerGameHandlers"; diff --git a/apps/server/src/network/handlers/RoomHandler.ts b/apps/server/src/network/handlers/RoomHandler.ts index 9710aaa..d3d8254 100644 --- a/apps/server/src/network/handlers/RoomHandler.ts +++ b/apps/server/src/network/handlers/RoomHandler.ts @@ -1,6 +1,7 @@ /** * RoomHandler - * ルーム関連ハンドラを外部公開する再エクスポート定義 + * ルーム関連ハンドラの公開窓口を提供する再エクスポートファイル + * ネットワーク層の import 経路を統一する */ -/** ルームイベント受信ハンドラ登録関数を再公開する */ +/** ルームイベント受信ハンドラ登録関数を外部参照向けに再公開 */ export { registerRoomHandlers } from "./room/registerRoomHandlers"; diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index 5b1b631..f96235d 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -21,6 +21,12 @@ import { createServerSocketOnBridge } from "@server/network/handlers/socketEventBridge"; import { createGameOutputAdapter } from "./createGameOutputAdapter"; +/** ゲーム受信イベントごとの入力検証関数を保持するテーブル */ +const gamePayloadValidators = { + [protocol.SocketEvents.PING]: isPingPayload, + [protocol.SocketEvents.MOVE]: isMovePayload, +} as const; + /** ゲームイベントの購読とユースケース呼び出しを設定する */ export const registerGameHandlers = ( io: Server, @@ -34,7 +40,7 @@ // 遅延計測用のPINGを検証しPONGを返す onEvent(protocol.SocketEvents.PING, (clientTime) => { - if (!isPingPayload(clientTime)) { + if (!gamePayloadValidators[protocol.SocketEvents.PING](clientTime)) { logEvent("Network", { event: "PING", result: "ignored_invalid_payload", @@ -71,7 +77,7 @@ // 移動入力を検証しプレイヤー移動ユースケースへ連携する onEvent(protocol.SocketEvents.MOVE, (data) => { - if (!isMovePayload(data)) { + if (!gamePayloadValidators[protocol.SocketEvents.MOVE](data)) { logEvent("Network", { event: "MOVE", result: "ignored_invalid_payload", diff --git a/apps/server/src/network/handlers/room/registerRoomHandlers.ts b/apps/server/src/network/handlers/room/registerRoomHandlers.ts index e799931..5c0145f 100644 --- a/apps/server/src/network/handlers/room/registerRoomHandlers.ts +++ b/apps/server/src/network/handlers/room/registerRoomHandlers.ts @@ -12,6 +12,11 @@ import { isJoinRoomPayload } from "@server/network/validation/socketPayloadValidators"; import { createRoomOutputAdapter } from "./createRoomOutputAdapter"; +/** ルーム受信イベントごとの入力検証関数を保持するテーブル */ +const roomPayloadValidators = { + [protocol.SocketEvents.JOIN_ROOM]: isJoinRoomPayload, +} as const; + /** ルーム参加イベントを検証して参加ユースケースへ連携する */ export const registerRoomHandlers = ( io: Server, @@ -24,7 +29,7 @@ // 参加要求のペイロード検証と参加処理を実行する onEvent(protocol.SocketEvents.JOIN_ROOM, async (data) => { - if (!isJoinRoomPayload(data)) { + if (!roomPayloadValidators[protocol.SocketEvents.JOIN_ROOM](data)) { logEvent("Network", { event: "JOIN_ROOM", result: "ignored_invalid_payload", diff --git a/apps/server/src/network/handlers/socketEventBridge.ts b/apps/server/src/network/handlers/socketEventBridge.ts index 1db3c62..2facfe6 100644 --- a/apps/server/src/network/handlers/socketEventBridge.ts +++ b/apps/server/src/network/handlers/socketEventBridge.ts @@ -1,22 +1,21 @@ +/** + * socketEventBridge + * サーバー向けソケットイベント bridge を生成する + * クライアント受信イベントを型安全に購読する入口を提供する + */ import type { Socket } from "socket.io"; -import type { PayloadOf, SocketPayloadMap } from "@repo/shared"; +import { + createSocketEventBridge, + type ClientToServerEventPayloadMap, + type ServerToClientEventPayloadMap, +} from "@repo/shared"; -type SocketEventName = Exclude; - +/** サーバー向けの型付きソケットイベント bridge を生成する */ export const createServerSocketOnBridge = (socket: Socket) => { - const onEvent = ( - event: TEvent, - callback: (payload: PayloadOf) => void - ) => { - (socket as any).on(event, callback); - }; - - const onceEvent = ( - event: TEvent, - callback: (payload: PayloadOf) => void - ) => { - (socket as any).once(event, callback); - }; + const { onEvent, onceEvent } = createSocketEventBridge< + ClientToServerEventPayloadMap, + ServerToClientEventPayloadMap + >(socket as any); return { onEvent, diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 14354f3..f7973ac 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,24 +1,45 @@ -// shared パッケージ公開 API +/** + * index + * shared パッケージの公開 API を集約して再公開するエントリ + * ドメイン型,プロトコル型,設定値を外部利用向けに束ねる + */ +/** グリッドマップ関連の型定義を再公開 */ export * as gridMapTypes from "./domains/gridMap/gridMap.type"; +/** グリッドマップ関連のロジックを再公開 */ export * as gridMapLogic from "./domains/gridMap/gridMap.logic"; +/** プレイヤー関連の型定義を再公開 */ export * as playerTypes from "./domains/player/player.type"; +/** ゲーム関連の型定義を再公開 */ export * as gameTypes from "./domains/game/game.type"; +/** アプリ状態関連の型定義を再公開 */ export * as appTypes from "./domains/app/app.type"; +/** アプリ状態関連の定数を再公開 */ export * as appConsts from "./domains/app/app.const"; +/** ルーム関連の型定義を再公開 */ export * as roomTypes from "./domains/room/room.type"; +/** ルーム関連の定数を再公開 */ export * as roomConsts from "./domains/room/room.const"; +/** ソケットイベント定義を再公開 */ export * as protocol from "./protocol/events"; +/** ソケットイベント関連の共有型を再公開 */ export type { + ConnectionLifecycleEventPayloadMap, + ConnectionLifecyclePayloadOf, + ClientToServerEventPayloadMap, + ClientToServerPayloadOf, + ServerToClientEventPayloadMap, + ServerToClientPayloadOf, CurrentPlayersPayload, GameStartPayload, MovePayload, NewPlayerPayload, - PayloadOf, PingPayload, PongPayload, RemovePlayerPayload, - SocketPayloadMap, UpdateMapCellsPayload, UpdatePlayersPayload, } from "./protocol/events"; +/** ソケットイベントブリッジ生成関数を再公開 */ +export { createSocketEventBridge } from "./protocol/socketEventBridge"; +/** 共有設定値を再公開 */ export * as config from "./config"; \ No newline at end of file diff --git a/packages/shared/src/protocol/events.ts b/packages/shared/src/protocol/events.ts index a0e3939..f1f207e 100644 --- a/packages/shared/src/protocol/events.ts +++ b/packages/shared/src/protocol/events.ts @@ -36,6 +36,12 @@ GAME_END: "game-end", // 3分経過時のゲーム終了通知 } as const; +/** + * ------------------------------------------------------------ + * ペイロード型定義 + * ------------------------------------------------------------ + */ + /** UPDATE_PLAYERS イベントで送受信するプレイヤー差分配列 */ export type UpdatePlayersPayload = TickData["playerUpdates"]; @@ -66,26 +72,55 @@ serverTime: number; }; -/** ソケットイベントごとのペイロード対応表 */ -export type SocketPayloadMap = { +/** + * ------------------------------------------------------------ + * イベント方向ごとのペイロード対応表 + * ------------------------------------------------------------ + */ + +/** 接続ライフサイクルイベントのペイロード対応表 */ +export type ConnectionLifecycleEventPayloadMap = { [SocketEvents.CONNECT]: undefined; [SocketEvents.DISCONNECT]: undefined; +}; + +/** クライアントからサーバーへ送信するイベントごとのペイロード対応表 */ +export type ClientToServerEventPayloadMap = { [SocketEvents.JOIN_ROOM]: roomTypes.JoinRoomPayload; + [SocketEvents.START_GAME]: undefined; + [SocketEvents.READY_FOR_GAME]: undefined; + [SocketEvents.MOVE]: MovePayload; + [SocketEvents.PING]: PingPayload; +}; + +/** サーバーからクライアントへ送信するイベントごとのペイロード対応表 */ +export type ServerToClientEventPayloadMap = { [SocketEvents.ROOM_JOIN_REJECTED]: roomTypes.JoinRoomRejectedPayload; [SocketEvents.ROOM_UPDATE]: roomTypes.Room; - [SocketEvents.START_GAME]: undefined; [SocketEvents.GAME_START]: GameStartPayload; - [SocketEvents.READY_FOR_GAME]: undefined; [SocketEvents.CURRENT_PLAYERS]: CurrentPlayersPayload; [SocketEvents.NEW_PLAYER]: NewPlayerPayload; [SocketEvents.UPDATE_PLAYERS]: UpdatePlayersPayload; [SocketEvents.REMOVE_PLAYER]: RemovePlayerPayload; - [SocketEvents.MOVE]: MovePayload; [SocketEvents.UPDATE_MAP_CELLS]: UpdateMapCellsPayload; - [SocketEvents.PING]: PingPayload; [SocketEvents.PONG]: PongPayload; [SocketEvents.GAME_END]: undefined; }; -/** 指定イベント名に対応するペイロード型を取得するユーティリティ */ -export type PayloadOf = SocketPayloadMap[TEvent]; +/** + * ------------------------------------------------------------ + * イベント名からペイロード型を引くユーティリティ + * ------------------------------------------------------------ + */ + +/** 接続ライフサイクルイベントのペイロード型を取得するユーティリティ */ +export type ConnectionLifecyclePayloadOf = + ConnectionLifecycleEventPayloadMap[TEvent]; + +/** クライアント送信イベントのペイロード型を取得するユーティリティ */ +export type ClientToServerPayloadOf = + ClientToServerEventPayloadMap[TEvent]; + +/** サーバー送信イベントのペイロード型を取得するユーティリティ */ +export type ServerToClientPayloadOf = + ServerToClientEventPayloadMap[TEvent]; diff --git a/packages/shared/src/protocol/socketEventBridge.ts b/packages/shared/src/protocol/socketEventBridge.ts new file mode 100644 index 0000000..f171a3f --- /dev/null +++ b/packages/shared/src/protocol/socketEventBridge.ts @@ -0,0 +1,57 @@ +type EventPayloadMap = Record; + +type EventNameOf = Extract; + +type SocketBridgeTarget = { + on: (event: string, callback: (payload: unknown) => void) => void; + once: (event: string, callback: (payload: unknown) => void) => void; + off: (event: string, callback: (payload: unknown) => void) => void; + emit: (event: string, payload?: unknown) => void; +}; + +/** + * 受信・送信イベントの型マップを指定して、Socket.IOブリッジを生成する + */ +export const createSocketEventBridge = < + TInboundMap extends EventPayloadMap, + TOutboundMap extends EventPayloadMap, +>(socket: SocketBridgeTarget) => { + const onEvent = >( + event: TEvent, + callback: (payload: TInboundMap[TEvent]) => void + ) => { + (socket as any).on(event, callback); + }; + + const onceEvent = >( + event: TEvent, + callback: (payload: TInboundMap[TEvent]) => void + ) => { + (socket as any).once(event, callback); + }; + + const offEvent = >( + event: TEvent, + callback: (payload: TInboundMap[TEvent]) => void + ) => { + (socket as any).off(event, callback); + }; + + function emitEvent>(event: TEvent): void; + function emitEvent>(event: TEvent, payload: TOutboundMap[TEvent]): void; + function emitEvent>(event: TEvent, payload?: TOutboundMap[TEvent]): void { + if (payload === undefined) { + (socket as any).emit(event); + return; + } + + (socket as any).emit(event, payload); + } + + return { + onEvent, + onceEvent, + offEvent, + emitEvent, + }; +};