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
| Module | What it owns | Audience |
|---|---|---|
tadpole | config builder: new, with_*, validate, describe_config | beginner |
tadpole/bot | the runner: start, run, send_message, reply, stop; one shard | beginner |
tadpole/guide | the walkthrough | beginner |
tadpole/gateway/events | the typed Event type: Ready, MessageCreate, Unknown, … | beginner |
tadpole/intent | intents bitfield, privileged-intent detection | beginner |
tadpole/error | every failure as a typed value | beginner |
tadpole/error/render | errors to human text; token redacted everywhere | beginner |
tadpole/types/ids | opaque IDs, so a UserId cannot go where a GuildId goes | beginner |
tadpole/rest/endpoints | GET /users/@me, post a message, reply | beginner |
tadpole/model/user | the user object | beginner |
tadpole/model/message | the message object and MESSAGE_UPDATE’s partial form | beginner |
tadpole/model/guild | the guild object and the unavailable stub | beginner |
tadpole/model/channel | the channel object, trimmed | beginner |
tadpole/gateway/shard | one gateway connection, end to end | internals |
tadpole/gateway/transport | the stratus websocket behind a wall | internals |
tadpole/gateway/frame | frame envelope parsing and building: {op, d, s, t} | internals |
tadpole/gateway/opcode | gateway opcodes, with a slot for ones Discord adds later | internals |
tadpole/gateway/identify_gate | identify pacing across a fleet; /gateway/bot wiring is later | internals |
tadpole/gateway | pure protocol decisions: close codes, backoff, sharding math | internals |
tadpole/event_type | event name to category and required intents | internals |
tadpole/rest | request builders, header-derived rate-limit parsing | internals |
tadpole/rest/execute | transport injection, 429 retries, rate-limit sessions | internals |
tadpole/rest/rate_limit | per-bucket limit state, pure | internals |
tadpole/model/decode | shared decoder plumbing | internals |
tadpole/types/snowflake | 64-bit snowflakes, timestamp extraction | internals |
“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:
- multi-shard (asking for more is refused with
ShardingNotSupportedbefore anything connects) - interactions and slash commands
- embeds, file uploads, message components
- voice
- a cache; every event is what Discord just sent, nothing is remembered between events
- graceful shutdown;
bot.stopcloses the gateway and that is all
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
-
ValidatedConfig(config: Config, rest: rest.RestClient)
Values
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_intents(
config: Config,
intents: intent.Intents,
) -> Config