Gleam primitives for text-frame boundaries

Socket.IO frames, kept deliberately small.

windsock encodes and decodes the text packets your higher-level protocol already knows how to name. It handles the frame shape; you keep ownership of schemas, sessions, transports, and application vocabulary.

A frame library, not a client stack.

windsock is intentionally narrow. It does not implement namespaces, binary attachments, ACK packet ids, Engine.IO transport negotiation, reconnection, or a full Socket.IO client/server. Those choices stay above this layer.

Everything exposed has one protocol job.

Socket.IO event frames

Encode and decode text packets shaped like 42["event", ...args].

windsock.encode("submitOp", [json.string("client-1")])

Dynamic positional args

Keep event arguments dynamic so callers decode with their own schema.

Incoming(event: String, args: List(Dynamic))

Engine.IO heartbeat constants

Expose ping and pong packets without owning transport negotiation.

windsock.ping == "2" && windsock.pong == "3"

Create a frame, then decode at your own boundary.

Encode

import gleam/json
import windsock

pub fn frame() {
  windsock.encode("submitOp", [
    json.string("client-1"),
    json.preprocessed_array([]),
  ])
}

Decode

case windsock.decode("42[\"op\",\"c1\"]") {
  Ok(incoming) -> incoming.event
  Error(windsock.InvalidFormat(reason)) -> reason
  Error(windsock.InvalidJson(reason)) -> reason
}

Small return types make failure explicit.

Core functions

  • encode(event, args) returns a 42[...] event packet.
  • encode_heartbeat() returns the Engine.IO ping packet.
  • decode(text) returns Result(Incoming, DecodeError).

Decode errors

InvalidJson
The frame prefix was right, but the JSON payload could not parse.
InvalidFormat
The packet was not a Socket.IO event array, or the event name was not a string.