glyde/client

The driver: runs glyde/gateway’s outputs against transport functions you hand it. Nothing here blocks, spawns, sleeps or returns a Result.

Three of the six adapter rules in glyde/gateway are kept here: outputs run in order, the shard is committed before any of them runs, and a bot arrives seeded. The other three are on the Transport you hand it.

Types

Opaque, unlike the shard inside it: the queue is what stops a handler’s command re-entering the machine mid-batch. Read it with shard and state.

pub opaque type Bot(state)

The zlib context for gateway.ZlibStream. A native mutable resource, so it lives out here rather than in the state machine.

This, not Config.compression, decides the mode: the bot writes the config to match whichever of these it holds. Compression negotiated with nothing to inflate with is a socket whose every frame is unreadable, so it is not something a builder can reach.

pub type Inflater(state) {
  Plaintext
  Zlib(
    inflate: fn(state, gateway.Conn, gateway.Stamp, BitArray) -> state,
    reset: fn(state, gateway.Conn) -> state,
  )
}

Constructors

  • Plaintext

    No context, and gateway.NoCompression with it.

  • Zlib(
      inflate: fn(state, gateway.Conn, gateway.Stamp, BitArray) -> state,
      reset: fn(state, gateway.Conn) -> state,
    )

    One context per connection, and gateway.ZlibStream with it. inflate answers by calling inflated; reset starts a fresh one, which every new connection needs.

What a handler gives back. Commands go through the machine rather than straight down the socket: they come out of the connection’s budget.

pub type Next(state) {
  Next(state: state, commands: List(command.Command))
}

Constructors

How a connection earns the right to send IDENTIFY. Discord meters it per bot, not per connection, so a fleet takes turns.

pub type Slots(state) {
  Solo
  Fleet(
    request: fn(state, gateway.Conn) -> state,
    release: fn(state, gateway.Conn) -> state,
  )
}

Constructors

  • Solo

    Grant every request immediately. Right for a single-shard bot.

  • Fleet(
      request: fn(state, gateway.Conn) -> state,
      release: fn(state, gateway.Conn) -> state,
    )

    Ask a coordinator, glyde/identify_queue being one, and call slot_granted when it answers. release frees the slot again.

The six things a shard asks of the outside world. Every function takes the bot’s state first and returns it, so a fake one can record every frame.

pub type Transport(state) {
  Transport(
    open: fn(state, gateway.Conn, String, String) -> state,
    send: fn(state, frame.Outbound) -> state,
    close: fn(state, Int) -> state,
    drop: fn(state) -> state,
    arm: fn(state, gateway.Timer, Int, gateway.Stamp) -> state,
    cancel: fn(state, gateway.Timer) -> state,
  )
}

Constructors

  • Transport(
      open: fn(state, gateway.Conn, String, String) -> state,
      send: fn(state, frame.Outbound) -> state,
      close: fn(state, Int) -> state,
      drop: fn(state) -> state,
      arm: fn(state, gateway.Timer, Int, gateway.Stamp) -> state,
      cancel: fn(state, gateway.Timer) -> state,
    )

    Arguments

    open

    Open a WebSocket to wss://<host><path>. Keep the Conn and hand it back on every input from this socket, and do not touch the path.

    send

    Write frame.text as one text frame. A failed write is closed(bot, conn, None), never a raise: raising abandons the batch, heartbeat timer and all. frame.op says what it was, for logging, so no adapter has to parse the payload back.

    close

    Write a close frame with this code, then tear the socket down. Only ever 1000, which ends the session, or 4000, which keeps it resumable.

    drop

    Abandon the socket with no close frame. The peer has already gone, or there was never a socket.

    arm

    Arm a one-shot timer, replacing whatever was armed under the same name. When it fires, call timer_fired with this exact stamp.

    cancel

    Cancel a timer. Cancelling one that is not armed is a no-op, and a fire that beats the cancel is recognised by its stamp and dropped.

Values

pub fn closed(
  bot: Bot(state),
  conn: gateway.Conn,
  code: option.Option(Int),
) -> Bot(state)

The socket closed. None is a transport that died with no close frame; 1005 and 1006 mean the same thing and normalise to it.

pub fn collecting(
  events: List(gateway.Event),
  event: gateway.Event,
) -> Next(List(gateway.Event))

A handler that keeps every event, newest first, for the “hand the events back to me” shape.

pub fn command(
  bot: Bot(state),
  command: command.Command,
) -> Bot(state)

Send a gateway command. Goes out when the shard is live and has budget, and waits in the shard’s own queue otherwise, so it survives a reconnect.

pub fn conn(bot: Bot(state)) -> gateway.Conn

The connection the bot will hear from. An adapter uses the Conn from Transport.open instead: synthesising one defeats the staleness check.

pub fn discarding() -> Transport(state)

Performs nothing. The explicit choice for a test that only wants to read the shard, and the only way to build a bot that reaches no socket.

pub fn feed(bot: Bot(state), input: gateway.Input) -> Bot(state)

Any input, for something this module has not given a name to.

pub fn from_shard(
  shard shard: gateway.Shard,
  state state: state,
  transport transport: Transport(state),
) -> Bot(state)

A bot around a shard you built yourself, for a custom seed or for gateway.resuming after a process restart.

The transport is taken here and not attached later, because a bot without one advances its shard and writes to nothing. Pass discarding() when that is what you want.

Compression starts off whatever the config asked for: with_inflater is the switch, and it moves both halves together.

pub fn inflated(
  bot: Bot(state),
  conn: gateway.Conn,
  stamp: gateway.Stamp,
  result: Result(String, gateway.InflateFailure),
) -> Bot(state)

The answer to an Inflater.inflate. Error names which way it failed, and no amount of retrying fixes either mid-connection.

pub fn is_terminal(bot: Bot(state)) -> Bool

True once the bot has stopped, or hit a close code no reconnect can fix. An adapter’s loop exits here.

pub fn keep(state: state) -> Next(state)

Keep this state and send nothing.

pub fn new(
  token token: String,
  intents intents: intents.Intents,
  transport transport: Transport(Nil),
) -> Bot(Nil)

Default gateway config, no state.

pub fn on_event(
  bot: Bot(state),
  handle: fn(state, gateway.Event) -> Next(state),
) -> Bot(state)

Fold events into the state. Replaces the previous handler.

pub fn on_notice(
  bot: Bot(state),
  notice: fn(state, gateway.Notice) -> state,
) -> Bot(state)

Watch the diagnostics: a zombie connection, a dropped command, a wait for an identify slot. Never protocol-significant, and where a log line belongs.

pub fn open_failed(
  bot: Bot(state),
  conn: gateway.Conn,
  failure: gateway.DialFailure,
) -> Bot(state)

The WebSocket could not be established, or died before the upgrade finished. An adapter that saw a response status says so: 401 is a dead token, and the shard halts on it instead of redialling.

pub fn opened(bot: Bot(state), conn: gateway.Conn) -> Bot(state)

The WebSocket is up and the upgrade finished.

pub fn received(
  bot: Bot(state),
  conn: gateway.Conn,
  text: String,
) -> Bot(state)

A text frame arrived, carrying exactly one gateway payload.

pub fn received_bytes(
  bot: Bot(state),
  conn: gateway.Conn,
  data: BitArray,
) -> Bot(state)

A binary frame arrived under a compressed transport. A fragment, a payload or several; the core buffers.

pub fn sending(
  next: Next(state),
  command: command.Command,
) -> Next(state)

Send a gateway command once the current batch is done.

pub fn session(bot: Bot(state)) -> option.Option(gateway.Session)

The session, if there is one to resume with. Persist it across a process restart and boot the next one with gateway.resuming.

pub fn shard(bot: Bot(state)) -> gateway.Shard

The pure core, for calling gateway.step directly or reading a phase.

pub fn slot_granted(
  bot: Bot(state),
  conn: gateway.Conn,
) -> Bot(state)

The fleet coordinator granted this connection its identify slot. Only needed under Slots.Fleet.

pub fn start(bot: Bot(state)) -> Bot(state)

Begin. Arms the reconnect timer at zero, so the dial happens on the fire and there is exactly one code path that dials.

pub fn state(bot: Bot(state)) -> state
pub fn stateful(
  config config: gateway.Config,
  state state: state,
  transport transport: Transport(state),
) -> Bot(state)

A bot carrying a state of your own, and the way to the rest of the config: sharding, the tuning numbers. Nothing to remember is Nil.

pub fn stop(bot: Bot(state)) -> Bot(state)

Shut down. Closes with 1000, which ends the session for good rather than leaving one to resume. Terminal, and every later input is ignored.

pub fn timer_fired(
  bot: Bot(state),
  timer: gateway.Timer,
  stamp: gateway.Stamp,
) -> Bot(state)

A timer fired. The stamp must be the one that armed it; anything else is a fire that lost the race with its own cancellation and is dropped.

pub fn update(
  bot: Bot(state),
  with: fn(state) -> state,
) -> Bot(state)

Change your state from outside a handler, for something that did not come from the gateway: a REST response, a tick of your own.

pub fn with_inflater(
  bot: Bot(state),
  inflater: Inflater(state),
) -> Bot(state)

Turn transport compression on, by handing over the context that makes it readable. Zlib asks Discord for zlib-stream, Plaintext goes back to uncompressed: the two are one switch, so neither can be set alone.

pub fn with_seed(bot: Bot(state), seed: Int) -> Bot(state)

Reseed the shard’s jitter. Below 2^31, and different for every shard of a fleet.

pub fn with_slots(
  bot: Bot(state),
  slots: Slots(state),
) -> Bot(state)

Take identify slots from a fleet coordinator instead of granting them here.

Search Document