R-002 — Transição sala de espera → chamada exige ACK bilateral
TLDR: ninguém navega da sala de espera para a chamada com base em um sinal enviado uma única vez; a transição só acontece após confirmação bilateral (ack) ou por um dos fallbacks explícitos, e a presença é sempre gravada antes do redirect.
Given / When / Then
Dado que dois participantes estão na sala de espera de uma reunião
Quando um deles clica em “Entrar”
Então ele entra em estado requesting, reenviando join_request (com requestId idempotente) a cada 1 segundo — nunca navega nesse instante
Dado que um participante em requesting recebe um join_request do outro lado (clique simultâneo) ou um join_ack com o mesmo requestId
Quando essa confirmação chega
Então ele responde join_ack (se aplicável), grava a própria presença com fetch keepalive (aguardando a conclusão) e só então navega para /app/video/{id}
Dado que um participante já está dentro da chamada
Quando ele recebe um join_request (de alguém saindo e reentrando, ou de um cliente legado ainda enviando WaitingForRemote)
Então ele responde join_ack imediatamente, permitindo reentrada sem depender da janela de presença
Restrições
- Semântica do ack:
join_acksignifica “estou pronto, pode navegar”. Só envia ack quem está emrequestingou já está dentro da call — nunca quem está apenas parado na sala de espera sem ter clicado. - Fallback legado (rollout): se
requestinghá mais de 5s eisRemoteWaiting(sinalWaitingForRemoteou presença HTTP) estátruesem nenhum ack ter chegado, confirma mesmo assim — cobre o período de deploy misto (cliente novo ↔ cliente antigo) e o caso do socket estar indisponível. - Janela de frescor de presença:
last_seenmais novo que 45 segundos é considerado “participante presente” (PRESENCE_FRESHNESS_SECONDSemsrc/hooks/useMeetingLastSeen.ts). Não é mais 4 minutos. - Toda escrita de presença que pode coincidir com navegação/unload usa
fetchcomkeepalive: true(persistPresenceBeforeLeave), nuncaaxios— o browser garante a entrega mesmo após a página começar a descarregar.sendBeaconnão é usado porque o endpoint éPUT. - Quem está dentro da chamada mantém presença sempre ativa (
useMeetingLastSeencomuseInterval: trueeisAloneInCall: truefixos emCall.tsx, não só quando sozinho) e respondejoin_acka qualquerjoin_requestrecebido no canal da wait room (useCallPresence) — é isso que permite reentrada imediata sem esperar a call ficar vazia. - O servidor
chat-api(chat.trg.club) é um relay puro sem histórico: mensagem emitida enquanto o destinatário está desconectado é perdida para sempre. O reenvio de 1s dorequestingé o que compensa essa ausência de garantia de entrega — não adicionar lógica que dependa de “a mensagem sempre chega”.
Teste vinculado
useJoinRoomFlow.test.ts (máquina de estados idle → requesting → confirmed), useParticipantDetection.test.tsx (handshake), useCallPresence.test.ts (ack de reentrada), useMeetingLastSeen.test.ts (keepalive, janela de 45s).