Resetar o estado de conexão da chamada ao desconectar
TLDR:
connectionStatusnunca volta paraDisconnected, então quem sai da sessão e entra de novo sem recarregar a página não reconecta ao Twilio e não publica câmera nem microfone para o outro participante.
Contexto
Relato: um usuário está em sessão, sai da sessão e entra de novo — a câmera dele não aparece para a outra pessoa. Ele mesmo continua vendo a própria imagem e a UI se comporta como se estivesse tudo conectado. Só um F5 (feito por quem saiu) restaura o comportamento normal.
Causa raiz confirmada na investigação:
- O
callManageré um singleton de módulo, criado fora do React emsrc/providers/CallManager/provider.tsx:11. Ele sobrevive a qualquer navegação client-side — e a entrada na sala vem de umnext/link(src/components/ui/LinkButton/index.tsx,component = Link), ou seja, navegação sem reload. connectionStatus(src/infra/CallManager/modules/connectionManager.ts:16) só é escrito paraConnectingeConnected. Nemdisconnect()(:47), nemdestroy()(:98), nem ocatchdoconnect()(:152) devolvem o estado paraDisconnected.connect()(:132-138) faz early return quando o status éConnectingouConnected. Na segunda entrada na sala ele retorna imediatamente sem conectar nada.- Sem conexão, o evento
LocalParticipantConnectednunca é emitido;src/containers/Call/hooks/useStreaming.ts:44-60depende desse evento para marcarconnected, entãostartStreaming()nunca roda e as tracks locais nunca são publicadas. O outro participante não recebe vídeo nem áudio. - O preview local não depende da sala (é attach direto na track), por isso quem voltou vê a própria imagem e não percebe o problema.
src/containers/Call/hooks/useConnection.ts:34chamasetConnected(true)mesmo quandoconnect()retornouundefined, então a UI declara “conectado” sobre uma conexão que não existe.- O F5 recria o módulo,
connectionStatusvolta aDisconnectede a conexão acontece normalmente.
Há um caminho adicional que produz o mesmo estado travado sem o usuário sair da página: initialize() registra disconnect em pagehide (:95). No mobile, colocar a aba em background dispara pagehide e derruba a sala; ao voltar (inclusive via bfcache, sem reexecutar módulos) o status continua Connected e nada reconecta.
O mesmo defeito trava a chamada em um segundo cenário: se o connect() falhar, o status fica preso em Connecting para sempre, e nenhuma tentativa posterior de conectar acontece até um reload.
Objetivos
- Devolver
connectionStatusparaDisconnectedem todo caminho que encerra ou falha a conexão, para que uma nova entrada na sala reconecte de fato - Zerar a referência de
roomjunto com o status, para questartStreaming/stopStreamingnão operem sobre uma sala morta - Impedir que a UI se declare conectada quando
connect()não devolveu uma sala
Fora de escopo
- O registro duplicado de
RemoteParticipantUnavailableemconnectionManager.ts:80-87, que vaza um listener por ciclo de navegação (achado da investigação, spec separada) - A blindagem de
src/containers/Call/hooks/useRemoteStreams.ts, que anula o stream remoto porkindsem verificar se a track removida é a que está em uso — tela preta latente para quem fica na sala, mas não é a causa deste relato (achado da investigação, spec separada) - Reconexão automática ao voltar de
pagehide/bfcache. Este fix garante que uma nova entrada na sala funcione; reconectar sozinho ao voltar do background é mudança de comportamento e fica para outra spec - Trocar o singleton de módulo por instância por mount do provider
Mudanças
| Arquivo | O que muda |
|---|---|
src/infra/CallManager/modules/connectionManager.ts |
disconnect() passa a aguardar strategy.disconnect() e, ao final, seta connectionStatus = EConnectionStatus.Disconnected e room = null. destroy() também reseta connectionStatus (hoje só zera room). O catch do connect() reseta connectionStatus para Disconnected e room para null, para que uma falha não trave todas as tentativas seguintes. Se strategy.connect() devolver algo falsy, tratar como falha (mesmo caminho do catch) em vez de marcar Connected. |
src/containers/Call/hooks/useConnection.ts |
connect() só chama setConnected(true) (e identifyUser) quando callManager.connect() devolveu uma sala; sem sala, apenas loga o erro e mantém isConnected como false, permitindo nova tentativa. |
src/infra/CallManager/modules/__tests__/connectionManager.test.ts |
Novo arquivo de teste. Casos: (a) connect → disconnect → connect conecta de novo e emite LocalParticipantConnected na segunda vez; (b) connect → destroy → connect conecta de novo; (c) uma falha em strategy.connect não impede o connect seguinte de funcionar; (d) chamadas concorrentes de connect continuam protegidas pelo early return enquanto o status é Connecting/Connected (não regredir a proteção contra conexão dupla). |
Como verificar
Automatizado:
bash
make run.test path=src/infra/CallManager
O teste do caso (a) deve falhar antes do fix e passar depois.
Manual, com dois usuários na mesma sessão:
- Usuário A e usuário B entram na sessão; ambos se veem.
- Usuário A sai da sala por navegação client-side (sem reload) e entra de novo pela sala de espera.
- Esperado: B volta a ver a câmera e ouvir o áudio de A, sem ninguém precisar dar F5.
- Nos logs do console de A na segunda entrada deve aparecer o
Twilio connecteddeconnectionFactory.ts, e não deve aparecerthere is no room yetdestreamManager.tsao alternar câmera/microfone. - Regressão a checar: entrar pela primeira vez continua conectando uma única vez (sem sala duplicada) e o botão “Encerrar Chamada” continua levando para o pós-atendimento.
Documentação
.project/docs/learnings/: registrar o aprendizado sobre estado mutável em singleton de módulo em app Next.js — sobrevive à navegação client-side e só é limpo por reload, então todo estado de ciclo de vida precisa de reset explícito no caminho de encerramento..project/docs/rules/: nenhuma regra de negócio muda.