glyde/websocket/frame
RFC 6455 framing for a client, as pure Gleam over BitArray. No FFI, no
socket, no randomness.
decode and encode are one frame each way. take feeds decoded frames
into an Assembly and hands back whole messages, since a message may
arrive as several frames with control frames in between.
Client role only: an inbound masked frame is rejected, every outbound frame
is masked and sets FIN, and a reserved bit is rejected rather than ignored.
An inbound length padded into a longer form than it needs is accepted.
Close codes stay plain numbers; glyde/gateway/close reads them.
Types
A message being put back together. Unbounded: a caller that wants a cap on
endless fragmenting measures Fragmented’s payload itself.
pub type Assembly {
Idle
Fragmented(text: Bool, payload: BitArray)
}
Constructors
-
Idle -
Fragmented(text: Bool, payload: BitArray)textis taken from the first fragment, since continuations no longer say.
pub type CloseBody {
NoCloseCode
CloseCode(code: Int, reason: String)
TruncatedCloseBody
UnreadableReason(code: Int)
}
Constructors
-
NoCloseCodeNo payload. Allowed, and it means the peer gave no code.
-
CloseCode(code: Int, reason: String) -
TruncatedCloseBodyA single byte, too few to hold a code. Nothing in it to read.
-
UnreadableReason(code: Int)The code read, and the reason after it is not UTF-8. Kept apart from
TruncatedCloseBodybecause the code is the part a caller acts on.
The four bytes a client masks a frame with. RFC 6455 wants them unpredictable, so draw them from a real random source, never a seeded one.
pub type Mask {
Mask(a: Int, b: Int, c: Int, d: Int)
}
Constructors
-
Mask(a: Int, b: Int, c: Int, d: Int)
A complete message, or a control frame, which is always complete.
pub type Message {
TextMessage(text: String)
BinaryMessage(bytes: BitArray)
PingMessage(payload: BitArray)
PongMessage(payload: BitArray)
CloseMessage(body: CloseBody)
}
Constructors
-
TextMessage(text: String) -
BinaryMessage(bytes: BitArray) -
PingMessage(payload: BitArray) -
PongMessage(payload: BitArray) -
CloseMessage(body: CloseBody)
pub type Opcode {
Continuation
Text
Binary
Close
Ping
Pong
Reserved(code: Int)
}
Constructors
-
ContinuationMore of the message that came before.
-
Text -
Binary -
Close -
Ping -
Pong -
Reserved(code: Int)3 to 7 and 11 to 15, which RFC 6455 reserves. A value rather than an error keeps the codec total;
takestill ends the connection.
What encode will build. Only these five: a client never sends a reserved
opcode, and a Continuation has nothing to continue when every frame out
of here sets FIN.
pub type Sendable {
SendText
SendBinary
SendPing
SendPong
SendClose
}
Constructors
-
SendText -
SendBinary -
SendPing -
SendPong -
SendClose
Why the bytes are not RFC 6455 any more. A value rather than a sentence, so a caller can act on one without reading English.
pub type Violation {
ReservedBitsSet
InboundFrameMasked
PayloadTooLarge
FragmentedControlFrame(opcode: Opcode)
ControlPayloadTooLarge(length: Int)
ReservedOpcode(code: Int)
ContinuationWithNoMessage
MessageBeforeLastFinished(opcode: Opcode)
TextNotUtf8
}
Constructors
-
ReservedBitsSet -
InboundFrameMasked -
PayloadTooLargeA declared length above
max_payload_bytes. The length is not carried: the high bytes alone refuse it, before the rest are read. -
FragmentedControlFrame(opcode: Opcode) -
ControlPayloadTooLarge(length: Int) -
ReservedOpcode(code: Int) -
ContinuationWithNoMessage -
MessageBeforeLastFinished(opcode: Opcode) -
TextNotUtf8
Values
pub fn close_body(payload: BitArray) -> CloseBody
pub fn close_payload(code: Int, reason: String) -> BitArray
The reason is cut on a codepoint boundary to fit beside the code. Trims rather than fails: a reason is a courtesy, a dropped connection is not.
pub fn decode(bytes: BitArray) -> Decoded
Read one frame from the front of a buffer. Total: malformed input is a value, never a crash.
pub fn encode(
kind: Sendable,
payload: BitArray,
mask: Mask,
) -> BitArray
Build one frame, masked, with FIN set.
An oversized control payload is not enforced: truncating a pong would break
the echo, and decode refuses one on the way in. close_payload fits a
close reason for you.
A payload that is not a whole number of bytes is padded with zero bits: a frame body is counted in bytes, so there is nowhere to put the odd bits.
pub fn is_control(opcode: Opcode) -> Bool
Control frames may appear between a message’s fragments, and must arrive
whole and within max_control_payload.
pub const max_control_payload: Int
RFC 6455 caps a control frame payload at 125 bytes so that a close, ping or pong can always be sent in one piece.
pub const max_payload_bytes: Int
Our cap on a declared payload length, 2^53 - 1. The wire allows 63 bits, which would ask us to buffer a petabyte. Refused, never truncated.
pub fn opcode_from_int(code: Int) -> Opcode
pub fn opcode_to_int(opcode: Opcode) -> Int
pub fn take(assembly: Assembly, frame: Frame) -> Taken
Feed one decoded frame to the message assembler. A control frame may arrive between fragments: it is delivered alone and leaves the assembly alone.
pub fn violation_to_string(violation: Violation) -> String
For a log line or a message to a human. A caller deciding what to do reads
the Violation itself.