tadpole/gateway/transport

The websocket transport: stratus behind a wall. Gateway code sees frames in and a send/close handle out; stratus’s Connection and InternalMessage types never cross this module, and neither does the socket. The shard actor owns the protocol state on top of it. Stability: Growing.

When you reach for this

Almost never directly. tadpole/gateway/shard opens connections and reads frames; tadpole/bot builds shards. The module exists so the rest of the library can be written and tested without stratus’s types in scope, and so a different websocket backend could replace stratus behind the same four functions.

The Connection contract

Connection is opaque: it holds the stratus subject and the owning process id, and nothing outside this module may touch either. That is what makes the socket race-free — only the transport actor ever writes to it.

TransportDown and Closed

Closed(close_code) arrives on the subject given to connect when the server closes the connection: Discord’s code when a close frame came, 1006 when the socket died without one. Stratus’s close reasons are mapped back to wire codes here — custom 4xxx codes pass through, NotProvided becomes 1006.

Monitor, not link

A connection can also die with no close at all (process death, a dropped socket) — that is not a Closed notice, it is the monitor firing. connect starts the connection as its own actor, which links to its spawner like any Gleam actor. The shard immediately severs that link (process.unlink(transport.owner_pid(conn))) and monitors the process instead: a transport death must notify the shard, not kill it. The monitor is the death signal; Closed is the polite one, and the shard swallows late duplicates with its closed_handled flag.

Failure modes

connect fails on a bad URL or a failed handshake; gateway_request rejects any scheme that is not wss/ws. send_text and close behave as the Connection contract above describes.

See also

Types

What a deliberate close means for the session Discord still holds. The docs are explicit: closing with close code 1000 or 1001 invalidates the session, and any other close code (or dropping the TCP connection) leaves it valid until it times out. So the intent picks the wire code, and the resume paths get a code outside the invalidating pair.

pub type CloseIntent {
  KeepSession
  EndSession
}

Constructors

  • KeepSession

    The session must stay valid server-side: the next connection will RESUME it. Sent as 4900, the convention for “reconnect intended”.

  • EndSession

    The session is dead or should die: the bot is stopping, or the next connection will identify fresh. Sent as 1000, the docs’ clean invalidation.

Notice that the websocket closed. close_code is Discord’s close code when the server sent a close frame; 1006 when the socket died without one.

pub type Closed {
  Closed(close_code: Int)
}

Constructors

  • Closed(close_code: Int)

A live websocket connection to the gateway. Opaque on purpose: the stratus connection behind it may only be touched from the transport actor, so nothing outside this module can race the socket.

pub opaque type Connection

Messages the transport actor runs on. Internal plumbing; public only because the stratus subject’s type mentions it. Send SendClose (via close) to end the connection politely.

pub type TransportMsg {
  SendText(
    reply: process.Subject(Result(Nil, error.TadpoleError)),
    payload: String,
  )
  SendClose(intent: CloseIntent)
}

Constructors

Values

pub fn close(conn: Connection, intent: CloseIntent) -> Nil

Ask the transport to send a close frame and end. The intent picks the wire code: KeepSession sends 4900 so Discord keeps the session for the resume that follows, EndSession sends 1000 to invalidate cleanly. Fire-and-forget: the usual Closed notice or process death follows on its own.

pub fn connect(
  url: String,
  inbound inbound: process.Subject(frame.Frame),
  on_closed on_closed: process.Subject(Closed),
) -> Result(Connection, error.TadpoleError)

Open a websocket to url and run it as its own actor. Parsed gateway frames arrive on inbound; when the server closes the connection a Closed notice arrives on on_closed. Monitor owner_pid as well — a connection that dies without a close frame sends no Closed.

Blocks until the websocket handshake finishes (stratus’s 5s connect timeout) or fails. Fails with GatewayConnectFailed for a bad URL or a failed handshake.

pub fn gateway_request(
  url: String,
) -> Result(request.Request(String), error.TadpoleError)

The stratus upgrade request for url. wss maps to TLS and ws to plain TCP; anything else is rejected — the gateway is always a websocket URL, and a config typo should fail immediately, not as a handshake mystery.

pub fn send_text(
  conn: Connection,
  payload: String,
) -> Result(Nil, error.TadpoleError)

Send one text frame. Blocks until the transport actor has written it (up to 5s). Fails when the socket refuses the write; if the transport process is gone the call crashes the caller — the shard only sends between protocol steps and treats a dead transport as a close.

Search Document