# WebSockets `http/ws-*` provides RFC 6455 framed bidirectional streams — both client and server — over plain `ws://` (and `wss://` from the client side via mbedTLS). The API mirrors `net/*` and uses the same per-fd epoll callbacks. ## `http/ws-*` Stdlib functions | Function | Returns | Purpose | |---|---|---| | `[http/ws-server $port %{ :on-connect $fn }]` | ws_server resource | Start a WebSocket server | | `[http/ws-connect $url %{ :on-message $fn :on-close $fn :headers $map }]` | future of ws_conn | Connect to a server | | `[http/ws-send $conn $msg]` | nil | Send a text frame | | `[http/ws-send-binary $conn $bytes]` | nil | Send a binary frame | | `[http/ws-ping $conn]` | nil | Send a ping frame | | `[http/ws-close $conn ?code ?reason]` | nil | Close a connection | | `[http/ws-on-message $conn $fn]` | nil | Register on-message callback | | `[http/ws-on-close $conn $fn]` | nil | Register on-close callback | | `[http/ws-stop $server]` | nil | Stop a server | ## WebSocket Server
 def on-connect {|conn|
    @doc "A new client connected. Register handlers on the connection."
    echo "ws client connected"
    http/ws-on-message $conn { |msg|
        echo "received: $msg"
        http/ws-send $conn "echo: $msg"
    }
    http/ws-on-close $conn {
        echo "ws client disconnected"
    }
}

def srv [http/ws-server 8080 %{ :on-connect $on-connect }]
echo "ws server on :8080"
sleep 60000 
## WebSocket Client `http/ws-connect` returns a **future** that resolves to a `ws_conn` resource once the handshake completes. Pass callbacks in the option map to handle messages from the start, or attach them later with `http/ws-on-message`.
 def on-message {|msg| echo "got: $msg" }
def on-close {|code reason| echo "closed: $code $reason" }

def conn-fut [http/ws-connect "ws://echo.example.com" %{
    :on-message $on-message
    :on-close $on-close
}]

def conn [await $conn-fut]
http/ws-send $conn "hello"

# For TLS:
# def conn-fut [http/ws-connect "wss://api.example.com" %{ :on-message $on-message }] 
## Frames Three frame kinds: text, binary, ping. Send with the matching stdlib function:
 http/ws-send $conn "plain text frame"
http/ws-send-binary $conn $byte-list
http/ws-ping $conn                              # server should auto-pong 
The `on-message` callback fires once per text or binary frame. Binary frames arrive as a list of byte values; text frames arrive as strings. ## Closing
 http/ws-close $conn                              # default code, no reason
http/ws-close $conn 1000 "bye"                   # explicit code and reason 
Standard close codes: `1000` (normal), `1001` (going away), `1008` (policy violation), `1011` (server error). The remote side's `on-close` handler receives `(code, reason)`. ## Server Lifecycle
 def srv [http/ws-server 8080 %{ :on-connect $on-connect }]

# ... run for a while ...

http/ws-stop $srv                                # stop accepting, drop connections 
## Architecture Notes - Each `WsConn` is heap-allocated on `std.heap.c_allocator` so it survives the VM that started it (matches the HTTP server's design). - Per-fd epoll callbacks drive a state machine: handshake phases → open → frame parser. - Frames are parsed incrementally; one `posix.read` can yield several frames, and one frame can span several reads. - `wss://` client connections reuse mbedTLS via the same BIO-callback model as `http_async.zig`. Server-side TLS is not currently supported. - Callbacks fire synchronously from inside the epoll callback, like `net/on-data`. ## Frame Size Limits | Limit | Default | |---|---| | Read buffer | 16 KB | | Max frame size | 1 MB | | Max handshake size | 64 KB | Frames larger than 1 MB are rejected and the connection closed. ## Examples ### Broadcast server
 def clients ^{}

def on-connect {|conn|
    @doc "Track clients in a set so we can broadcast to all."
    $clients << $conn
    http/ws-on-message $conn { |msg|
        # Broadcast to every connected client
        for c in $clients {
            if ($c != $conn) { http/ws-send $c $msg }
        }
    }
    http/ws-on-close $conn {
        # Manual removal — sets don't support removal, so re-add all-but-this
        set clients [into ^{} [filter [collect $clients] { $it != $conn }]]
    }
}

def srv [http/ws-server 8080 %{ :on-connect $on-connect }]
sleep 600000 
### JSON RPC client
 def on-message {|msg|
    try {
        set data [json/parse $msg]
        echo "result: $data~result"
    } catch _e {
        echo "bad json: $msg"
    }
}

def c [await [http/ws-connect "wss://rpc.example.com" %{ :on-message $on-message }]]

http/ws-send $c [json/stringify %{ :jsonrpc "2.0" :id 1 :method "ping" }] 
## Quick Reference | Form | Purpose | |---|---| | `[http/ws-server $port %{ :on-connect $fn }]` | Start server | | `[http/ws-connect $url %{ :on-message $fn ... }]` | Connect (returns future) | | `[http/ws-send $conn $msg]` | Text frame | | `[http/ws-send-binary $conn $bytes]` | Binary frame | | `[http/ws-ping $conn]` | Ping | | `[http/ws-close $conn ?code ?reason]` | Close | | `[http/ws-on-message $conn $fn]` | Set message handler | | `[http/ws-on-close $conn $fn]` | Set close handler | | `[http/ws-stop $server]` | Stop server |