137 lines
3.6 KiB
JavaScript
137 lines
3.6 KiB
JavaScript
import { wsUrl } from './api'
|
|
|
|
const CHANNEL_IDENTIFIER = JSON.stringify({ channel: 'MatchmakingChannel' })
|
|
|
|
export class MatchmakingError extends Error {
|
|
constructor(code, message) {
|
|
super(message)
|
|
this.code = code // 'connection' | 'timeout' | 'rejected'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Подключается к main-server по WebSocket (Action Cable) и инициирует
|
|
* поиск соперника. Возвращает объект с promise (резолвится при событии
|
|
* `matched`) и функцией отмены `cancel()`.
|
|
*
|
|
* @param {string} token JWT пользователя
|
|
* @param {(status: 'connecting' | 'searching') => void} onStatus
|
|
*/
|
|
export function createMatchmaking(token, onStatus) {
|
|
let ws = null
|
|
let settled = false
|
|
|
|
const send = (payload) => {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(payload))
|
|
}
|
|
}
|
|
|
|
const promise = new Promise((resolve, reject) => {
|
|
const fail = (err) => {
|
|
if (settled) return
|
|
settled = true
|
|
cleanup()
|
|
reject(err)
|
|
}
|
|
|
|
const succeed = (match) => {
|
|
if (settled) return
|
|
settled = true
|
|
cleanup()
|
|
resolve(match)
|
|
}
|
|
|
|
onStatus('connecting')
|
|
|
|
ws = new WebSocket(wsUrl(`/cable?token=${encodeURIComponent(token)}`))
|
|
let subscribed = false
|
|
|
|
ws.onopen = () => {
|
|
send({ command: 'subscribe', identifier: CHANNEL_IDENTIFIER })
|
|
}
|
|
|
|
ws.onmessage = (event) => {
|
|
let msg
|
|
try {
|
|
msg = JSON.parse(event.data)
|
|
} catch {
|
|
return
|
|
}
|
|
|
|
if (msg.type === 'confirm_subscription') {
|
|
subscribed = true
|
|
onStatus('searching')
|
|
send({
|
|
command: 'message',
|
|
identifier: CHANNEL_IDENTIFIER,
|
|
data: JSON.stringify({ action: 'start' }),
|
|
})
|
|
return
|
|
}
|
|
|
|
if (msg.type === 'reject_subscription') {
|
|
fail(new MatchmakingError('rejected', 'Не удалось подписаться на поиск соперника'))
|
|
return
|
|
}
|
|
|
|
if (msg.type === 'ping' || msg.type === 'welcome') return
|
|
|
|
const message = msg.message
|
|
if (!message || typeof message !== 'object') return
|
|
|
|
if (message.type === 'matched') {
|
|
succeed({
|
|
matchId: message.match_id,
|
|
gameToken: message.game_token,
|
|
gameServerUrl: message.game_server,
|
|
players: message.players,
|
|
})
|
|
} else if (message.type === 'timeout') {
|
|
fail(new MatchmakingError('timeout', 'Соперник не найден'))
|
|
}
|
|
}
|
|
|
|
ws.onerror = () => {
|
|
if (!settled) fail(new MatchmakingError('connection', 'Сетевая ошибка'))
|
|
}
|
|
|
|
ws.onclose = (event) => {
|
|
if (!settled) {
|
|
const err = new MatchmakingError('connection', 'Соединение прервано')
|
|
err.closeCode = event.code
|
|
fail(err)
|
|
}
|
|
}
|
|
|
|
// Подписка не подтверждена — защита от «зависшего» соединения
|
|
setTimeout(() => {
|
|
if (!settled && !subscribed) {
|
|
fail(new MatchmakingError('connection', 'Не удалось подключиться к серверу'))
|
|
}
|
|
}, 10000)
|
|
})
|
|
|
|
function cleanup() {
|
|
if (ws) {
|
|
ws.onopen = ws.onmessage = ws.onerror = ws.onclose = null
|
|
ws.close()
|
|
ws = null
|
|
}
|
|
}
|
|
|
|
/** Явная отмена поиска: команда `stop` + закрытие сокета. */
|
|
function cancel() {
|
|
if (settled) return
|
|
settled = true
|
|
send({
|
|
command: 'message',
|
|
identifier: CHANNEL_IDENTIFIER,
|
|
data: JSON.stringify({ action: 'stop' }),
|
|
})
|
|
cleanup()
|
|
}
|
|
|
|
return { promise, cancel }
|
|
}
|