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)

    text is taken from the first fragment, since continuations no longer say.

pub type CloseBody {
  NoCloseCode
  CloseCode(code: Int, reason: String)
  TruncatedCloseBody
  UnreadableReason(code: Int)
}

Constructors

  • NoCloseCode

    No payload. Allowed, and it means the peer gave no code.

  • CloseCode(code: Int, reason: String)
  • TruncatedCloseBody

    A 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 TruncatedCloseBody because the code is the part a caller acts on.

pub type Decoded {
  Complete(frame: Frame, rest: BitArray)
  Incomplete
  Invalid(reason: Violation)
}

Constructors

  • Complete(frame: Frame, rest: BitArray)

    rest goes straight back into decode: one read often lands several frames.

  • Incomplete
  • Invalid(reason: Violation)

    These bytes cannot be a frame. The connection is finished.

pub type Frame {
  Frame(fin: Bool, opcode: Opcode, payload: BitArray)
}

Constructors

  • Frame(fin: Bool, opcode: Opcode, payload: BitArray)

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

  • Continuation

    More 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; take still 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
pub type Taken {
  Whole(message: Message, assembly: Assembly)
  Held(assembly: Assembly)
  Broken(reason: Violation)
}

Constructors

  • Whole(message: Message, assembly: Assembly)

    A control frame that arrived between fragments hands the assembly back untouched.

  • Held(assembly: Assembly)

    A fragment stored. Nothing to deliver yet.

  • Broken(reason: Violation)

    The fragment sequence is broken, so the connection is finished.

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
  • PayloadTooLarge

    A 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.

Search Document