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.
connectblocks until the handshake finishes (stratus’s 5s connect timeout) and fails withGatewayConnectFailedfor a bad URL or a failed handshake. Server frames are parsed withtadpole/gateway/frameand forwarded toinbound; text that is not a gateway envelope is dropped with a warning, never a crash, and binary frames are ignored (the v10 JSON gateway sends none).send_textblocks until the transport actor has written the frame (up to 5s). If the transport process is already dead, the call crashes the caller — the shard only sends between protocol steps and treats a dead transport as a close, not as data loss to hide.closesends a close frame whose code depends on the intent:KeepSessionsends 4900 (outside the 1000/1001 pair Discord treats as session invalidation),EndSessionsends 1000. It returns immediately; theClosednotice or process death follows on its own.
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
tadpole/gateway/shard— the only consumertadpole/gateway/frame— the frames it forwards
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
-
KeepSessionThe session must stay valid server-side: the next connection will RESUME it. Sent as 4900, the convention for “reconnect intended”.
-
EndSessionThe 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
-
SendText( reply: process.Subject(Result(Nil, error.TadpoleError)), payload: String, ) -
SendClose(intent: CloseIntent)
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.