tadpole

Tadpole: a Discord library for Gleam. Every frog starts as a tadpole. A bot starts here: new builds a config, with_* adjusts it, validate checks it before anything connects. The runner that uses a validated config is tadpole/bot.

New here? tadpole/guide walks from an empty directory to a running echo bot.

The shape of the library

       your handler (fn(Bot, Event) -> Nil)
         ^ |
         | | bot.send_message / bot.reply
         | v
     tadpole/bot  (the runner: validates, connects, dispatches
         |         events one at a time, in arrival order)
         |                \
    events                 |  REST calls
         v                 v
     tadpole/gateway/shard   tadpole/rest/execute
       (heartbeats, identify   (rate limits learned from
        or resume, backoff)     response headers, 429 retries)
         |                       |
     tadpole/gateway/transport   gleam_httpc
       (stratus websocket)       |
         |                       v
         v                   Discord REST API
     Discord gateway (wss)

The two sides never share state: the gateway delivers events, REST sends answers. A Bot from bot.start holds both handles.

Module directory

ModuleWhat it ownsAudience
tadpoleconfig builder: new, with_*, validate, describe_configbeginner
tadpole/botthe runner: start, run, send_message, reply, stop; one shardbeginner
tadpole/guidethe walkthroughbeginner
tadpole/gateway/eventsthe typed Event type: Ready, MessageCreate, Unknown, …beginner
tadpole/intentintents bitfield, privileged-intent detectionbeginner
tadpole/errorevery failure as a typed valuebeginner
tadpole/error/rendererrors to human text; token redacted everywherebeginner
tadpole/types/idsopaque IDs, so a UserId cannot go where a GuildId goesbeginner
tadpole/rest/endpointsGET /users/@me, post a message, replybeginner
tadpole/model/userthe user objectbeginner
tadpole/model/messagethe message object and MESSAGE_UPDATE’s partial formbeginner
tadpole/model/guildthe guild object and the unavailable stubbeginner
tadpole/model/channelthe channel object, trimmedbeginner
tadpole/gateway/shardone gateway connection, end to endinternals
tadpole/gateway/transportthe stratus websocket behind a wallinternals
tadpole/gateway/frameframe envelope parsing and building: {op, d, s, t}internals
tadpole/gateway/opcodegateway opcodes, with a slot for ones Discord adds laterinternals
tadpole/gateway/identify_gateidentify pacing across a fleet; /gateway/bot wiring is laterinternals
tadpole/gatewaypure protocol decisions: close codes, backoff, sharding mathinternals
tadpole/event_typeevent name to category and required intentsinternals
tadpole/restrequest builders, header-derived rate-limit parsinginternals
tadpole/rest/executetransport injection, 429 retries, rate-limit sessionsinternals
tadpole/rest/rate_limitper-bucket limit state, pureinternals
tadpole/model/decodeshared decoder plumbinginternals
tadpole/types/snowflake64-bit snowflakes, timestamp extractioninternals

“Internals” means you can use it, but the API moves more freely and a newer milestone may ask you to re-read the docs.

Status and stability

Works today: gateway connect, identify, heartbeats on Discord’s interval, resume after a disconnect, reconnect with backoff, typed events. REST runs over gleam_httpc with rate limits learned from response headers and 429 bodies — no hardcoded bucket table.

Not here yet:

Each module’s header declares a stability tier (Stable, Growing, Experimental) under the policy in CONTRIBUTING.md. Most of this slice is Growing or Experimental.

The publish gate — a live gateway roundtrip plus one real REST call — has been tripped. Hex is the next milestone.

Types

pub type Config {
  Config(
    token: String,
    intents: intent.Intents,
    shard_count: Int,
    rest_timeout_ms: Int,
    retry_on_429: Bool,
    max_retries: Int,
    gateway_reconnect: Bool,
    log_level: LogLevel,
  )
}

Constructors

  • Config(
      token: String,
      intents: intent.Intents,
      shard_count: Int,
      rest_timeout_ms: Int,
      retry_on_429: Bool,
      max_retries: Int,
      gateway_reconnect: Bool,
      log_level: LogLevel,
    )
pub type LogLevel {
  Debug
  Info
  Warn
  ErrorLevel
}

Constructors

  • Debug
  • Info
  • Warn
  • ErrorLevel
pub type ValidatedConfig {
  ValidatedConfig(config: Config, rest: rest.RestClient)
}

Constructors

Values

pub fn describe_config(config: Config) -> String

Safe to log: token redacted.

pub fn new(token: String) -> Config
pub fn privileged_intents_requested(config: Config) -> List(Int)
pub fn validate(
  config: Config,
) -> Result(ValidatedConfig, error.TadpoleError)

Validation errors before any connection is attempted: empty or malformed tokens. Privileged intents are reported, not rejected — Discord enforces those at Identify with close code 4014.

pub fn with_gateway_reconnect(
  config: Config,
  enabled: Bool,
) -> Config
pub fn with_intents(
  config: Config,
  intents: intent.Intents,
) -> Config
pub fn with_log_level(config: Config, level: LogLevel) -> Config
pub fn with_max_retries(config: Config, max: Int) -> Config
pub fn with_rest_timeout(
  config: Config,
  timeout_ms: Int,
) -> Config
pub fn with_retry_on_429(config: Config, enabled: Bool) -> Config
pub fn with_shards(config: Config, count: Int) -> Config
Search Document