Gateway
O Trivo empurra os eventos, e o seu código só registra handlers. O que o SDK faz por baixo — abrir, bater, retomar, recuar e desistir — está descrito aqui para você reconhecer no log, não para reescrever.
Ouvir
import { createApp } from "@trivo/sdk";
const app = createApp({ intents: ["messages", "members"] });
app.on("ready", () => console.log("conectado"));
app.on("message_create", (evento) => { /* … */ });
await app.connect();
// e, quando o processo for descer, devolvendo a vaga do teto:
await app.disconnect();Os nomes de evento são os mesmos que viajam no fio — message_create, e não messageCreate. Um contrato só, sem tabela de tradução: o nome que você lê aqui é o que você vê no JSON e o que escreve no editor.
Os três canais
app.on("message_create", (evento) => {}); // o que aconteceu no Trivo
app.onStatus((status) => {}); // como está a minha conexão
app.onError((erro, { event }) => {}); // o MEU handler lançouSem nenhum onStatus registrado, o SDK escreve uma linha de console.error quando desiste — nunca silêncio. E um handler seu que lança não derruba a conexão: ele vira onError.
Referência de transporte
O formato do quadro
O que viaja no fio, para transparência, depuração e compatibilidade — não é um segundo jeito de construir App. Se você está escrevendo um App, o SDK já faz isto.
A rota é GET /api/apps/eventos, a resposta é text/event-stream, e o parâmetro intents leva as famílias separadas por vírgula. Não há Identify, não há Hello e não há Ready: a credencial vai no cabeçalho da própria requisição, e o primeiro quadro já é evento.
data: {"type":"connection_open","connectionId":"…","version":1}
id: 41
data: {"type":"message_create","serverId":"…","message":{…}}
: batimento
id: 42
data: {"type":"reaction_update","serverId":"…","emoji":"👍","count":2,…}O tipo do evento viaja dentro do JSON, e não no campo event: do protocolo. O id: é a sequência, e é ela que a retomada usa. A versão da API vai no cabeçalho x-trivo-api-version, nunca em parâmetro, e o connection_open devolve a que aquela conexão está usando.
O batimento, e quem detecta o silêncio
A cada 25 segundos o Trivo escreve um comentário (: batimento) no fluxo. Ele não é evento e não pede resposta: existe para dar o que ler a quem está esperando.
É o sentido inverso do batimento com confirmação que quem vem de outra plataforma espera: não há nada a responder, há um relógio a manter — passou tempo demais sem nenhum byte, a conexão está morta mesmo que o sistema operacional ainda a ache viva. Esse relógio é do SDK, e ele reconecta sozinho; o que chega até você é um onStatus.
Retomada
O SDK guarda o último id: recebido e o devolve em Last-Event-ID ao reconectar. A janela é de 500 eventos ou 120 segundos, o que vier primeiro.
- Dentro da janela: você recebe
connection_resumee, logo atrás, exatamente o intervalo perdido. - Fora da janela: você recebe
connection_open— que significa recomece. O estado anterior não vale mais.
A reentrega é real, e é a única parte que o SDK não pode decidir por você: mande clientRef quando escrever em resposta a um evento. Sem ele, o mesmo evento reentregue produz a mesma mensagem duas vezes.
O fecho com motivo
O Gateway não fecha mudo. Antes de encerrar, ele escreve um quadro de despedida — e é o campo reconnect que a sua máquina lê:
data: {
"type": "connection_close",
"code": "CREDENTIAL_REVOKED",
"message": "A credencial que abriu esta conexão foi revogada. Reconecte apenas com a credencial nova.",
"reconnect": false
}| Código | Reconectar | O que aconteceu |
|---|---|---|
TRANSIENT_FAILURE | sim | A conexão caiu por um problema passageiro. Reconecte. |
SERVER_RESTARTING | sim | O Trivo está reiniciando. Reconecte daqui a pouco. |
AUTHORIZATION_LOST | sim | O App perdeu a autorização. Reconecte para receber a visão nova. |
CONNECTION_LIMIT_EXCEEDED | sim | Este App passou do teto de conexões do gateway. Espere antes de abrir outra. |
CREDENTIAL_REVOKED | não | A credencial que abriu esta conexão foi revogada. Reconecte apenas com a credencial nova. |
APP_DELETED | não | Este App foi excluído. Não existe credencial com que reconectar. |
OWNER_SANCTIONED | não | A conta dona deste App está sancionada, e as conexões dele ficam fechadas. |
APP_SANCTIONED | não | Este App está suspenso ou banido pela plataforma. As conexões dele ficam fechadas. |
FATAL_ERROR | não | A conexão foi encerrada por um erro que tentar de novo não resolve. |
O message é frase em português para o seu log; a decisão sai do code e do reconnect. Código ausente ou desconhecido vale como "reconecte": causa desconhecida nunca vira "desista", porque a reconexão diz a verdade — um 401 se a credencial morreu, uma visão nova se ela vive.
Contrapressão, ordem e ritmo
Seus handlers nunca correram em série: o SDK entrega um evento, chama o seu código e segue lendo. O que ele acrescenta é o chão — e o teto de fila é contrapressão de verdade, porque parar de ler fecha a janela TCP e faz o Trivo segurar o resto, em vez de o seu processo acumular na memória o que não dá conta.
const app = createApp({
handlerConcurrency: 256, // handlers em voo
eventQueueLimit: 1000, // passado isso, o SDK PARA DE LER o gateway
maxConcurrentRequests: 32, // chamadas REST em voo
eventOrdering: "channel", // serializa por canal; o padrão é "none"
});
app.pendingEvents; // enfileirados + em voo, para a sua métricaCom eventOrdering: "channel", dois eventos do mesmo canal são processados um depois do outro, e canais, servidores e pessoas diferentes continuam em paralelo.
O 429 do gateway é obedecido com ruído para cima — cem processos que leram o mesmo Retry-After não voltam no mesmo milissegundo — e com teto de sanidade de 5 minutos. No REST a recusa vem antes: espera acima de maxRetryDelayMs (30 s por padrão) volta como erro em vez de virar sono.
Tetos
São 2 conexões por App e 50 no gateway inteiro, e a contagem é separada da humana: um bot mal escrito não fecha a porta de quem usa o produto. Estourar responde 429 antes de o fluxo abrir. Os números todos, com a janela de cada balde, estão em Limites e ritmo.
Fechar a conexão
Fechar o socket basta. A vaga do teto é liberada quando a conexão morre — não há gesto de despedida a mandar, e app.disconnect() é o que o SDK faz por você quando o processo desce.
O recuo em JSON, quando o fluxo não flui
Alguns caminhos de rede seguram a resposta inteira antes de entregá-la: o GET responde 200, o corpo abre e nenhum byte atravessa — nem o batimento, que existe justamente para atravessar. Por padrão o SDK começa no fluxo e recua sozinho depois de duas aberturas mudas seguidas, passando a perguntar à caixa: mesmos eventos, mesma ordem, mesmo funil, mesmas intents.
createApp({ transport: "auto" }); // o padrão: recua sozinho
createApp({ transport: "mailbox" }); // direto na caixa
createApp({ transport: "stream" }); // nunca recua
createApp({ mailboxPollMs: 1000 }); // de quanto em quanto perguntarReferência de transporte
A caixa, por baixo
O que viaja no fio, para transparência, depuração e compatibilidade — não é um segundo jeito de construir App. Se você está escrevendo um App, o SDK já faz isto.
# abre a caixa e recebe o primeiro lote
GET /api/apps/eventos?format=json&intents=messages
→ { "mailbox": "…", "events": [...], "next": 12, "intents": ["messages"] }
# lê o que chegou desde então
GET /api/apps/eventos?format=json&mailbox=<mailbox>&since=12
→ { "events": [...], "next": 19 }
# devolve a vaga quando acabar
DELETE /api/apps/eventos?mailbox=<mailbox>A caixa tem prazo, e ele é curto: 45 segundos sem uma leitura e ela é varrida. Depois disso a resposta é 409 com MAILBOX_EXPIRED, e o que fazer é abrir outra. Ela também guarda no máximo 400 eventos entre duas leituras — e passar disso não poda o mais antigo: a caixa é marcada como perdida, esvazia inteira, e a leitura seguinte responde o mesmo 409. É de propósito — entregar os mais novos e calar sobre o buraco daria um vão silencioso no meio da conversa. O cursor next só anda para a frente.
A leitura da caixa não consome vaga de conexão (abrir consome), mas tem freio próprio: 90 leituras por minuto, e perguntar em laço apertado responde 429.
A caixa não recebe quadro de despedida: um App sancionado ou de credencial revogada descobre pelo 409 na leitura seguinte, e não por um connection_close. Se você usa este caminho, trate o 409 como "confira a credencial antes de abrir outra".
O que o Gateway NÃO faz
- Não entrega conversa privada, presença nem ocupação de voz. O corte é estrutural: esses eventos são endereçados por id de conta, e um App não tem conta.
- Não aceita comando. Nada é enviado por ele: ele é um cano de mão única, e tudo o que o App faz, faz pelo
app.rest. - Não tem sharding. Não há o que repartir, e não há número de shard a calcular.
- Não guarda o que você perdeu além da janela. Fora dela, a resposta honesta é "recomece".