export class GameConnectionError extends Error { constructor(code, message) { super(message) this.code = code // 'connection' | 'join_timeout' | 'closed' } } // Опкоды протокола game-server const OP_JOIN = 1 const OP_END = 2 const OP_STATE = 3 const OP_ACK = 4 const CLOSE_REASONS = { 4000: 'Соединение заменено новым', 4001: 'Ожидалось сообщение JOIN', 4002: 'Некорректное сообщение', 4003: 'Недействительный игровой токен', 4004: 'Вы не участник этой партии', } /** * Подключается к game-server и отправляет JOIN. Резолвится, когда партия * началась (получен STATE с кодом 1 — расстановка кораблей). Возвращает * объект с promise и функцией отмены `cancel()`. * * @param {{ gameServerUrl: string, gameToken: string, userToken: string }} session */ export function createGameConnection({ gameServerUrl, gameToken, userToken }) { let ws = null let settled = false let lastSeq = 0 const promise = new Promise((resolve, reject) => { const fail = (err) => { if (settled) return settled = true cleanup() reject(err) } const succeed = (connection) => { if (settled) return settled = true resolve(connection) // Сокет НЕ закрываем — соединение переиспользуется для игры ws.onerror = null ws.onclose = (event) => { if (connection.onClose) connection.onClose(event.code) } } ws = new WebSocket(gameServerUrl) ws.onopen = () => { ws.send(JSON.stringify([OP_JOIN, gameToken, userToken])) } ws.onmessage = (event) => { let msg try { msg = JSON.parse(event.data) } catch { return } if (!Array.isArray(msg) || typeof msg[0] !== 'number') return const [opcode, seq] = msg if (typeof seq === 'number' && seq > lastSeq) { lastSeq = seq ack(seq) } if (opcode === OP_STATE) { const state = msg[2] if (state === 1) { succeed({ socket: ws, send: sendMessage }) } } else if (opcode === OP_END) { const code = msg[2] const reason = msg[3] || 'Партия завершена' fail(new GameConnectionError(code === -1 ? 'join_timeout' : 'closed', reason)) } } ws.onerror = () => { if (!settled) fail(new GameConnectionError('connection', 'Сетевая ошибка')) } ws.onclose = (event) => { if (!settled) { const message = CLOSE_REASONS[event.code] || (event.code === 1000 ? 'Соединение закрыто' : 'Соединение прервано') fail(new GameConnectionError('closed', message)) } } }) function sendMessage(payload) { if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(payload)) } } function ack(seq) { sendMessage([OP_ACK, seq]) } function cleanup() { if (ws) { ws.onopen = ws.onmessage = ws.onerror = ws.onclose = null ws.close() ws = null } } function cancel() { if (settled) return settled = true cleanup() } return { promise, cancel } }