add matchmaking progress screen
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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 }
|
||||
}
|
||||
Reference in New Issue
Block a user