feat(api): add game:ready message for client state sync

- 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
This commit is contained in:
lila 2026-04-18 09:54:31 +02:00
parent 4d4715b4ee
commit 6975384751
3 changed files with 38 additions and 2 deletions

View file

@ -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<void> => {
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,

View file

@ -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);
}

View file

@ -52,6 +52,12 @@ export const WsLobbyStartSchema = z.object({
export type WsLobbyStart = z.infer<typeof WsLobbyStartSchema>;
export const WsGameReadySchema = z.object({
type: z.literal("game:ready"),
lobbyId: z.uuid(),
});
export type WsGameReady = z.infer<typeof WsGameReadySchema>;
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<typeof WsClientMessageSchema>;