// WebSocket routes (← FastAPI's `@app.websocket(path)`). A handler talks to a
// `WebSocket` value — accept, receive, send, close — whose actions are recorded
// as `@moonasgi.Event`s (`WebSocketAccept` / `WebSocketSendText` / … / the WS
// half of the moonasgi SEAM). The handler is a synchronous core, so it runs
// identically in a test (drive an in-memory frame queue with `drive_websocket`)
// and under a real server. `App::to_asgi` provides the async serving shell.
///|
/// Register a WebSocket route (← FastAPI's `@app.websocket(path)`). The path
/// matches with the same `:name` segment rules as HTTP routes.
pub fn App::websocket(
self : App,
path : String,
handler : @socket.WsHandler,
) -> Unit {
self.ws_routes.push({ path, handler, })
}
///|
/// Match a WebSocket path against the registered routes, returning the handler
/// and the extracted path parameters.
fn App::match_ws(
self : App,
path : String,
) -> (@socket.WsHandler, Map[String, String])? {
for route in self.ws_routes {
match match_path(route.path, path) {
Some(params) => return Some((route.handler, params))
None => ()
}
}
None
}
///|
/// The async serving shell for a WebSocket scope, driven by `App::to_asgi`. It
/// buffers the client's inbound frames off the SEAM, runs the handler's sync
/// core, then emits its accept / send / close events.
///
/// Because the sync core cannot suspend on the async transport (MoonBit's async
/// wall — see the README design note), it sees the client's whole frame
/// sequence before it runs rather than interleaving live. Message content and
/// order are preserved, which is exact for echo, broadcast, and request-reply
/// handlers; live per-frame duplex is the documented boundary.
async fn App::serve_websocket(
self : App,
scope : @moonasgi.WebSocketScope,
receive : @moonasgi.Receive,
send : @moonasgi.Send,
) -> Unit {
let (handler, params) = match self.match_ws(scope.path) {
Some(pair) => pair
None => {
send(@moonasgi.Event::WebSocketClose(code=1000, reason="no route"))
return
}
}
// Consume the connect, then drain frames until the client disconnects.
let _ = receive()
let inbox : Array[@socket.WsMessage] = []
let mut open = true
while open {
match receive() {
WebSocketReceive(text~, bytes~) =>
match text {
Some(t) => inbox.push(WsText(t))
None =>
match bytes {
Some(b) => inbox.push(WsBinary(b))
None => ()
}
}
_ => open = false
}
}
let sock = @socket.WebSocket::new(inbox, params, scope.subprotocols)
handler(sock)
for ev in sock.outbox {
send(ev)
}
}