Spraxium logoSpraxium

Signal Client para emissores de serviço

Use o Signal Client para publicar eventos confiáveis, assinados com HMAC, no runtime do seu bot. Aprenda o contrato do construtor, a rotação do pool de webhooks, estratégias para rate limit e padrões práticos de emissão.

Papel do emissor

Signal Client existe para publicar eventos de serviços externos em uma fronteira de integração orientada a bots. Mesmo quando o emissor é um endpoint de API, um cron worker ou uma Server Action em Next.js, o objetivo operacional é sempre o mesmo: influenciar o comportamento do bot de forma controlada e auditável. O pacote cuida da construção do envelope, da assinatura HMAC, da rotação de webhooks e da absorção de rate limit para que o código produtor continue focado na lógica de negócio.

Essa perspectiva evita que a equipe transforme o client em uma ferramenta genérica de envio de mensagens. O client é mais valioso quando seus serviços precisam de uma forma segura e consistente de avisar domínios do bot sobre mudanças de estado sem acoplamento aos internals do gateway do Discord.

Contrato do construtor

SignalClient recebe um único objeto SignalClientConfig e, opcionalmente, um FallbackStore customizado. O construtor cria internamente um SignalEnvelopeBuilder para assinatura HMAC, um WebhookPool para entrega em round-robin e, quando habilitado, um FallbackWorker que é iniciado imediatamente. Todo campo além de webhookUrls e hmacSecret possui um valor padrão sensato, então uma configuração mínima precisa apenas desses dois valores.

CampoTipoPadrãoFinalidade
webhookUrlsstring[]obrigatório

Uma ou mais URLs de webhook do Discord apontando para o canal privado de signal. O pool alterna entre elas em round-robin.

hmacSecretstringobrigatório

Segredo compartilhado HMAC-SHA256. Precisa coincidir com o hmacSecret configurado em @spraxium/signal no lado do bot.

rateLimitStrategyskip | waitskip

Comportamento quando todos os webhooks ficam limitados ao mesmo tempo. Veja a seção de estratégias abaixo.

fallbackFallbackConfigundefined

Habilita um sistema persistente de retry por fallback. Ele é detalhado na página de confiabilidade.

signal-client.setup.ts
import { SignalClient } from '@spraxium/signal-client';

const client = new SignalClient({
  webhookUrls: [
    process.env.SIGNAL_WEBHOOK_URL_1!,
    process.env.SIGNAL_WEBHOOK_URL_2!,
  ],
  hmacSecret: process.env.SIGNAL_HMAC_SECRET!,
  rateLimitStrategy: 'skip',
});

O segredo compartilhado em hmacSecret precisa coincidir exatamente com a configuração do lado do bot. Se os valores divergirem, o bot rejeita todo envelope na verificação HMAC, regra R10, e nenhum listener será executado.

Construção e assinatura do envelope

Toda chamada de send delega internamente para SignalEnvelopeBuilder, que produz um envelope completo e assinado criptograficamente. O builder gera um UUID aleatório como nonce, captura o timestamp atual como sentAt e calcula a assinatura HMAC-SHA256 sobre a concatenação nonce.sentAt.JSON.stringify(payload). Essa fórmula é idêntica à usada por @spraxium/signal no lado receptor, então as duas pontas concordam com o contrato de assinatura sem qualquer negociação adicional.

O envelope resultante sempre carrega a versão 1 do protocolo, o nome do evento, o guild ID de destino e o payload, além dos campos criptográficos. O código produtor nunca precisa interagir diretamente com o builder, porque send encapsula esse fluxo inteiro.

Campo do envelopeTipoOrigem
v1Versão do protocolo fixada no código.
eventstringPrimeiro argumento de send.
guildIdstringSegundo argumento de send.
noncestringUUID aleatório gerado pela função de UUID do runtime.
sentAtnumberTimestamp Unix em milissegundos capturado no momento do envio.
signaturestringDigest hexadecimal HMAC-SHA256 do nonce, timestamp e payload serializado.
payloadRecord<string, unknown>Terceiro argumento de send, com objeto vazio como padrão.

O método send

send é a API principal. Ele recebe um nome de evento, um guild ID, um payload opcional e SendOptions também opcionais. O fluxo interno monta o envelope, escolhe o próximo webhook disponível do pool e entrega via POST HTTP. Se o webhook responder com 429, o pool o marca como limitado e tenta imediatamente o próximo candidato. Quando todos os webhooks se esgotam, o comportamento passa a depender da configuração em rateLimitStrategy.

emit-event.ts
await client.send('billing.status.changed', guildId, {
  plan: 'premium',
  expiresAt: '2025-12-31',
});

A interface SendOptions expõe dois campos opcionais. O campo webhookIndex força a entrega por um webhook específico, ignorando a rotação round-robin. O campo skipFallback é usado internamente pelo FallbackWorker para evitar loops de reenfileiramento durante tentativas de retry.

OpçãoTipoPadrãoFinalidade
webhookIndexnumber | undefinedundefined

Força um índice de webhook específico, baseado em zero. Lança RangeError se sair dos limites.

skipFallbackboolean | undefinedundefined

Quando verdadeiro, falhas não são reenfileiradas no fallback store. Uso interno do sistema de retry.

Rotação do pool de webhooks

WebhookPool distribui os envios entre os webhooks configurados usando um cursor round-robin. Quando um webhook responde com HTTP 429, ele é marcado como limitado pelo período indicado no cabeçalho Retry-After, e o pool passa a ignorá-lo automaticamente até o fim do cooldown. Adicionar múltiplas URLs de webhook melhora bastante a resiliência em picos, porque o client sempre tem endpoints alternativos para tentar.

O pool expõe vários métodos de inspeção úteis para monitoramento e depuração. Você pode acessar o pool a qualquer momento pela propriedade pool da instância do client.

Método / PropriedadeRetornaFinalidade
next | nullRetorna o próximo webhook disponível em ordem round-robin. Retorna null quando todos estão limitados.
getstringRetorna o webhook de um índice específico, sem checar rate limit. Lança RangeError se sair dos limites.
markRateLimitedvoidMarca um webhook como limitado por uma duração específica, em milissegundos. Chamado automaticamente em respostas 429.
msUntilAvailablenumberRetorna quantos milissegundos faltam até o webhook mais próximo ficar disponível. Retorna 0 quando ao menos um já está pronto.
statusWebhookStatus[]Snapshot somente leitura de cada webhook com index, url, available e rateLimitedUntil.
sizenumberTotal de webhooks no pool.
pool-inspection.ts
const poolStatus = client.pool.status();

for (const entry of poolStatus) {
  console.log(
    `Webhook #${entry.index}: ${entry.available ? 'ready' : `cooldown until ${entry.rateLimitedUntil}`}`,
  );
}

Estratégias de rate limit

Quando todos os webhooks do pool estão limitados ao mesmo tempo, o client precisa decidir o que fazer. O campo rateLimitStrategy controla esse comportamento. As duas estratégias são válidas, dependendo da arquitetura do seu serviço.

EstratégiaComportamentoCenário ideal
skip

Lança AllWebhooksRateLimitedError imediatamente. O erro inclui o status completo do pool com timestamps de cooldown para que o chamador decida quando tentar de novo.

Serviços que já possuem orquestração própria de retry ou infraestrutura de fila.

wait

Aguarda até o webhook com cooldown mais curto ficar disponível, por meio de msUntilAvailable, e tenta a entrega exatamente uma vez de novo. Se essa nova tentativa falhar, ele lança o erro.

Absorção de picos curtos dentro do próprio client, sem lógica externa de retry.

O padrão é 'skip', porque mantém o client não bloqueante. Na maioria dos contextos de Server Action ou handlers de API, em que uma pequena latência extra é aceitável, 'wait' oferece ergonomia melhor ao absorver picos transitórios de rate limit de forma transparente.

Tipos de erro

O client expõe duas classes de erro que entregam informações estruturadas de falha. Ambas estendem Error e carregam campos legíveis por máquina para tratamento programático.

SignalClientError é lançado em respostas HTTP que não são 2xx nem 429. Ele expõe statusCode e o body da resposta como propriedades, o que facilita registrar falhas de transporte com contexto completo. Esse erro indica que o problema está no próprio endpoint de webhook, não em uma situação de rate limit.

AllWebhooksRateLimitedError estende SignalClientError com statusCode igual a 429 e expõe um array poolStatus. Cada entrada desse array é um snapshot de WebhookStatus com index, url, a flag available e o timestamp rateLimitedUntil. Esse erro só é lançado sob a estratégia 'skip' ou quando 'wait' esgota sua única nova tentativa.

Emissor prático: Server Actions no Next.js

Um padrão comum em produção é um painel administrativo feito em Next.js, no qual uma Server Action atualiza dados de negócio e emite um evento Signal para o bot. Essa abordagem mantém as mutações de UI e as reações do bot fracamente acopladas, preservando um único fluxo operacional para quem administra o sistema. A Server Action cuida de autorização e persistência dentro da sua aplicação web, e o Signal Client cuida da entrega assíncrona para o runtime do bot.

'use server';

import { SignalClient } from '@spraxium/signal-client';

const client = new SignalClient({
  webhookUrls: [process.env.SIGNAL_WEBHOOK_URL_1!],
  hmacSecret: process.env.SIGNAL_HMAC_SECRET!,
  rateLimitStrategy: 'wait',
});

export async function updatePrefix(guildId: string, prefix: string) {
  // persist in your database first
  await db.guilds.update(guildId, { prefix });

  // then emit the event for the bot
  await client.send('config.update', guildId, {
    prefix,
    source: 'admin-panel',
  });
}

Diretrizes de modelagem de eventos

Prefira nomes orientados ao domínio, que descrevam fatos de negócio em vez de ações de transporte. Um nome como billing.status.changed é mais fácil de versionar e de entender do que rótulos voltados à ação, que acabam vazando detalhes internos de implementação. Mantenha nomes de evento em minúsculas e separados por ponto para seguir as convenções já usadas em todo o sistema do Signal.

Os payloads devem ser intencionalmente pequenos e estáveis. Inclua apenas os campos de que o bot realmente precisa para decidir o que fazer e evolua o schema com mudanças aditivas sempre que possível. Remover ou renomear um campo existente é uma quebra de compatibilidade e exige coordenação entre as implantações do emissor e do listener.

Use o filtro events em FallbackConfig para concentrar as garantias de confiabilidade nos eventos mais críticos. Eventos que não aparecem nessa lista continuam sendo entregues pelo fluxo normal, mas não entram no fallback store quando ocorre falha.

Passo a passo do fluxo de entrega

Monte o envelope assinado

O método send delega para SignalEnvelopeBuilder. O builder gera um nonce com crypto.randomUUID(), captura Date.now() como sentAt, calcula a assinatura HMAC-SHA256 e devolve um envelope completo com a versão 1 do protocolo.

Escolha o próximo webhook

WebhookPool.next retorna o próximo webhook disponível em ordem round-robin. Se SendOptions.webhookIndex tiver sido informado, o pool retorna essa entrada específica via get, sem passar pela rotação. Se todos os webhooks estiverem limitados e nenhum índice tiver sido forçado, o fluxo salta para a etapa cinco.

Entregue via POST HTTP

O client envia uma requisição POST para a URL do webhook com o envelope serializado em JSON dentro do campo de conteúdo de mensagem do Discord. Se a resposta for 2xx, a entrega termina com sucesso e o método retorna.

Trate respostas 429

Quando o webhook responde com HTTP 429, o pool marca aquele índice como limitado pela duração extraída do cabeçalho Retry-After, usando 1000 ms como padrão. O client tenta imediatamente o próximo webhook disponível. Essa rotação continua até que um webhook funcione ou todos se esgotem.

Aplique a estratégia de rate limit

Quando não resta nenhum webhook, rateLimitStrategy decide o resultado. Em 'skip', um AllWebhooksRateLimitedError é lançado na hora. Em 'wait', o client dorme até o cooldown mais curto terminar e tenta uma vez de novo. Se o fallback estiver habilitado e o erro puder ser capturado, o envelope é enfileirado no fallback store para novo processamento posterior.

Quando esse modelo é a ferramenta errada

Se você precisa de uma confirmação síncrona imediata da lógica do bot dentro do mesmo ciclo da requisição, Signal normalmente não é o contrato certo. Chamadas diretas de serviço continuam mais simples e fáceis de rastrear para comunicação in-process dentro da mesma fronteira de serviço. O Signal foi desenhado para entrega assíncrona de eventos no estilo fire-and-forget, em que o produtor não precisa de resposta do consumidor.

Da mesma forma, se seus eventos exigem garantias rígidas de ordenação, uma fila dedicada, como RabbitMQ ou Redis Streams, é mais adequada. O Signal entrega mensagens tão rápido quanto o pool de webhooks permite, e a ordem dos retries depois de falhas é best-effort, não estritamente sequencial.