From 69753847513ceccff44f5bfc7c6775e15c921bb3 Mon Sep 17 00:00:00 2001 From: lila Date: Sat, 18 Apr 2026 09:54:31 +0200 Subject: [PATCH] feat(api): add game:ready message for client state sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WsGameReadySchema added to shared schemas and WsClientMessageSchema - handleGameReady sends current game:question directly to requesting client socket (not broadcast) — foundation for reconnection slice - router dispatches game:ready to handleGameReady handler --- apps/api/src/ws/handlers/gameHandlers.ts | 28 +++++++++++++++++++++++- apps/api/src/ws/router.ts | 5 ++++- packages/shared/src/schemas/lobby.ts | 7 ++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/api/src/ws/handlers/gameHandlers.ts b/apps/api/src/ws/handlers/gameHandlers.ts index ee06ce8..5ced050 100644 --- a/apps/api/src/ws/handlers/gameHandlers.ts +++ b/apps/api/src/ws/handlers/gameHandlers.ts @@ -1,6 +1,6 @@ import type { WebSocket } from "ws"; import type { User } from "better-auth"; -import type { WsGameAnswer } from "@lila/shared"; +import type { WsGameAnswer, WsGameReady } from "@lila/shared"; import { finishGame, getLobbyByCodeWithPlayers } from "@lila/db"; import { broadcastToLobby, getConnections } from "../connections.js"; import { lobbyGameStore, timers } from "../gameState.js"; @@ -52,6 +52,32 @@ export const handleGameAnswer = async ( } }; +export const handleGameReady = async ( + ws: WebSocket, + msg: WsGameReady, + _user: User, +): Promise => { + const state = await lobbyGameStore.get(msg.lobbyId); + if (!state) throw new NotFoundError("Game not found"); + + const currentQuestion = state.questions[state.currentIndex]; + if (!currentQuestion) throw new NotFoundError("No active question"); + + ws.send( + JSON.stringify({ + type: "game:question", + question: { + questionId: currentQuestion.questionId, + prompt: currentQuestion.prompt, + gloss: currentQuestion.gloss, + options: currentQuestion.options, + }, + questionNumber: state.currentIndex + 1, + totalQuestions: state.questions.length, + }), + ); +}; + export const resolveRound = async ( lobbyId: string, questionIndex: number, diff --git a/apps/api/src/ws/router.ts b/apps/api/src/ws/router.ts index 1a1dad4..d4b04f4 100644 --- a/apps/api/src/ws/router.ts +++ b/apps/api/src/ws/router.ts @@ -6,7 +6,7 @@ import { handleLobbyLeave, handleLobbyStart, } from "./handlers/lobbyHandlers.js"; -import { handleGameAnswer } from "./handlers/gameHandlers.js"; +import { handleGameAnswer, handleGameReady } from "./handlers/gameHandlers.js"; import { AppError } from "../errors/AppError.js"; export type AuthenticatedUser = { session: Session; user: User }; @@ -60,6 +60,9 @@ export const handleMessage = async ( case "game:answer": await handleGameAnswer(ws, msg, auth.user); break; + case "game:ready": + await handleGameReady(ws, msg, auth.user); + break; default: assertExhaustive(msg); } diff --git a/packages/shared/src/schemas/lobby.ts b/packages/shared/src/schemas/lobby.ts index b2e3c55..7b7b7d5 100644 --- a/packages/shared/src/schemas/lobby.ts +++ b/packages/shared/src/schemas/lobby.ts @@ -52,6 +52,12 @@ export const WsLobbyStartSchema = z.object({ export type WsLobbyStart = z.infer; +export const WsGameReadySchema = z.object({ + type: z.literal("game:ready"), + lobbyId: z.uuid(), +}); +export type WsGameReady = z.infer; + export const WsGameAnswerSchema = z.object({ type: z.literal("game:answer"), lobbyId: z.uuid(), @@ -66,6 +72,7 @@ export const WsClientMessageSchema = z.discriminatedUnion("type", [ WsLobbyLeaveSchema, WsLobbyStartSchema, WsGameAnswerSchema, + WsGameReadySchema, ]); export type WsClientMessage = z.infer;