Packages

A small, typed, ergonomic WebSocket layer for Gleam applications built on top of Mist

Current section

Files

Jump to
wist src wist.gleam
Raw

src/wist.gleam

//// Wist provides a transport-neutral, stateful WebSocket programming model for Gleam.
////
//// This module contains the core data structures, codecs, events, effects, and handler
//// constructors. It represents the "port" in Wist's ports-and-adapters architecture,
//// defining the API interface that developers use to write WebSocket connection logic.
////
//// Transports (like the Mist web server) consume the `Handler` defined here and run it
//// using a specific transport adapter (e.g. `wist/adapters/mist`).
import gleam/option.{type Option}
/// A transport-neutral representation of the HTTP WebSocket handshake request.
///
/// It contains connection-specific metadata that is available before the connection
/// is upgraded.
///
/// Use `Context` to extract authentication tokens from headers, route based on path,
/// or load initial configuration parameters.
pub type Context {
Context(
/// The HTTP path of the WebSocket request (e.g., `"/ws"`).
path: String,
/// A list of HTTP request headers sent by the client during upgrade.
headers: List(#(String, String)),
/// The raw query string, if present (e.g., `Some("token=xyz")`).
query: Option(String),
)
}
/// Represents the WebSocket close code and close reason payload sent on the wire.
///
/// Under the hood, close frames are encoded with a 16-bit status code and an
/// optional UTF-8 string payload.
pub type CloseFrame {
CloseFrame(
/// The 16-bit WebSocket status code (e.g. `1000` for normal closure).
code: Int,
/// The human-readable reason payload.
reason: String,
)
}
/// Represents the classification of why a WebSocket connection closed.
pub type CloseReason {
/// The connection closed cleanly via a normal close handshake.
Normal
/// The connection closed due to a protocol violation, crash, or transport-level error.
Abnormal(String)
/// The connection closed without a clear code or details.
Unknown
}
/// Represents an error encountered while decoding a raw WebSocket frame.
pub type DecodeError {
DecodeError(String)
}
/// Represents an error encountered while encoding an application message.
pub type EncodeError {
EncodeError(String)
}
/// Represents physical socket or TCP/SSL transport failures.
pub type SocketError {
SocketError(String)
}
/// Represents all standard WebSocket frame types defined by RFC-6455.
pub type Frame {
/// A UTF-8 text data frame.
Text(String)
/// A binary data frame.
Binary(BitArray)
/// A control Ping frame carrying a raw payload.
Ping(BitArray)
/// A control Pong frame carrying a raw payload.
Pong(BitArray)
/// A control Close frame indicating connection shutdown.
Close(Option(CloseFrame))
}
/// Represents connection lifecycle and message events dispatched to `Handler.update`.
pub type Event(message) {
/// Dispatched exactly once when the connection has been successfully established.
/// Generate any initial welcome messages or startup pushes here.
Opened
/// Dispatched when a new application message is successfully received and decoded.
Message(message)
/// Dispatched when the connection closes. This is a terminal event; any returned
/// effects will be ignored by the runtime.
Closed(CloseReason)
/// Dispatched when a socket or protocol failure occurs. This is a terminal event.
Failed(SocketError)
}
/// Represents output actions or side-effects returned by the handler's reducer.
pub type Effect(message) {
/// Encode and send a typed application message to the client.
Send(message)
/// Send a raw WebSocket data frame directly to the client.
SendFrame(Frame)
/// Initiate the WebSocket close handshake carrying optional close metadata.
CloseConnection(Option(CloseFrame))
}
/// A Codec specifies how to serialize and deserialize messages.
///
/// It translates raw wire `Frame`s to typed `inbound` messages, and typed
/// `outbound` messages back to raw `Frame`s.
pub type Codec(inbound, outbound) {
Codec(
decode: fn(Frame) -> Result(inbound, DecodeError),
encode: fn(outbound) -> Result(Frame, EncodeError),
)
}
/// A no-op codec that maps raw `Frame`s directly to and from themselves.
///
/// Useful for handlers that process raw text or binary frames directly.
///
/// ### Example
/// ```gleam
/// import wist
///
/// let codec = wist.raw_codec()
/// ```
pub fn raw_codec() -> Codec(Frame, Frame) {
Codec(decode: fn(frame) { Ok(frame) }, encode: fn(frame) { Ok(frame) })
}
/// A stateful WebSocket handler that manages its own state and responds to events.
///
/// Handlers are opaque to ensure binary compatibility and prevent external code
/// from directly reading or mutating the handler's transitions. Create them using `handler`.
pub opaque type Handler(state, inbound, outbound) {
Handler(
init_state: fn(Context) -> state,
update: fn(state, Event(inbound)) -> #(state, List(Effect(outbound))),
)
}
/// Constructor to create a stateful WebSocket handler.
///
/// The handler defines:
/// 1. `init_state`: How to construct the connection state from the `Context`.
/// 2. `update`: A pure reducer mapping state and events to state transitions and effects.
///
/// ### Example
/// ```gleam
/// import wist
///
/// let echo = wist.handler(
/// init_state: fn(_ctx) { Nil },
/// update: fn(state, event) {
/// case event {
/// wist.Message(frame) -> #(state, [wist.SendFrame(frame)])
/// _ -> #(state, [])
/// }
/// }
/// )
/// ```
pub fn handler(
init_state init_state: fn(Context) -> state,
update update: fn(state, Event(inbound)) -> #(state, List(Effect(outbound))),
) -> Handler(state, inbound, outbound) {
Handler(init_state:, update:)
}
/// Internal accessor to retrieve the init_state function of a Handler.
/// Used by transport adapters to build the initial state.
@internal
pub fn get_init_state(
handler: Handler(state, inbound, outbound),
) -> fn(Context) -> state {
handler.init_state
}
/// Internal accessor to retrieve the update function of a Handler.
/// Used by transport adapters to run the connection event loop.
@internal
pub fn get_update(
handler: Handler(state, inbound, outbound),
) -> fn(state, Event(inbound)) -> #(state, List(Effect(outbound))) {
handler.update
}