Projeto final: chat real-time com rooms, presence e sync de estado
2 min de leitura
Hora de unir tudo. Voce vai construir um chat real-time com rooms, presence e sync de estado, integrando os 7 nos da trilha num app real que: conecta via WebSocket (ou Socket.IO), tem rooms (canais de chat), mostra presence (quem ta' online + typing indicator), sincroniza estado entre clients (quem mandou o que, ordem das mensagens), e sobrevive a quedas de rede com reconexao automatica.
Esse projeto nao segue o esqueleto de
"explicar conceito + dar exemplo" dos
outros nos. E' um brief de projeto, no
estilo de projects/<slug>.mdx do
aprenda-community. Le ate o fim antes de
comecar.
O que voce vai construir
Um chat real-time completo, entregue como:
- App funcional com 2 ou mais clients conectados (abra 2 abas, mesmo chat, mensagens aparecem em tempo real).
- Rooms (ex:
#geral,#random,#dev) - cada room tem seu canal. - Presence visivel: lista de quem ta' online em cada room, com typing indicator ("Ana esta' digitando...").
- Reconexao robusta: mate o server, os clients reconectam com backoff + jitter automatico, mensagens enviadas durante a queda nao se perdem (fila offline).
- Sync de estado ao reconectar: o
client que reconectou recebe as mensagens
que perdeu (via Last-Event-ID no SSE ou
sinceno Socket.IO).
Stack obrigatoria:
- Frontend: Vanilla JS ou React/Vue
(sua escolha). Conexao via
socket.io-clientouEventSource. - Backend: Node + Express + Socket.IO
(ou
wspuro + custom logic). - Storage: SQLite ou JSON file (simples) pra persistir mensagens.
- Dev:
pnpmounpm, 2 sessoes de terminal abertas (server + 2 clients).
Objetivo
- Praticar ciclo completo de real-time: conexao, mensagens, presence, reconexao, sync de estado.
- Construir um app que funciona com conexao instavel (mate o server, veja reconectar).
- Aplicar rooms/namespaces pra isolar canais de chat.
- Implementar typing indicator com throttling.
- Testar com 2+ abas abertas, simular queda de rede via DevTools (Network > Offline).
Requisitos (minimo)
Conexao e Mensagens:
- Server Socket.IO escutando em
http://localhost:3001. - Client conecta com
io("http://localhost:3001"). - Evento
chat-message(envio) echat-message(recebimento) com payload{ user, text, timestamp, id }. - 2 abas abertas: mensagem de uma aparece na outra em < 100ms.
Rooms:
- Pelo menos 3 rooms pre-definidas:
geral,random,dev. - Client entra na room via
socket.emit("join-room", "geral"). - Mensagens isoladas por room (msg
em
geralnao aparece emdev). -
socket.to(room).emit("chat-message", msg)server-side.
Presence:
- Ao entrar na room, server manda
lista de quem ja' ta' la'
(
presence-listevent). - Quando user novo entra, demais recebem
user-joined(com nome + cor). - Quando user sai (disconnect), demais
recebem
user-left. - Typing indicator: ao digitar no
input, manda
typing(throttled); demais mostram "X esta' digitando...". - Cor por user (hash do userId → cor).
Reconexao e Resiliencia:
- Mate o server, o client mostra "Reconectando..." no UI.
- Client reconecta automaticamente com backoff + jitter (config do Socket.IO).
- Mensagens enviadas durante a queda ficam em fila offline e sao enviadas ao reconectar.
- Ao reconectar, server manda mensagens
que o client perdeu (sync -
sincetimestamp ou Last-Event-ID). - Indicador visual de estado: verde (conectado), amarelo (reconectando), vermelho (desconectado).
UI minima:
- Lista de rooms (sidebar) com botao "Entrar".
- Lista de mensagens (scroll auto).
- Input de mensagem (Enter envia).
- Lista de presence (avatar + nome).
- Indicador "X esta' digitando..." no rodape.
Validacao:
- 2 abas trocam mensagens em < 100ms.
- 2 abas em rooms diferentes nao veem mensagens cruzadas.
- Typing indicator aparece ao digitar na outra aba.
- Mate o server, client mostra "Reconectando", religue, mensagens fluem de novo.
- Mande mensagem durante a queda: religue, mensagem aparece na ordem certa.
- Desconecte uma aba, outra mostra user saindo da presence.
Estrutura do relatorio (real-time-chat.md)
# Real-time Chat - [nome-do-app]
## TL;DR
- App: chat real-time com Socket.IO
- Latencia P50: X ms
- Latencia P95: Y ms
- Reconexao automatica: sim
- Fila offline: sim
- Presence: sim
- Rooms: 3 (geral, random, dev)
## Arquitetura
### Server
- Node + Express + Socket.IO
- Rooms: `geral`, `random`, `dev`
- Mensagens: SQLite (id, room, user, text, timestamp)
- Presence: Map<roomId, Map<userId, Presenca>> em memoria
- Backoff: padrao Socket.IO (1s → 30s, jitter 0.5)
### Client
- Vanilla JS (ou React)
- socket.io-client
- UI: rooms sidebar + chat panel + presence list
- Fila offline: array de mensagens pendentes
## Fluxo de mensagem
1. User digita "Oi" no input.
2. Enter → `socket.emit("chat-message", { text: "Oi" })`.
3. Server: `socket.to(room).emit("chat-message", { user, text, timestamp, id })`.
4. Outros clients: `socket.on("chat-message", ...)` → renderiza na lista.
## Fluxo de reconexao
1. Server cai.
2. Client: `disconnect` event → status "Reconectando".
3. Socket.IO: tenta reconectar com backoff
(1s, 2s, 4s, 8s, 16s, 30s + jitter).
4. Server volta.
5. Client: `connect` event → status "Conectado".
6. Client: re-emite mensagens da fila offline.
7. Client: requisita mensagens perdidas
(sync desde lastTimestamp).
8. Server: SELECT * FROM messages WHERE timestamp > lastTimestamp.
## Resultados
- Latencia P50: X ms (medida com 100 mensagens)
- Reconexao media: Y segundos
- Mensagens perdidas: 0 (com fila offline)
- Typing indicator latencia: Z ms
## Aprendizados
...
Desafios extras (stretch goals)
- Mensagens com read receipts - "Visto por 3" estilo WhatsApp.
- Reply / thread - responder mensagem especifica.
- Edit + delete - editar mensagem enviada (com indicador "editado").
- Reactions - emoji reactions em mensagens.
- File upload - enviar imagem via base64 ou presigned URL.
- Voice messages - gravar audio no browser (MediaRecorder) e enviar.
- Yjs integrado - adicionar Yjs pra documento colaborativo dentro do chat (whiteboard por sala).
- Push notification - quando user nao esta' com a aba aberta, manda push via web-push.
- Auth com JWT - middleware
Socket.IO valida token no
connection. - Redis adapter - scaling horizontal
com
@socket.io/redis-adapter. - Persistencia avancada - Postgres com LISTEN/NOTIFY pra escalar.
- E2E encryption - mensagens criptografadas client-side com libsodium.
- Rate limiting - max 10 mensagens por segundo por user.
- Mute / ban - admins podem silenciar users.
Dicas
Por onde comecar:
- Setup minimo:
pnpm init, instalasocket.io,express,socket.io-client. Server basico com 1 eventochat-messagebroadcast. 2 abas trocam msg. - Rooms:
socket.join("geral")no server,socket.to("geral").emit(...). Teste isolamento (msg emgeralnao aparece emdev). - Presence: mantenha
Map<userId, Presenca>em memoria, sincronize via eventosuser-joined,user-left. - Typing indicator: throttle
typing(manda a cada 1-2s, nao a cada keystroke). Usesocket.volatile.emitpra performance. - Reconexao: Socket.IO ja' faz automatico. Visualize o estado no UI (conectado / reconectando / desconectado).
- Fila offline: array de mensagens
pendentes. Ao reconectar, drena a fila
(
while (fila.length) socket.send(...)). - Sync de estado: salve
timestampda ultima mensagem recebida. Ao reconectar,socket.emit("sync", { since: timestamp }). Server retorna mensagens desde la'.
Armadilhas comuns:
- CORS no Socket.IO. Default bloqueia
outras origens. Configure
cors: { origin: "*" }em dev (ou lista branca em prod). - Disconnect nao significa "user saiu da
room". User pode trocar de aba e voltar
- Socket.IO disconecta/reconecta. Trate
disconnectcomo transitorio se quiser presence precisa, ou implemente heartbeat custom (Socket.IO ja' tem, da' pra adicionar logica de "afk timeout").
- Socket.IO disconecta/reconecta. Trate
- Eventos nao tipados. Use TypeScript ou JSDoc pra documentar payload de cada evento. Em prod, bom typing evita 80% dos bugs.
- Memory leak no Map de presence. Limpe
Map.delete(userId)nodisconnect. Sem isso, Map cresce ate crashar. - Broadcast em vez de
to(room). Cuidado comio.emit()(manda pra TODOS os clients- todas as rooms). Use
io.to(room).emit()pra isolar.
- todas as rooms). Use
- Backoff em loop infinito. Configure
reconnectionAttempts: 10(ou Infinity com UI de "desistir"). Sem limite, fica tentando pra sempre. - Mensagens duplicadas ao reconectar. Client mandou msg, server recebeu, mas resposta nao chegou. Client re-manda (fila), server recebe 2x. Solucao: id de mensagem (UUID) no client, server dedup.
- Sync pesado.
SELECT * FROM messagesretorna milhares. Limite comLIMIT 50e paginacao. - Typing spam. User digitando rapido
= 50 emits/seg. Throttle (1-2s entre
emits) e use
volatile. - Testar com 1 aba so'. Real-time
so' faz sentido com 2+. Abra 2 abas
desde o inicio, ou use
window.openno DevTools.
Como validar que terminou:
- 2 abas trocam mensagens em < 100ms
(mede com
performance.now()). - Mensagens isoladas por room
(msg em
geralnao aparece emdev). - Typing indicator aparece ao digitar na outra aba (com throttling visivel).
- Mate o server, client mostra "Reconectando", religue, mensagens fluem de novo.
- Mande mensagem durante a queda, religue, mensagem aparece na ordem certa (fila offline).
- Desconecte uma aba (fechar), outra mostra user saindo da presence em < 5s.
- Latencia P95 < 200ms (mede com 100 mensagens e calcula percentil).
- Zero mensagens perdidas com reconexao + fila offline (teste 5 quedas seguidas, todas as mensagens aparecem).
Leituras que ajudam durante o projeto:
- Socket.IO Tutorial - Step 1 - setup basico, primeiro chat.
- Yjs Getting Started - se for adicionar colaboracao (stretch).
- MDN - EventSource - alternativa SSE caso queira comparar.
O projeto final e' onde a trilha vira "sua". A escolha de qual chat construir (1-1, grupo, com doc colaborativo) e' o caminho feliz. Os principios - conexao, rooms, presence, reconexao, sync
- aplicam a qualquer app real-time. O esqueleto dado e' o caminho feliz, desvie quando precisar e anote as decisoes no
editorial-decisions.mdda trilha.