import type { ServerMessage, ClientMessage } from "./protocol"; import type { Session } from "./types"; import { DomainError } from "./errors"; import { addChatMessage, approveTakeover, bankerAdjust, bankerForceTransfer, createChatGroup, createDummyPlayer, disconnectPlayer, requestTakeover, setBlackout, snapshotSession, startSession, endSession, transfer, } from "./domain"; import { getSession, removeSession } from "./store"; import { now } from "./util"; import { notifyChat, notifyTransaction } from "./notifications"; const socketsBySession = new Map>(); const metaBySocket = new WeakMap(); const testTimers = new Map>(); function randomInt(min: number, max: number): number { return Math.floor(Math.random() * (max - min + 1)) + min; } function scheduleTestTransfer(sessionId: string): void { const delay = randomInt(3000, 7000); const timer = setTimeout(() => { testTimers.delete(sessionId); runTestTransfer(sessionId); }, delay); testTimers.set(sessionId, timer); } function runTestTransfer(sessionId: string): void { const session = getSession(sessionId); if (!session || !session.isTest) { return; } if (session.status !== "active") { return; } const players = Array.from(session.players.values()).filter( (player) => player.role === "player", ); if (players.length < 2) { scheduleTestTransfer(sessionId); return; } let attempts = 0; while (attempts < 5) { const from = players[randomInt(0, players.length - 1)]; let to = players[randomInt(0, players.length - 1)]; if (to.id === from.id) { attempts += 1; continue; } const amount = randomInt(10, 250); if (from.balance < amount) { attempts += 1; continue; } try { const transaction = transfer(session, from.id, to.id, amount, null); notifyTransaction(session, transaction); sendStateToSession(session); break; } catch { attempts += 1; } } scheduleTestTransfer(sessionId); } export function startTestSimulation(sessionId: string): void { if (testTimers.has(sessionId)) return; scheduleTestTransfer(sessionId); } export function stopTestSimulation(sessionId: string): void { const timer = testTimers.get(sessionId); if (timer) { clearTimeout(timer); } testTimers.delete(sessionId); } function getSessionSockets(sessionId: string): Set { let set = socketsBySession.get(sessionId); if (!set) { set = new Set(); socketsBySession.set(sessionId, set); } return set; } export function registerSocket(ws: WebSocket, sessionId: string, playerId: string): void { const session = getSession(sessionId); if (!session) { ws.close(1008, "Session not found"); return; } const player = session.players.get(playerId); if (!player) { ws.close(1008, "Player not found"); return; } player.connected = true; player.isDummy = false; player.lastActiveAt = now(); metaBySocket.set(ws, { sessionId, playerId }); getSessionSockets(sessionId).add(ws); sendStateToSession(session); } export function unregisterSocket(ws: WebSocket): void { const meta = metaBySocket.get(ws); if (!meta) { return; } const { sessionId, playerId } = meta; const session = getSession(sessionId); if (session) { disconnectPlayer(session, playerId); sendStateToSession(session); } const set = socketsBySession.get(sessionId); if (set) { set.delete(ws); if (set.size === 0) { socketsBySession.delete(sessionId); if (session?.isTest) { stopTestSimulation(sessionId); removeSession(sessionId); } } } metaBySocket.delete(ws); } export function handleSocketMessage(ws: WebSocket, raw: string | ArrayBuffer): void { const messageText = typeof raw === "string" ? raw : new TextDecoder().decode(raw); let parsed: ClientMessage; try { parsed = JSON.parse(messageText) as ClientMessage; } catch (error) { send(ws, { type: "error", message: "Invalid message" }); return; } const session = getSession(parsed.sessionId); if (!session) { send(ws, { type: "error", message: "Session not found" }); return; } try { handleMessage(session, parsed); } catch (error) { const message = error instanceof DomainError ? error.message : "Unexpected error while processing request"; send(ws, { type: "error", message }); return; } sendStateToSession(session); } function handleMessage(session: Session, message: ClientMessage): void { switch (message.type) { case "chat_send": { const chat = addChatMessage(session, message.playerId, message.body, message.groupId); notifyChat(session, chat); return; } case "transfer": { const transaction = transfer( session, message.playerId, message.toPlayerId, message.amount, message.note, ); notifyTransaction(session, transaction); return; } case "banker_adjust": { const transaction = bankerAdjust( session, message.bankerId, message.targetId, message.amount, message.note, ); notifyTransaction(session, transaction); return; } case "banker_force_transfer": { const transaction = bankerForceTransfer( session, message.bankerId, message.fromId, message.toId, message.amount, message.note, ); notifyTransaction(session, transaction); return; } case "banker_blackout": setBlackout(session, message.bankerId, message.active, message.reason); return; case "banker_start": startSession(session, message.bankerId); return; case "banker_end": endSession(session, message.bankerId); return; case "banker_create_dummy": createDummyPlayer(session, message.bankerId, message.name, message.balance); return; case "group_create": createChatGroup(session, message.playerId, message.name, message.memberIds); return; case "takeover_request": requestTakeover(session, message.playerId, message.dummyId); return; case "banker_takeover_approve": { const assignedId = approveTakeover( session, message.bankerId, message.dummyId, message.requesterId, ); notifyTakeoverApproval(session.id, message.requesterId, assignedId); return; } case "ping": touchPlayer(session, message.playerId); return; default: return; } } function touchPlayer(session: Session, playerId: string): void { const player = session.players.get(playerId); if (player) { player.lastActiveAt = now(); } } function sendStateToSession(session: Session): void { const sockets = socketsBySession.get(session.id); if (!sockets) { return; } const stateMessage: ServerMessage = { type: "state", session: snapshotSession(session), }; sockets.forEach((socket) => send(socket, stateMessage)); } export function broadcastSessionState(sessionId: string): void { const session = getSession(sessionId); if (!session) return; sendStateToSession(session); } function notifyTakeoverApproval( sessionId: string, requesterId: string, assignedId: string, ): void { const sockets = socketsBySession.get(sessionId); if (!sockets) { return; } sockets.forEach((socket) => { const meta = metaBySocket.get(socket); if (!meta) { return; } if (meta.playerId === requesterId) { send(socket, { type: "takeover_approved", assignedPlayerId: assignedId }); meta.playerId = assignedId; } }); } function send(ws: WebSocket, message: ServerMessage): void { if (ws.readyState !== WebSocket.OPEN) { return; } ws.send(JSON.stringify(message)); }