ws
WebSocket client (RFC 6455). Connect to WebSocket servers, send and receive text messages, handle ping/pong automatically.
| Version | v0.1 |
| Platform | PC, ESP32 |
| Type | Native (C) |
star.mod
Usage
Functions
Open / Close
| Function | Signature | Description |
|---|---|---|
ws.open(url) |
(string) -> i32 |
Start async connection and handshake, returns handle (-1 on error) |
ws.close(conn) |
(i32) -> void |
Send close frame and disconnect |
ws.open() is non-blocking — the call returns the handle immediately and connection/handshake proceeds asynchronously. The connected event fires when the WebSocket connection is established. Register events with ws.on() before or after calling open().
Send
| Function | Signature | Description |
|---|---|---|
ws.send(conn, msg) |
(i32, string) -> i32 |
Queue a text frame, returns the payload length (-1 if the outbound buffer is full) |
ws.send() never blocks and never writes a partial frame. The frame is appended to a per-connection outbound buffer and flushed to the socket as it drains; if the buffer is full (the network cannot keep up with the send rate), the call returns -1 and the frame is not queued — this is backpressure, not a fatal error, so retry it on a later event-loop turn. The connection stays open. A return ≥ 0 means the frame was queued whole, not that the peer has received it yet.
For receiving messages, use ws.on() with the event system — see Lifecycle Events below.
Lifecycle Events
| Function | Signature | Description |
|---|---|---|
ws.on(conn, event, EventType) |
(i32, string, i32) -> i32 |
Bind lifecycle/data event to declared event type (0=success, -1=error) |
Supported events: "connected", "disconnected", "message".
event WsConnected {
conn: i32
}
event WsDisconnected {
conn: i32
}
event WsMessage {
message: string
}
ws.on(conn, "connected", WsConnected)
ws.on(conn, "disconnected", WsDisconnected)
ws.on(conn, "message", WsMessage)
on WsConnected fn(e: WsConnected): void {
console.log("connected")
}
on WsMessage fn(e: WsMessage): void {
console.log(e.message)
}
runtime.keepAlive()
ws.on registers lifecycle and data events for the connection. The native layer emits the appropriate event struct when the lifecycle transition or incoming data occurs. See Events for full details.
Auto-Reconnect
| Function | Signature | Description |
|---|---|---|
ws.setAutoReconnect(conn, enabled) |
(i32, bool) -> void |
Enable/disable automatic reconnection on unexpected disconnect |
ws.setReconnectDelays(conn, delays) |
(i32, array) -> void |
Set backoff delay schedule in seconds (max 8 entries) |
var conn: i32 = ws.open("ws://192.168.1.10:8080/stream")
ws.setAutoReconnect(conn, true)
ws.setReconnectDelays(conn, [1, 2, 5, 10, 30])
When auto-reconnect is enabled and the connection drops unexpectedly:
WsDisconnectedevent fires- After the first delay (1s), the native layer attempts to reconnect
- If reconnect fails, the next delay in the array is used (2s, 5s, 10s...)
- The last delay value repeats indefinitely until connection succeeds
- On successful reconnect,
WsConnectedfires and the retry index resets to 0
If no delays are configured, a default of 1 second is used. Calling ws.close() explicitly disables auto-reconnect — only unexpected disconnects trigger it.
Status
| Function | Signature | Description |
|---|---|---|
ws.state(conn) |
(i32) -> string |
Connection state: "connecting", "open", or "closed" |
ws.ping(conn) |
(i32) -> void |
Send ping frame |
ws.setTimeout(conn, ms) |
(i32, i32) -> void |
No-op, kept for API compatibility — the socket is permanently non-blocking |
URL Format
| Component | Default |
|---|---|
| Port | 80 (ws), 443 (wss) |
| Path | / |
Pattern: Event-driven Data Stream
package main
import "ws"
import "json"
event WsConnected {
conn: i32
}
event WsMessage {
message: string
}
fn main():void {
var conn: i32 = ws.open("ws://192.168.1.10:8080/sensors")
ws.on(conn, "connected", WsConnected)
ws.on(conn, "message", WsMessage)
on WsConnected fn(e: WsConnected): void {
console.log("streaming")
}
on WsMessage fn(e: WsMessage): void {
var data: dict = json.parse(e.message)
console.log(data["value"])
}
runtime.keepAlive()
}
Pattern: Command Channel
package main
import "ws"
import "json"
event WsConnected {
conn: i32
}
event WsMessage {
message: string
}
var gConn: i32 = -1
fn main():void {
gConn = ws.open("ws://server:9000/control")
ws.on(gConn, "connected", WsConnected)
ws.on(gConn, "message", WsMessage)
on WsConnected fn(e: WsConnected): void {
# send command once connected — response arrives as event
ws.send(gConn, json.stringify({"cmd": "start", "id": 42}))
}
on WsMessage fn(e: WsMessage): void {
var resp: dict = json.parse(e.message)
console.log(resp["status"])
}
runtime.keepAlive()
}
Pattern: Resilient WebSocket Stream
package main
import "ws"
import "json"
event WsConnected {
conn: i32
}
event WsDisconnected {
conn: i32
}
event WsMessage {
message: string
}
fn main():void {
var conn: i32 = ws.open("ws://192.168.1.10:8080/sensors")
ws.setAutoReconnect(conn, true)
ws.setReconnectDelays(conn, [1, 2, 5, 10, 30])
ws.on(conn, "connected", WsConnected)
ws.on(conn, "disconnected", WsDisconnected)
ws.on(conn, "message", WsMessage)
on WsConnected fn(e: WsConnected): void {
console.log("connected")
}
on WsDisconnected fn(e: WsDisconnected): void {
console.log("disconnected — will reconnect")
}
on WsMessage fn(e: WsMessage): void {
var data: dict = json.parse(e.message)
console.log(data["value"])
}
runtime.keepAlive()
}
After server restart or network glitch, the connection is automatically restored with full WebSocket handshake.
Notes
- RFC 6455 compliant handshake with SHA-1 + Base64 key verification
- Client always masks outgoing frames (per spec)
- Auto ping/pong: incoming pings are answered with pong during event loop
- Close handshake:
ws.close()sends close frame before disconnecting - Up to 16 concurrent connections on host, 2 on ESP32 (smaller receive buffers on-device)
- Non-blocking socket from open through close: the connect, handshake, and frame reads all advance from a state machine driven by the event loop, so the VM is never blocked waiting on the network
- Outbound frames are queued per connection and flushed as the socket drains; a
frame is queued whole or rejected whole (
ws.sendreturns -1 under backpressure), so a slow network never tears a frame on the wire - Auto-reconnect with configurable backoff delays (max 8 entries, last value repeats)
- Explicit
ws.close()disables auto-reconnect - Text frames only in v0.1 — binary frame support planned
- No WSS (TLS) yet — planned for future version
- On ESP32, uses
lwIPsocket layer; refuses to open until WiFi has an IP - Event field order is verified at runtime via signature hash — see Events