///|
/// Websocket support on top of the native µWebSockets binding.
///|
/// A message delivered on a websocket.
pub enum WsMessage {
Text(String)
Binary(Bytes)
}
///|
/// One open websocket connection.
pub struct WsConn {
priv id : UInt64
priv socket : UwsSocket
priv is_ssl : Bool
}
///|
/// Connection id, unique for the lifetime of the process.
pub fn WsConn::id(self : WsConn) -> UInt64 {
self.id
}
///|
/// Sends a text frame.
pub fn WsConn::send(self : WsConn, text : String) -> Unit {
let bytes = utf8_bytes(text)
if self.is_ssl {
uwb_ws_send_ssl(self.socket, bytes, bytes.length(), TEXT_OPCODE, 0)
} else {
uwb_ws_send(self.socket, bytes, bytes.length(), TEXT_OPCODE, 0)
}
}
///|
/// Sends a binary frame.
pub fn WsConn::send_bytes(self : WsConn, data : Bytes) -> Unit {
if self.is_ssl {
uwb_ws_send_ssl(self.socket, data, data.length(), BINARY_OPCODE, 0)
} else {
uwb_ws_send(self.socket, data, data.length(), BINARY_OPCODE, 0)
}
}
///|
/// Sends an arbitrary frame, e.g. a ping, optionally compressed.
pub fn WsConn::send_frame(
self : WsConn,
data : Bytes,
opcode : Int32,
compress : Bool,
) -> Unit {
let compress = if compress { 1 } else { 0 }
if self.is_ssl {
uwb_ws_send_ssl(self.socket, data, data.length(), opcode, compress)
} else {
uwb_ws_send(self.socket, data, data.length(), opcode, compress)
}
}
///|
/// Closes the connection. Defaults to a normal closure (`1000`).
pub fn WsConn::close(self : WsConn, code? : Int32, reason? : String) -> Unit {
let code = match code {
Some(value) => value
None => 1000
}
let reason = match reason {
Some(value) => value
None => ""
}
let bytes = utf8_bytes(reason)
if self.is_ssl {
uwb_ws_close_ssl(self.socket, code, bytes, bytes.length())
} else {
uwb_ws_close(self.socket, code, bytes, bytes.length())
}
}
///|
const TEXT_OPCODE : Int32 = 1
///|
const BINARY_OPCODE : Int32 = 2
///|
/// Callbacks for one websocket route. All of them are optional.
pub struct WsBehavior {
open : ((WsConn) -> Unit)?
message : ((WsConn, WsMessage) -> Unit)?
close : ((WsConn, Int32, String) -> Unit)?
}
///|
/// Builds a websocket behavior, mirroring `app.ws("/path", { open, message, close })`.
pub fn ws_behavior(
open? : (WsConn) -> Unit,
message? : (WsConn, WsMessage) -> Unit,
close? : (WsConn, Int32, String) -> Unit,
) -> WsBehavior {
{ open, message, close, }
}
///|
/// Registers a websocket route. Must be called before the app listens.
pub fn App::ws(self : App, path : String, behavior : WsBehavior) -> Unit {
let route_id = g_next_ws_route[0]
g_next_ws_route[0] = route_id + 1
g_ws_behaviors.set(route_id, behavior)
self.ws_routes.push((route_id, path))
g_ws_route_is_ssl.set(route_id, self.is_ssl)
}
// ---------- registry ----------
///|
priv struct WsConnState {
route_id : Int32
conn : WsConn
}
///|
let g_ws_behaviors : Map[Int32, WsBehavior] = Map([])
///|
let g_ws_conns : Map[UInt64, WsConnState] = Map([])
///|
let g_next_ws_route : Array[Int32] = [1]
///|
/// Maps route_id to whether the app is SSL
let g_ws_route_is_ssl : Map[Int32, Bool] = Map([])
///|
fn init_websockets() -> Unit {
let on_open : FuncRef[(UInt64, Int32, UwsSocket) -> Unit] = (
conn_id,
route_id,
ws,
) => ws_dispatch_open(conn_id, route_id, ws)
let on_message : FuncRef[(UInt64, Bytes, Int32, Int32) -> Unit] = (
conn_id,
data,
len,
opcode,
) => ws_dispatch_message(conn_id, data, len, opcode)
let on_close : FuncRef[(UInt64, Int32, Bytes, Int32) -> Unit] = (
conn_id,
code,
message,
len,
) => ws_dispatch_close(conn_id, code, message, len)
uwb_set_ws_callbacks(on_open, on_message, on_close)
}
///|
fn ws_dispatch_open(conn_id : UInt64, route_id : Int32, ws : UwsSocket) -> Unit {
let is_ssl = match g_ws_route_is_ssl.get(route_id) {
Some(v) => v
None => false
}
let conn = WsConn::{ id: conn_id, socket: ws, is_ssl, }
g_ws_conns.set(conn_id, { route_id, conn, })
match g_ws_behaviors.get(route_id) {
Some(behavior) =>
match behavior.open {
Some(handler) => handler(conn)
None => ()
}
None => ()
}
}
///|
fn ws_dispatch_message(
conn_id : UInt64,
data : Bytes,
_len : Int32,
opcode : Int32,
) -> Unit {
match g_ws_conns.get(conn_id) {
Some(state) =>
match g_ws_behaviors.get(state.route_id) {
Some(behavior) =>
match behavior.message {
Some(handler) => {
let payload = if opcode == TEXT_OPCODE {
WsMessage::Text(utf8_string(data))
} else {
WsMessage::Binary(data)
}
handler(state.conn, payload)
}
None => ()
}
None => ()
}
None => ()
}
}
///|
fn ws_dispatch_close(
conn_id : UInt64,
code : Int32,
message : Bytes,
_len : Int32,
) -> Unit {
match g_ws_conns.get(conn_id) {
Some(state) => {
g_ws_conns.remove(conn_id)
match g_ws_behaviors.get(state.route_id) {
Some(behavior) =>
match behavior.close {
Some(handler) => handler(state.conn, code, utf8_string(message))
None => ()
}
None => ()
}
}
None => ()
}
}