///|
/// Opaque handle to the running WebSocket server and its client registry.
#external
pub type NodeWsRuntime
///|
/// 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.
pub extern "js" fn server_log(text : String) -> Unit =
#| (text) => {
#| const fs = require("fs");
#| const path = require("path");
#| const dir = path.join(process.cwd(), ".runtime-monitor");
#| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
#| const file = path.join(dir, "server.log");
#| fs.appendFileSync(file, new Date().toISOString() + " " + text + "\n");
#| console.log("[ws-server] " + text);
#| }
///|
extern "js" fn _start_ws_server(
port : Int,
path : String,
on_open : (String) -> Unit,
on_message : (String, String) -> Unit,
on_close : (String) -> Unit,
) -> NodeWsRuntime =
#| (port, path, onOpen, onMessage, onClose) => {
#| const crypto = require("crypto");
#| const { WebSocketServer } = require("ws");
#| const clients = new Map();
#| const wss = new WebSocketServer({ port, path });
#| wss.on("connection", (socket) => {
#| const sid = crypto.randomUUID();
#| clients.set(sid, socket);
#| onOpen(sid);
#| socket.on("message", (buf) => onMessage(sid, String(buf)));
#| socket.on("close", () => { clients.delete(sid); onClose(sid); });
#| });
#| wss.on("listening", () =>
#| console.log(`[ws-server] listening on ws://localhost:${port}${path}`)
#| );
#| return { wss, clients };
#| }
///|
extern "js" fn _send_ws(
runtime : NodeWsRuntime,
sid : String,
text : String,
) -> Unit =
#| (rt, sid, text) => {
#| const socket = rt.clients.get(sid);
#| if (socket && socket.readyState === 1) socket.send(text);
#| }
///|
/// 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
///
/// ```moonbit nocheck
/// 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)
/// ```
pub fn[In : @json.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 {
let runtime = _start_ws_server(
port,
path,
on_open,
fn(sid, text) {
try {
let msg : In = @json.from_json(@json.parse(text))
on_message(sid, msg)
} catch {
_ => ()
}
},
on_close,
)
fn(sid : String, msg : Out) {
_send_ws(runtime, sid, msg.to_json().stringify())
}
}