Pular para o conteúdo

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çou

Sem 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_resume e, 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ódigoReconectarO que aconteceu
TRANSIENT_FAILUREsimA conexão caiu por um problema passageiro. Reconecte.
SERVER_RESTARTINGsimO Trivo está reiniciando. Reconecte daqui a pouco.
AUTHORIZATION_LOSTsimO App perdeu a autorização. Reconecte para receber a visão nova.
CONNECTION_LIMIT_EXCEEDEDsimEste App passou do teto de conexões do gateway. Espere antes de abrir outra.
CREDENTIAL_REVOKEDnãoA credencial que abriu esta conexão foi revogada. Reconecte apenas com a credencial nova.
APP_DELETEDnãoEste App foi excluído. Não existe credencial com que reconectar.
OWNER_SANCTIONEDnãoA conta dona deste App está sancionada, e as conexões dele ficam fechadas.
APP_SANCTIONEDnãoEste App está suspenso ou banido pela plataforma. As conexões dele ficam fechadas.
FATAL_ERRORnãoA 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étrica

Com 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 perguntar

Referê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".