glyde/gateway/frame

The {op, s, t, d} envelope, in both directions.

Structure is strict, content is lenient: a frame needs an opcode and a dispatch needs a sequence and a name. Anything else Discord sends is read or ignored, never fatal.

Types

HELLO’s heartbeat_interval, already inside [1000, 600_000]. Opaque and built nowhere but parse: an interval of 0 read straight off the wire would arm a repeating timer with no delay, so there is no way to write one.

pub opaque type HeartbeatInterval
pub type Inbound {
  Dispatch(seq: Int, name: String, data: dynamic.Dynamic)
  HeartbeatRequest
  Reconnect
  InvalidSession(resumable: Bool)
  Hello(interval: HeartbeatInterval)
  HeartbeatAck
  UnknownOp(op: Int)
  Undecodable(reason: Unreadable)
}

Constructors

  • Dispatch(seq: Int, name: String, data: dynamic.Dynamic)

    op 0. seq and name exist exactly when the opcode is 0.

  • HeartbeatRequest

    op 1. Discord wants a heartbeat right now.

  • Reconnect

    op 7.

  • InvalidSession(resumable: Bool)

    op 9. d is whether the session may still be resumed.

  • Hello(interval: HeartbeatInterval)

    op 10.

  • HeartbeatAck

    op 11.

  • UnknownOp(op: Int)

    A well formed frame with an opcode glyde does not model. Discord adding one must not kill a session.

  • Undecodable(reason: Unreadable)

    Not a frame we can read at all.

Discord’s gateway opcode table, both directions. The numbers are theirs, 5 was removed and is not here, and the names carry an Op prefix because Inbound already spells four of them.

pub type Opcode {
  OpDispatch
  OpHeartbeat
  OpIdentify
  OpPresenceUpdate
  OpVoiceStateUpdate
  OpResume
  OpReconnect
  OpRequestGuildMembers
  OpInvalidSession
  OpHello
  OpHeartbeatAck
}

Constructors

  • OpDispatch
  • OpHeartbeat
  • OpIdentify
  • OpPresenceUpdate
  • OpVoiceStateUpdate
  • OpResume
  • OpReconnect
  • OpRequestGuildMembers
  • OpInvalidSession
  • OpHello
  • OpHeartbeatAck

A serialised payload and the opcode that built it. Opaque so the two cannot disagree: outbound is the only constructor, and it derives the text from the opcode. text is the only thing that goes on the socket, and IDENTIFY carries the token, so a log line is the easiest place to leak one.

pub opaque type Outbound

The six opcodes a client ever writes. Outbound is typed on this rather than Opcode, so a frame carrying a receive-only op cannot be built.

pub type SendOp {
  SendHeartbeat
  SendIdentify
  SendPresenceUpdate
  SendVoiceStateUpdate
  SendResume
  SendRequestGuildMembers
}

Constructors

  • SendHeartbeat
  • SendIdentify
  • SendPresenceUpdate
  • SendVoiceStateUpdate
  • SendResume
  • SendRequestGuildMembers

Why a frame could not be read. A value, not a sentence: the caller decides how to say it, and a test can assert which one it got.

pub type Unreadable {
  BadEnvelope
  NotJson
  DispatchWithoutSequence
  DispatchWithoutName
  HelloIntervalUnreadable
}

Constructors

  • BadEnvelope

    JSON, but not an {"op": n, ...} envelope. Covers a top level that is not an object, an op that is not a number, and no op at all.

  • NotJson
  • DispatchWithoutSequence
  • DispatchWithoutName
  • HelloIntervalUnreadable

Values

pub fn describe_unreadable(why: Unreadable) -> String

Each line has to be true of everything its variant covers: BadEnvelope also catches a non-object top level and an op spelled as a string, and a heartbeat_interval can be present and still not be a whole number.

pub fn heartbeat(seq: option.Option(Int)) -> Outbound

None before this session has seen a dispatch: Discord wants a literal null, and a 0 asks for a replay from the start and earns an op 9.

pub fn heartbeat_interval_ms(interval: HeartbeatInterval) -> Int
pub fn opcode_from_int(value: Int) -> option.Option(Opcode)

The inverse of opcode_to_int. None for 5 and for anything Discord adds after this table was written.

pub fn opcode_to_int(op: Opcode) -> Int
pub fn outbound(op: SendOp, data: json.Json) -> Outbound

One gateway payload, serialised. s and t are receive only, so an outbound frame is op and d. Every frame glyde sends is built here, including the ones glyde/gateway/command encodes.

pub fn outbound_op(outbound: Outbound) -> SendOp

The opcode this frame was built with. Carried so nothing downstream has to read it back out of the JSON.

pub fn outbound_text(outbound: Outbound) -> String

The serialised frame, and the only thing that goes on the socket.

pub fn parse(text: String) -> Inbound

Total: never returns an error. HELLO’s interval is clamped on the way through, so every HeartbeatInterval that comes out is armable.

pub fn resume(
  token secret: token.Token,
  session_id session_id: String,
  seq seq: Int,
) -> Outbound
pub fn send_op_to_int(op: SendOp) -> Int
Search Document