feat(api): add WebSocket foundation and multiplayer game store
- Add ws/ directory: server setup, auth, router, connections map - WebSocket auth rejects upgrade with 401 if no Better Auth session - Router parses WsClientMessageSchema, dispatches to handlers, two-layer error handling (AppError -> WsErrorSchema, unknown -> 500) - connections.ts: in-memory Map<lobbyId, Map<userId, WebSocket>> with addConnection, removeConnection, broadcastToLobby - LobbyGameStore interface + InMemoryLobbyGameStore implementation following existing GameSessionStore pattern - multiplayerGameService: generateMultiplayerQuestions() decoupled from single-player flow, hardcoded defaults en->it nouns easy 3 rounds - handleLobbyJoin and handleLobbyLeave implemented - WsErrorSchema added to shared schemas - server.ts switched to createServer + setupWebSocket
This commit is contained in:
parent
b0aef8cc16
commit
745c5c4e3a
14 changed files with 443 additions and 1 deletions
52
apps/api/src/ws/index.ts
Normal file
52
apps/api/src/ws/index.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { WebSocketServer } from "ws";
|
||||
import type { WebSocket } from "ws";
|
||||
import type { Server } from "http";
|
||||
import type { IncomingMessage } from "http";
|
||||
import { handleUpgrade } from "./auth.js";
|
||||
import { handleMessage, type AuthenticatedUser } from "./router.js";
|
||||
import { removeConnection } from "./connections.js";
|
||||
import { handleLobbyLeave } from "./handlers/lobbyHandlers.js";
|
||||
|
||||
export const setupWebSocket = (server: Server): WebSocketServer => {
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
|
||||
server.on("upgrade", (request, socket, head) => {
|
||||
if (request.url !== "/ws") {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
handleUpgrade(request, socket, head, wss);
|
||||
});
|
||||
|
||||
wss.on(
|
||||
"connection",
|
||||
(ws: WebSocket, _request: IncomingMessage, auth: AuthenticatedUser) => {
|
||||
ws.on("message", (rawData) => {
|
||||
handleMessage(ws, rawData, auth);
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
handleDisconnect(ws, auth);
|
||||
});
|
||||
|
||||
ws.on("error", (err) => {
|
||||
console.error(`WebSocket error for user ${auth.user.id}:`, err);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return wss;
|
||||
};
|
||||
|
||||
const handleDisconnect = async (
|
||||
ws: WebSocket,
|
||||
auth: AuthenticatedUser,
|
||||
): Promise<void> => {
|
||||
if (!ws.lobbyId) return; // user connected but never joined a lobby
|
||||
removeConnection(ws.lobbyId, auth.user.id);
|
||||
await handleLobbyLeave(
|
||||
ws,
|
||||
{ type: "lobby:leave", lobbyId: ws.lobbyId },
|
||||
auth.user,
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue