Sign in

    thin_ws

    Thin WebSocket FFI for MoonBit — Node.js server + browser client with typed JSON messaging

    websocket
    ffi
    node
    browser
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    5 months ago
    Downloads
    35

    No dependencies

    #thin_ws

    tiye/thin_ws is a thin WebSocket FFI package for MoonBit with typed JSON messaging.

    • Node.js server: starts a ws-based WebSocket server with UUID-keyed client registry.
    • Browser client: wraps the native WebSocket API, auto-builds the backend URL for dev/prod.

    Both sides speak typed MoonBit values. JSON serialization/deserialization is handled internally — callers never touch raw text.

    #Server usage

    let send = @thin_ws.run_json_server(
    5022, // port — nothing hardcoded
    "/ws", // path
    fn(sid) { log("connect \{sid}") },
    fn(sid, op : MyClientOp) { handle(sid, op) },
    fn(sid) { log("disconnect \{sid}") },
    )
    // send a typed message to a client
    send(sid, my_server_event)

    #Browser client usage

    let url = @thin_ws.make_ws_url("5173,5174", 5022, "/ws")
    let (socket, send) = @thin_ws.connect_json(
    url,
    fn() { log("opened") },
    fn(payload : MyServerEvent) { apply_delta(payload) },
    fn() { log("closed") },
    )
    // send a typed op to the server (no-op until the connection opens)
    send(my_client_op)

    #Key design choices

    • Generic over message types — In : @json.FromJson / Out : ToJson
    • No hardcoded ports or paths — all caller-supplied
    • Ready guard on browser side — send is safe to call before the socket opens; messages are dropped silently
    • Log helpers — server_log, ws_log, ws_warn, ws_error write to runtime monitor and console

    #Import

    In your moon.pkg (or moon.pkg.json):

    import { "tiye/thin_ws" @thin_ws, }

    node_server.mbt compiles only for ["js"] target (Node.js). browser_client.mbt compiles only for ["js"] target (browser).

    BrowserWebSocket

    #external
    pub type BrowserWebSocket

    Opaque handle to a browser-native WebSocket instance.

    NodeWsRuntime

    #external
    pub type NodeWsRuntime

    Opaque handle to the running WebSocket server and its client registry.

    connect_json

    fn[In :
    FromJson
    , Out : ToJson] connect_json(url : String, on_open : () -> Unit, on_message : (In) -> Unit, on_close : () -> Unit) -> (BrowserWebSocket, (Out) -> Unit)

    Connect to a WebSocket server with fully typed JSON messages.

    • on_open() — connection established
    • on_message(msg) — called with the already-parsed In value; JSON parse errors are logged via ws_error and dropped
    • on_close() — connection closed or errored

    Returns (socket, send_fn):
    • socket — live handle; pass to ws_close when needed
    • send_fn — typed (Out) -> Unit; serializes to JSON internally; safe to call immediately — no-ops until the connection is open (guards against CONNECTING-state race conditions)

    Example

    let url = make_ws_url("5173,5174", 5022, "/ws")
    let (socket, send) = connect_json(
    url,
    fn() { store.update(Connected) },
    fn(msg : ServerEvent) { store.update(Received(msg)) },
    fn() { store.update(Disconnected) },
    )
    // later: send(ClientOp::Login(...))

    make_ws_url

    fn make_ws_url(dev_ports : String, backend_port : Int, ws_path : String) -> String

    Build a WebSocket URL that works for both dev and production.

    dev_ports — comma-separated Vite (or other dev-server) ports, e.g. "5173,5174,5175,5176". backend_port — the port the WebSocket backend listens on. ws_path — the path, e.g. "/ws".

    When the current page is served from one of dev_ports, the WS request is routed to backend_port on the same hostname (bypasses the dev proxy). In production the request uses same-origin with ws_path.

    No ports or paths are hardcoded — fully caller-configurable.

    run_json_server

    fn[In :
    FromJson
    , Out : ToJson] run_json_server(port : Int, path : String, on_open : (String) -> Unit, on_message : (String, In) -> Unit, on_close : (String) -> Unit) -> ((String, Out) -> Unit)

    Start a WebSocket server on port at path with fully typed JSON messages.

    • on_open(sid) — called when a client connects; sid is a UUID
    • on_message(sid, msg) — called with the already-parsed In value; JSON parse errors are silently dropped
    • on_close(sid) — called when a client disconnects

    Returns a typed send function (sid, msg) -> Unit. The function serializes msg to JSON internally, so callers never touch raw text. Keep the returned value alive to prevent GC of the server runtime.

    Both port and path are caller-supplied — nothing is hardcoded.

    Example

    let send = run_json_server(
    5022,
    "/ws",
    fn(sid) { log("connect \{sid}") },
    fn(sid, op : MyClientOp) { handle(sid, op) },
    fn(sid) { log("disconnect \{sid}") },
    )
    // later: send(sid, my_server_event)

    server_log

    fn server_log(text : String) -> Unit

    Write a timestamped line to .runtime-monitor/server.log and echo to stdout.

    The log directory is created automatically. Useful for persistent server-side diagnostics without a separate logging framework.

    ws_close

    fn ws_close(socket : BrowserWebSocket) -> Unit

    Initiate a clean close handshake on an existing WebSocket.

    ws_error

    fn ws_error(text : String) -> Unit

    ws_log

    fn ws_log(text : String) -> Unit

    ws_warn

    fn ws_warn(text : String) -> Unit