Skip to content

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

require dev-libs/ws v0.1

Usage

import "ws"

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
var conn: i32 = ws.open("ws://192.168.1.10:8080/stream")
# ... use connection ...
ws.close(conn)

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(conn, "hello server")

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:

  1. WsDisconnected event fires
  2. After the first delay (1s), the native layer attempts to reconnect
  3. If reconnect fails, the next delay in the array is used (2s, 5s, 10s...)
  4. The last delay value repeats indefinitely until connection succeeds
  5. On successful reconnect, WsConnected fires 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
if (ws.state(conn) == "open") {
    ws.ping(conn)
}

URL Format

ws://host/path
ws://host:port/path
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.send returns -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 lwIP socket layer; refuses to open until WiFi has an IP
  • Event field order is verified at runtime via signature hash — see Events