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
3 months ago
Downloads
21

#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 typesIn : @json.FromJson / Out : ToJson
  • No hardcoded ports or paths — all caller-supplied
  • Ready guard on browser sidesend is safe to call before the socket opens; messages are dropped silently
  • Log helpersserver_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