Spraxium logoSpraxium

Modos de Runtime do Bot Bridge

Entenda como BotBridge funciona em runtimes diretos e sharded. Desenhe contratos de endpoint que permaneçam estáveis independentemente da topologia de processo do Discord.

Por que o bridge existe

BotBridge é a camada de abstração entre controllers HTTP e operações do Discord. Controllers devem depender de métodos de BotBridge, não de internals brutos do discord.js como client.guilds.cache ou guild.members.ban(). Isso mantém o comportamento das rotas estável mesmo quando a topologia de implantação muda de um processo único para workers sharded, porque o código do controller não muda.

Sem o bridge, sair do modo direto para o modo sharded exigiria reescrever todo controller que interage com o Discord. O padrão bridge isola essa diferença em duas classes concretas, DirectBotBridge e ShardedBotBridge, ambas implementando o mesmo contrato abstrato. BridgeFactory escolhe a implementação correta na inicialização do servidor e a injeta no mapa de dependências da DI, então controllers e services podem declarar BotBridge como parâmetro de construtor e receber automaticamente a implementação certa.

O bridge também força a serialização na fronteira. Objetos do discord.js contêm referências circulares, caches vivos e métodos que não são seguros para serializar em JSON. O bridge retorna objetos tipados simples como SerializedGuild, SerializedMember e SerializedBan, seguros para transporte HTTP.

Seleção do modo de runtime

BridgeFactory.create(client, manager) escolhe a implementação de bridge com base na referência de runtime disponível. A factory segue uma precedência estrita: ShardingManager tem prioridade sobre Client. Se nenhum dos dois for fornecido, a factory lança um erro na inicialização, impedindo que o servidor rode sem conectividade com o Discord.

ModoImplementação

Como acessa o Discord

Quando usar
DiretoDirectBotBridge

Usa a instância local de Client do Discord. Lê do cache de guilds e chama métodos da API diretamente no mesmo processo.

Bots em processo único ou ambientes de desenvolvimento em que sharding é desnecessário.

ShardedShardedBotBridge

Usa manager.broadcastEval() para enviar uma função a todos os shards. Retorna o primeiro resultado não nulo, em leituras, ou o primeiro true, em ações.

Bots de produção em vários shards, em que o servidor HTTP roda no processo pai sem um Client local.

Contrato de capacidades do BotBridge

A classe abstrata BotBridge define 12 métodos organizados em três grupos de operação. Toda implementação concreta precisa cumprir todos esses 12 métodos, garantindo que controllers tenham uma API completa e consistente independentemente do modo de runtime.

Métodos de leitura retornam objetos serializados ou null. Métodos de ação retornam flags booleanas de sucesso. Todo método do bridge envolve seu corpo em um try/catch: em caso de falha, métodos de leitura retornam null, ou array vazio no caso de getBans, e métodos de ação retornam false. Isso significa que controllers nunca precisam tratar exceções inesperadas vindas de chamadas ao bridge.

Como o bridge sharded funciona

ShardedBotBridge usa manager.broadcastEval() para executar um closure em cada shard do cluster. O closure recebe o Client local daquele shard e um objeto de contexto serializado com os parâmetros, como guild ID e user ID. Só o shard que controla a guild de destino vai encontrá-la em seu cache, então os demais retornam null ou false.

Para operações de leitura, o bridge coleta resultados de todos os shards e retorna o primeiro valor não nulo usando .find(r => r !== null). Para operações de ação, retorna true se qualquer shard tiver retornado true, usando .some(r => r === true). Esse padrão de fan-out faz com que toda operação chegue ao shard correto sem exigir que o servidor HTTP conheça a topologia dos shards nem o mapeamento de guild para shard.

O custo aqui é latência: toda chamada ao bridge no modo sharded paga o preço da comunicação entre processos com todos os shards. Para bots com muitos shards, considere cachear mapeamentos de guild para shard se endpoints com muita leitura virarem gargalo.

Controller consumindo o bridge

import { HttpController, HttpGet, HttpPost, HttpParam, HttpBody, HttpStatus } from '@spraxium/http';
import { BotBridge, NotFoundError } from '@spraxium/http';
import { BanMemberDto } from '../dto/ban-member.dto';

@HttpController('/guilds/:guildId/moderation')
export class ModerationHttpController {
  constructor(private readonly bridge: BotBridge) {}

  @HttpGet('/bans')
  async listBans(@HttpParam('guildId') guildId: string) {
    const bans = await this.bridge.getBans(guildId);
    return { ok: true, guildId, bans, count: bans.length };
  }

  @HttpGet('/members/:userId')
  async getMember(
    @HttpParam('guildId') guildId: string,
    @HttpParam('userId') userId: string,
  ) {
    const member = await this.bridge.getMember(guildId, userId);
    if (!member) throw new NotFoundError('Member not found in guild');
    return { ok: true, member };
  }

  @HttpPost('/bans/:userId')
  @HttpStatus(201)
  async banMember(
    @HttpParam('guildId') guildId: string,
    @HttpParam('userId') userId: string,
    @HttpBody(BanMemberDto) body: BanMemberDto,
  ) {
    const success = await this.bridge.banMember(
      guildId,
      userId,
      body.reason,
      { deleteMessageSeconds: body.deleteMessageSeconds },
    );
    return { ok: success, guildId, userId };
  }
}

Modelo de serialização

O bridge usa três classes serializer para converter objetos de runtime do discord.js em payloads tipados simples, seguros para transporte JSON. Esses serializers removem referências circulares, convertem datas para strings ISO e extraem apenas os campos de que clientes externos precisam. Os tipos serializados funcionam como fronteira estável da API: controllers retornam esses tipos, e clientes os consomem.

SerializerEntradaCampos de saída
GuildSerializerdiscord.js Guildid, name, icon, memberCount, ownerId, createdAt (string ISO), features (array de strings).
MemberSerializerdiscord.js GuildMemberid, username, displayName, discriminator, avatar, bot, joinedAt, createdAt, roles (array de SerializedRole com id/name/color/position/permissions/managed/mentionable), pending, communicationDisabledUntil.
BanSerializerdiscord.js GuildBanuser (SerializedUser aninhado com id/username/discriminator/avatar/bot/createdAt), reason (string ou null).

No modo direto, serializers rodam no mesmo processo do client do Discord. No modo sharded, o closure de broadcastEval faz a serialização inline no shard que controla a guild, retornando objetos simples que atravessam a fronteira de IPC com segurança. Os dois caminhos produzem o mesmo formato de saída, então o código do controller é idêntico em qualquer modo.

Checklist de design para endpoints com bridge

Projete para transparência de modo

Mantenha a lógica do controller idêntica em implantações diretas e sharded, dependendo apenas de métodos de BotBridge. A seleção do modo deve continuar sendo uma preocupação de configuração em defineHttp({ sharding }), não do controller. Nunca importe DirectBotBridge ou ShardedBotBridge diretamente no código do controller.

Retorne apenas payloads serializados

Trate os tipos de saída dos serializers como a fronteira da sua API. Nunca retorne objetos de runtime do discord.js a partir de handlers, porque eles contêm referências circulares e caches vivos que quebram a serialização JSON. Se você precisar de campos não cobertos pelos serializers padrão, crie um serializer customizado em vez de expor objetos brutos do discord.js.

Trate leituras anuláveis explicitamente

Operações de leitura podem retornar null quando entidades estão ausentes ou indisponíveis no estado atual do runtime. Converta resultados null em respostas HTTP claras com base no contrato da sua rota. Uma guild null normalmente vira NotFoundError(404), enquanto um membro null pode pedir NotFoundError ou uma resposta vazia filtrada, dependendo da semântica do endpoint.

Mantenha mutações idempotentes quando possível

Métodos de ação retornam booleanos e podem rodar em contextos distribuídos, onde retries acontecem. A semântica do endpoint deve tolerar retries sem efeitos colaterais. Por exemplo, banir um usuário já banido retorna false, mas não cria conflito, e o controller ainda pode reportar o resultado da operação sem lançar erro.

Valide payloads de mutação com DTOs

Use @HttpBody(DtoClass) com constraints de class-validator em todo endpoint de mutação. Ações do bridge como banMember e timeoutMember recebem parâmetros que devem ser validados antes de alcançar a camada de bridge. A validação por DTO captura entrada malformada cedo e produz respostas de erro estruturadas automaticamente.

Métodos do bridge nunca lançam exceções em caso de falha

Todos os métodos do bridge envolvem as operações internamente em try/catch. Métodos de leitura retornam null em caso de falha, e métodos de ação retornam false. Desenhe o tratamento de erro do controller em torno desses valores de retorno, e não esperando exceções lançadas por chamadas ao bridge.