///|
/// A complete WebSocket message received from the Gateway.
pub(all) enum GatewayFrame {
Text(String)
Binary(Bytes)
} derive(Debug)
///|
/// The seam between the shard driver and the network: implemented by the
/// real WebSocket connection in production and by an in-memory fake in tests.
pub(open) trait GatewayTransport {
async fn recv(Self) -> GatewayFrame
async fn send(Self, String) -> Unit
async fn close(Self, code~ : Int32) -> Unit
}
///|
/// Raised by transports when the peer closed the connection. `code` is the
/// WebSocket close code (Discord uses 4xxx application codes), `None` for
/// abnormal/transport-level termination.
pub(all) suberror TransportClosed {
TransportClosed(code~ : Int32?, reason~ : String)
} derive(Debug)
///|
priv struct WsTransport {
conn : @websocket.Conn
closed : Ref[Bool]
max_message_bytes : Int32
}
///|
/// The largest Gateway message a shard accepts by default, compressed or
/// not: far above anything Discord sends, but a bound on what one message can
/// make the process allocate.
pub const DEFAULT_MAX_MESSAGE_BYTES : Int32 = 67_108_864
///|
/// Raised for a Gateway configuration that cannot work.
pub(all) suberror GatewayConfigError {
/// `max_message_bytes` must be positive.
InvalidMaxMessageBytes(value~ : Int32)
} derive(Debug)
///|
/// Check a `max_message_bytes` setting: a limit below 1 refuses every
/// message, so the shard would reconnect forever without seeing Hello.
fn check_max_message_bytes(value : Int32) -> Unit raise GatewayConfigError {
if value < 1 {
raise InvalidMaxMessageBytes(value~)
}
}
///|
impl GatewayTransport for WsTransport with fn recv(self) {
let message = self.conn.recv() catch {
error if @async.is_being_cancelled() => raise error
@websocket.WebSocketError::ConnectionClosed(code, reason) =>
raise TransportClosed(
code=Some(@websocket.close_code_to_int(code)),
reason=reason.unwrap_or(""),
)
e => raise TransportClosed(code=None, reason="\{Repr(e)}")
}
let data = @websocket.read_message(message, max_bytes=self.max_message_bytes) catch {
error if @async.is_being_cancelled() => raise error
e => raise TransportClosed(code=None, reason="\{Repr(e)}")
}
guard data is Some(data) else {
// Sent to the peer too, so the shard's own close that follows is a
// no-op. The session survives, so the shard resumes.
GatewayTransport::close(self, code=OVERSIZED_CLOSE_CODE) catch {
error if @async.is_being_cancelled() => raise error
_ => ()
}
raise TransportClosed(
code=Some(OVERSIZED_CLOSE_CODE),
reason="message larger than \{self.max_message_bytes} bytes",
)
}
match message.kind {
Text =>
Text(
@utf8.decode(data) catch {
e => raise TransportClosed(code=None, reason="\{Repr(e)}")
},
)
Binary => Binary(data)
}
}
///|
impl GatewayTransport for WsTransport with fn send(self, text) {
self.conn.send_text(text)
}
///|
impl GatewayTransport for WsTransport with fn close(self, code~) {
if self.closed.val {
return
}
self.closed.val = true
// `send_close` only writes the WebSocket close frame. The underlying
// transport must be closed even when that write fails or is cancelled.
defer self.conn.close()
@async.with_timeout(1000, () => {
self.conn.send_close(code=@websocket.close_code_from_int(code))
})
}
///|
/// Connect to the gateway over WebSocket (the default connector). A message
/// larger than `max_message_bytes` ends the connection with code 1009
/// (Message Too Big), or 4000 on JavaScript, whose WebSocket API can send
/// only 1000 and 3000-4999.
pub async fn connect_websocket(
url : String,
max_message_bytes~ : Int32 = DEFAULT_MAX_MESSAGE_BYTES,
) -> &GatewayTransport {
check_max_message_bytes(max_message_bytes)
let conn = @websocket.connect(url)
WsTransport::{ conn, closed: Ref(false), max_message_bytes, }
}