Pular para o conteúdo principal

WebSocket

O feed ao vivo. Uma conexão por batalha, por espectador.

wss://slotbattle.example.com/ws?battle_id=btl_CIkE8lctOrFDe1hJlG0T
Sec-WebSocket-Protocol: slotbattle.jwt, <token de viewer>

O feed roda na própria porta

/ws não roda na porta REST. Ele é servido por um servidor HTTP separado, porque uma conexão feita para ficar aberta durante uma batalha inteira não sobrevive ao write timeout global da superfície REST. Por padrão a API REST escuta em 8070 e o feed em 8071.

A consequência para você: use o ws_url que a API retorna em vez de montar um a partir da URL base REST. Veja Arquitetura.

Um ws_url na porta REST significa que falta uma configuração

Se a instância não declara uma URL base pública de WebSocket, o ws_url é derivado do host da requisição e volta apontando para a porta REST, onde nada aceita o upgrade. Verificado ao vivo: com a configuração vazia, o control plane retornou um ws_url na própria porta REST enquanto o feed escutava em outra.

Se o ws_url não conecta, é a primeira coisa a reportar ao seu host. Você não consegue corrigir isso do seu lado.

O handshake

Browsers não conseguem definir headers arbitrários num handshake de WebSocket, então o token viaja na lista de subprotocolos, o único header que eles controlam:

const ws = new WebSocket(wsUrl, ['slotbattle.jwt', token])

Duas entradas: o nome fixo slotbattle.jwt, depois o token cru. O servidor negocia de volta slotbattle.jwt.

Verificado contra uma instância em execução:

HandshakeResposta
Token válido101 Switching Protocols, Sec-WebSocket-Protocol: slotbattle.jwt
Sem token401 Unauthorized
Token inválido ou expirado401 Unauthorized
battle_id ausente400 Bad Request
O feed não usa o envelope JSON de erro

Um handshake recusado responde texto puro, e não {"error":{...}}, porque é outro servidor. Não escreva um cliente que tente fazer parse de um upgrade falho como JSON.

Um token emitido para uma batalha diferente da que está no battle_id é recusado: o claim e o parâmetro de query precisam concordar.

O que chega

Os frames são eventos JSON. Só oito tipos chegam a um browser:

lobby.updatedUm assento mudou
battle.startedAssentos despachados, a gravação começa
spin.startedUm giro começou em algum assento
segment.newNovo segmento de vídeo disponível
playlist.updateA playlist HLS mudou
battle.completedTodos os assentos reportaram
battle.failedA batalha falhou
battle.cancelledA batalha foi cancelada

Todo o resto do vocabulário, incluindo browser.ready, capture.*, click.* e user.*, é interno e nunca vai chegar. Veja Eventos para o envelope e a lista completa.

ws.onmessage = (e) => {
const ev = JSON.parse(e.data)
switch (ev.type) {
case 'lobby.updated': return renderSeats(ev.data)
case 'battle.started': return startPlayers()
case 'spin.started': return flashSeat(ev.user_id)
case 'segment.new':
case 'playlist.update': return nudgePlayer(ev.user_id)
case 'battle.completed':
case 'battle.failed':
case 'battle.cancelled': return showTerminal(ev)
}
}

Não há replay

Um espectador que conecta no meio da batalha vê o que acontece dali em diante, não o histórico. Nada é bufferizado e nada é reentregue.

Projete para isso: busque o estado atual da batalha no seu backend quando a página carrega, renderize isso, e deixe o socket aplicar mudanças por cima. Um front end que assume ter visto a sequência desde o começo renderiza um lobby quebrado para quem chegou tarde ou reconectou.

Reconectando

O socket pode fechar por motivos comuns: mudança de rede, notebook dormindo, idle timeout de proxy. Um loop de reconexão que funciona:

  1. Reconecte com backoff e jitter, com teto de poucos segundos.
  2. Busque o estado da batalha de novo, porque você perdeu eventos enquanto esteve desconectado.
  3. Se faltar menos de um minuto para o token expirar, emita um novo pelo seu backend antes.
  4. Pare quando a batalha chegar a um estado terminal; não há mais nada para receber.

Um 401 na reconexão quase sempre significa token expirado, e não revogado. Emita e tente de novo uma vez antes de mostrar erro.

Vídeo

O feed não carrega vídeo. segment.new e playlist.update informam que o stream HLS de um assento avançou; o vídeo em si vem da CDN, no hls_url do assento.

Aponte um player HLS por assento para essa URL e deixe os eventos avisarem para buscar novos segmentos. Para uma batalha ainda em andamento, veja o padrão de playlist pelo socket em Assistir ao vivo.

Próximo