sigildocs

(sigil websocket connection)

(sigil websocket connection) - WebSocket Connection Management

Handles WebSocket connections with handshake, frame I/O, and state management. Supports both ws:// (plain) and wss:// (TLS) connections.

Exports

ws-connectionprocedure

Construct a ws-connection struct.

Test if a value is a ws-connection struct.

Get the socket field of a ws-connection struct.

Get the state field of a ws-connection struct.

Get the url field of a ws-connection struct.

Get the host field of a ws-connection struct.

Get the buffer field of a ws-connection struct.

Get the fragments field of a ws-connection struct.

Get the non-blocking field of a ws-connection struct.

Set the state field of a ws-connection struct.

Set the buffer field of a ws-connection struct.

Set the fragments field of a ws-connection struct.

Set the non-blocking field of a ws-connection struct.

Set the non-blocking field of a ws-connection struct.

ws-messageprocedure

Construct a ws-message struct.

ws-message?procedure

Test if a value is a ws-message struct.

Get the type field of a ws-message struct.

Get the data field of a ws-message struct.

parse-ws-urlprocedure

Parse WebSocket URL into components Returns (scheme host port path) or #f on error

Generate random 16-byte key and base64 encode

Calculate expected Sec-WebSocket-Accept value

Build HTTP upgrade request

Parse HTTP response status line Returns status code or #f

get-headerprocedure

Extract header value (case-insensitive)

Find the first HTTP header terminator in a bytevector. The response can share a socket read with the first WebSocket frame, whose arbitrary binary payload must never be decoded as UTF-8 while locating the marker.

Read HTTP response and validate handshake Returns #t on success, #f on failure

conn-writeprocedure

Write one complete WebSocket frame. Both plain and TLS sockets may accept only a prefix when nonblocking. Keep the encoded (and masked) bytes intact and resume at the committed offset after backpressure.

conn-readprocedure

Read from socket (handles both TCP and TLS).

Returns a bytevector for plain TCP so binary WebSocket frame bytes round-trip cleanly. The legacy socket-read path returns a string and re-encodes any non-ASCII bytes through UTF-8, corrupting frame headers (the high opcode byte 0x81 in particular). TLS still goes through tls-read which handles raw bytes natively.

conn-closeprocedure

Close socket (handles both TCP and TLS)

Set socket to non-blocking mode (handles both TCP and TLS)

ws-connectvariable

Connect to a WebSocket server url: WebSocket URL (ws:// or wss://) Returns ws-connection on success, #f on failure

ws-connected?procedure

Check if connection is open

ws-sendprocedure

Send a text message

Send a binary message

ws-pingprocedure

Send a ping frame

ws-closeprocedure

Close the WebSocket connection

Internal: Read data and try to decode a message Returns ws-message, 'closed, 'need-more, or #f on error

Try to decode a complete message from buffer Returns ws-message, 'closed, 'need-more, or #f on error

process-frameprocedure

Process a decoded frame Returns ws-message, 'closed, 'need-more, or #f on error

ws-receiveprocedure

Receive a message (with async I/O support) Returns ws-message, 'closed, or #f on error When running in async scheduler, yields while waiting for data.

Receive a message that is ALREADY BUFFERED, without ever waiting. Returns ws-message, 'closed, or #f when nothing complete is buffered.

WHY THIS EXISTS. ws-receive consults the connection buffer only INSIDE the call, and when that buffer holds no complete frame it waits (await-readable, no timeout). So a caller that drives several things from one event pump cannot ask "do you already have a frame?" without risking a park. That matters because one socket read routinely yields SEVERAL complete frames: after the first is returned, the rest sit in the buffer with nothing further on the socket. A pump that gates ws-receive on socket readability — the only way to keep it from parking — then strands those frames until unrelated bytes happen to arrive. Measured against this library at 0.10.4: two 13-byte frames written in one segment followed by three seconds of silence, and the second frame surfaced 2997 ms late, with the wake frame.

CALL IT ALONE, ON A LOOP — BUT ONLY IF THE CONNECTION WAS OPENED SO THAT ITS TRANSPORT IS NON-BLOCKING. That is the whole precondition, and it is not a formality:

(ws-connect url upgrade-timeout-ms: 5000) ; non-blocking -> self-sufficient (with-async … (ws-connect url) …) ; non-blocking -> self-sufficient (ws-connect url) ; BLOCKING -> BUFFER-ONLY, DEAF

ws-connect puts the transport in non-blocking mode when a scheduler is running OR an upgrade-timeout-ms was supplied. With NEITHER, the socket stays blocking, this procedure cannot take its socket read without risking a park, so it correctly declines to — and a pump that calls it alone will never hear anything. Pass a timeout, or run under a scheduler, or do not use a nowait-only pump.

Given that precondition: it first decodes anything already buffered, then takes exactly one socket read. A pump needs nothing else, and needs no scheduler.

DO NOT pair it with socket-ready?. An earlier revision of this docstring prescribed

(or (ws-receive-nowait conn) (and (socket-ready? sock) (ws-receive conn)))

and that advice is unwritable over wss://: socket-ready? raises socket-ready?: expected socket on a TLS connection, because its native rejects anything that is not a plain socket instead of resolving the fd the way socket-select does. socket-select accepts both and is the primitive to reach for if you ever do need an external readiness gate. The two disagree about the same object; see the sigil-lib note on native_socket_readyp.

It is also the wrong shape for a no-scheduler caller for a second reason: ws-receive only yields via await-readable when a scheduler is present, so on a partial frame without one it spins.

THE RETURN CONTRACT, and the distinction is load-bearing:

#f nothing to give you RIGHT NOW — an empty buffer, a partial frame, or a connection not yet promoted to 'open. 'closed affirmatively closed, and ONLY that.

An absent answer and a negative answer must not share a value. A first draft mirrored ws-receive and returned 'closed for anything that was not ws-connected? — which REPORTS A FRESH SOCKET AS DEAD, because the state is not promoted to 'open until a receive poll has run. ws-receive can get away with that conflation only because it blocks afterwards and the window never shows; a non-blocking caller sees it immediately and tears down a healthy connection. Consumers therefore act on 'closed without needing to know anything about promotion order.