glyde/websocket
A WebSocket client.
let assert Ok(bye) = websocket.close_code(1000)
let socket = websocket.open("wss://gateway.discord.gg/?v=10")
let socket = websocket.send_text(socket, identify)
let socket = websocket.close(socket, bye, "bye")
Every call hands back the socket to use next, the writes included: a write
that fails is what ends a socket most of the time, and the reason for it
comes out of the next poll.
Mask bytes and the handshake nonce come from crypto:strong_rand_bytes,
never from glyde/rng, which is reproducible and so is the very thing
masking exists to stop.
No reconnect: glyde/gateway owns redialling. No ping, though a peer’s is
answered here and never reported. No extensions, no ws://, no server
role, no subprotocols, no request headers, no backpressure.
The events are glyde/transport.Event, not ours: they are the vocabulary
every transport speaks, and one of those is this client.
Types
A close code a client is allowed to put on the wire, and the only thing
close accepts: a code it would have turned down cannot reach it.
pub opaque type CloseCode
A socket and what it still owes the caller. It carries bytes read but not yet handed over, and every call that changes it hands back a new one, so always use the newest.
Nothing can be reported before open returns, so the events it cannot
deliver wait in here until the first poll.
pub opaque type Socket
Values
pub fn close(
socket: Socket,
code: CloseCode,
reason: String,
) -> Socket
Start closing: the frame goes out and the transport comes down, so carry
the socket this hands back. It refuses every write and owes exactly one
Closed(1006, ""), which a later poll reports.
The reason is cut to 123 bytes. Closing a socket that is already ending or finished leaves it alone.
pub fn close_code(number: Int) -> Result(CloseCode, Int)
1000, or anything from 3000 to 4999, which is what RFC 6455 lets a client
send. Discord uses 1000 to end a session and 4000 to keep it resumable.
The Error hands the number back.
pub const dial_timeout: Int
Milliseconds. Our number: Discord’s gateway answers in well under a second.
pub fn drop(socket: Socket) -> Socket
Abandon the socket, with no close frame. For a peer that has already gone
or a socket that was never up. It owes the same one Closed close does,
so nothing is lost by a caller that counts them.
pub fn failed(reason: String) -> Socket
A socket for a dial that never got as far as answering, because the
platform raised under it. It behaves like any other failed dial: no writes,
and the reason out of the first poll.
pub fn finished(socket: Socket) -> Bool
Whether Closed has been reported and there is nothing left to do.
pub fn live(socket: Socket) -> Bool
Whether a write would be attempted. False from the moment the socket starts
ending, which is one poll before finished can be true.
pub const max_buffered_bytes: Int
Bytes one socket will hold with no whole message out of them before it
gives up on the peer. The same ceiling glyde/gateway puts on a
reassembled payload, which is the largest thing that can arrive.
pub fn open(url: String) -> Socket
Dial a wss URL.
Blocks for up to dial_timeout. Nothing is reported here, a failed dial
included: that comes out of the first poll.
pub fn poll(
socket: Socket,
timeout timeout: Int,
) -> #(Socket, List(transport.Event))
turn with the events handed back instead of reported.
pub fn send_bytes(socket: Socket, bytes: BitArray) -> Socket
Write one binary frame. Answers like send_text.
pub fn send_text(socket: Socket, text: String) -> Socket
Write one text frame. A socket whose write failed comes back ending, so
live says whether it went out and the next poll says why it did not.
pub fn turn(
socket: Socket,
timeout timeout: Int,
report report: fn(transport.Event) -> Nil,
) -> Socket
Read for up to timeout milliseconds and hand what came of it to report.
Stop once finished says so: a finished socket returns at once, so a loop
that keeps going spins. The timeout bounds the whole read, so a peer that
sends bytes without finishing a message cannot stretch it.