///|
type WsTextSender = (String) -> Unit
///|
type WsBinarySender = (Bytes) -> Unit
///|
type WsPongSender = () -> Unit
///|
priv enum OutboundFrame {
OutboundText(String)
OutboundBinary(Bytes)
OutboundPong
}
///|
priv struct WebSocketOutboundQueue {
mut frames : Array[OutboundFrame]
mut draining : Bool
mut closed : Bool
}
///|
fn WebSocketOutboundQueue::new() -> WebSocketOutboundQueue {
{ frames: [], draining: false, closed: false }
}
///|
let ws_text_senders : Map[String, WsTextSender] = Map([])
///|
let ws_binary_senders : Map[String, WsBinarySender] = Map([])
///|
let ws_pong_senders : Map[String, WsPongSender] = Map([])
///|
let ws_client_channels : Map[String, Array[String]] = Map([])
///|
let ws_channel_clients : Map[String, Map[String, Unit]] = Map([])
///|
let native_ws_handler_map : Map[Int, Mocket] = Map([])
///|
fn request_method_to_string(meth : @http.RequestMethod) -> String {
match meth {
Get => "GET"
Head => "HEAD"
Post => "POST"
Put => "PUT"
Delete => "DELETE"
Connect => "CONNECT"
Options => "OPTIONS"
Trace => "TRACE"
Patch => "PATCH"
}
}
///|
fn string_headers_to_views(
headers : Map[String, String],
) -> Map[StringView, StringView] {
let out : Map[StringView, StringView] = Map([])
headers.each((key, value) => out.set(key, value))
out
}
///|
fn view_headers_to_strings(
headers : Map[StringView, StringView],
) -> Map[String, String] {
let out : Map[String, String] = Map([])
headers.each((key, value) => out.set(key.to_owned(), value.to_owned()))
out
}
///|
fn header_contains_token(
headers : Map[String, String],
header_name : String,
token : String,
) -> Bool {
let token = token.to_lower()
let header_name = header_name.to_lower()
for pair in headers {
let (key, value) = pair
if key.to_lower() == header_name {
for part in value.split(",") {
if part.trim().to_lower() == token {
return true
}
}
}
}
false
}
///|
fn header_equals(
headers : Map[String, String],
header_name : String,
expected : String,
) -> Bool {
let header_name = header_name.to_lower()
let expected = expected.to_lower()
for pair in headers {
let (key, value) = pair
if key.to_lower() == header_name {
return value.trim().to_lower() == expected
}
}
false
}
///|
fn is_websocket_upgrade(request : @http.Request) -> Bool {
header_contains_token(request.headers, "connection", "upgrade") &&
header_equals(request.headers, "upgrade", "websocket")
}
///|
fn request_has_body(request : @http.Request) -> Bool {
match request.meth {
Post | Put | Patch => true
_ =>
request.headers.get("transfer-encoding") is Some(_) ||
request.headers
.get("content-length")
.map(value => value.trim() != "0")
.unwrap_or(false)
}
}
///|
fn request_route_path(path : String) -> String {
match path.find("?") {
Some(query_start) => path[:query_start].to_owned()
None => path
}
}
///|
fn find_ws_route(
mocket : Mocket,
path : String,
) -> (WebSocketHandler, Map[String, StringView])? {
match mocket.ws_static_routes.get(path) {
Some(handler) => return Some((handler, {}))
None => ()
}
for route in mocket.ws_dynamic_routes {
let (route_path, handler) = route
match match_path(route_path, path) {
Some(params) => return Some((handler, params))
None => ()
}
}
None
}
///|
fn next_ws_connection_id(port : Int) -> String {
"native-\{port}-\{@env.now()}"
}
///|
pub fn register_ws_connection(
connection_id : String,
text_sender : WsTextSender,
binary_sender : WsBinarySender,
pong_sender : WsPongSender,
) -> Unit {
ws_text_senders.set(connection_id, text_sender)
ws_binary_senders.set(connection_id, binary_sender)
ws_pong_senders.set(connection_id, pong_sender)
ws_client_channels.set(connection_id, [])
}
///|
pub fn unregister_ws_connection(connection_id : String) -> Unit {
let channels = ws_client_channels.get(connection_id)
ignore(ws_text_senders.remove(connection_id))
ignore(ws_binary_senders.remove(connection_id))
ignore(ws_pong_senders.remove(connection_id))
ignore(ws_client_channels.remove(connection_id))
match channels {
Some(channels) =>
for channel in channels {
match ws_channel_clients.get(channel) {
Some(clients) => ignore(clients.remove(connection_id))
None => ()
}
}
None => ()
}
}
///|
pub fn register_ws_handler(mocket : Mocket, port : Int) -> Unit {
native_ws_handler_map.set(port, mocket)
}
///|
pub fn ws_send(id : String, msg : String) -> Unit {
match ws_text_senders.get(id) {
Some(send) => send(msg)
None => ()
}
}
///|
pub fn ws_send_bytes(id : String, msg : Bytes) -> Unit {
match ws_binary_senders.get(id) {
Some(send) => send(msg)
None => ()
}
}
///|
pub fn ws_pong(id : String) -> Unit {
match ws_pong_senders.get(id) {
Some(send) => send()
None => ()
}
}
///|
pub fn ws_subscribe(id : String, channel : String) -> Unit {
let client_channels = match ws_client_channels.get(id) {
Some(channels) => channels
None => {
let channels = []
ws_client_channels.set(id, channels)
channels
}
}
if !client_channels.contains(channel) {
client_channels.push(channel)
}
let channel_clients = match ws_channel_clients.get(channel) {
Some(clients) => clients
None => {
let clients : Map[String, Unit] = Map([])
ws_channel_clients.set(channel, clients)
clients
}
}
channel_clients.set(id, ())
}
///|
pub fn ws_unsubscribe(id : String, channel : String) -> Unit {
match ws_channel_clients.get(channel) {
Some(clients) => ignore(clients.remove(id))
None => ()
}
match ws_client_channels.get(id) {
Some(channels) => {
let mut index = None
for i = 0; i < channels.length(); i = i + 1 {
if channels[i] == channel {
index = Some(i)
break
}
}
match index {
Some(i) => ignore(channels.remove(i))
None => ()
}
}
None => ()
}
}
///|
pub fn ws_publish(channel : String, msg : String) -> Unit {
match ws_channel_clients.get(channel) {
Some(clients) => clients.keys().each(id => ws_send(id, msg))
None => ()
}
}
///|
fn WebSocketOutboundQueue::close(self : WebSocketOutboundQueue) -> Unit {
self.closed = true
self.frames.clear()
}
///|
fn WebSocketOutboundQueue::enqueue(
self : WebSocketOutboundQueue,
ws : @websocket.Conn,
frame : OutboundFrame,
) -> Unit {
if self.closed {
return
}
self.frames.push(frame)
if !self.draining {
self.draining = true
async_run(async fn() noraise { self.drain(ws) })
}
}
///|
async fn WebSocketOutboundQueue::drain(
self : WebSocketOutboundQueue,
ws : @websocket.Conn,
) -> Unit noraise {
for ;; {
if self.closed {
self.frames.clear()
self.draining = false
return
}
if self.frames.length() == 0 {
self.draining = false
return
}
let frame = self.frames[0]
ignore(self.frames.remove(0))
try {
match frame {
OutboundText(msg) => ws.send_text(msg)
OutboundBinary(msg) => ws.send_binary(msg)
OutboundPong => ws.ping()
}
} catch {
_ => {
self.close()
self.draining = false
return
}
}
}
}
///|
fn register_native_ws_connection(
connection_id : String,
ws : @websocket.Conn,
) -> WebSocketOutboundQueue {
let outbound = WebSocketOutboundQueue::new()
register_ws_connection(
connection_id,
msg => outbound.enqueue(ws, OutboundText(msg)),
msg => outbound.enqueue(ws, OutboundBinary(msg)),
() => outbound.enqueue(ws, OutboundPong),
)
outbound
}
///|
async fn send_native_response(
request : @http.Request,
conn : @http.ServerConnection,
response : HttpResponse,
) -> Unit {
let headers = view_headers_to_strings(response.headers)
if !response.cookies.is_empty() {
let cookies = response.cookies
.values()
.map(cookie => cookie.to_string())
.to_array()
headers.set("Set-Cookie", cookies.join("\r\nSet-Cookie: "))
}
conn.send_response(response.status_code.to_int(), "OK", extra_headers=headers)
if request.meth != Head && !response.raw_body.is_empty() {
conn.write(response.raw_body)
}
conn.end_response()
}
///|
async fn handle_http_request(
mocket : Mocket,
request : @http.Request,
body_reader : &@io.Reader,
conn : @http.ServerConnection,
) -> Unit {
let raw_body = if request_has_body(request) {
body_reader.read_all().binary()
} else {
b""
}
let response = dispatch_http(
mocket,
request_method_to_string(request.meth),
request_route_path(request.path),
string_headers_to_views(request.headers),
raw_body,
)
send_native_response(request, conn, response)
}
///|
async fn handle_websocket_request(
port : Int,
mocket : Mocket,
request : @http.Request,
conn : @http.ServerConnection,
) -> Unit {
match find_ws_route(mocket, request.path) {
Some((handler, _params)) => {
let ws = @websocket.from_http_server(request, conn)
defer ws.close()
let connection_id = next_ws_connection_id(port)
let outbound = register_native_ws_connection(connection_id, ws)
defer outbound.close()
let peer = WebSocketPeer::{ connection_id, subscribed_channels: [] }
handler(Open(peer))
try {
for ;; {
let msg = ws.recv()
match msg.kind {
Text => {
let text = msg.read_all().text() catch { _ => "" }
handler(Message(peer, Text(text)))
}
Binary => handler(Message(peer, Binary(msg.read_all().binary())))
}
}
} catch {
@websocket.ConnectionClosed(_, _) => ()
_ => ()
}
outbound.close()
handler(Close(peer))
unregister_ws_connection(connection_id)
}
None => {
let response = HttpResponse::new(NotFound, raw_body=b"Not Found")
response.headers.set("Content-Type", "text/plain; charset=utf-8")
send_native_response(request, conn, response)
}
}
}
///|
pub async fn listen_ffi(mocket : Mocket, address : String) -> Unit noraise {
let address = normalize_listen_address(address)
let addr = @socket.Addr::parse(address) catch {
err => {
println("mocket: invalid native listen address \{address}: \{err}")
return
}
}
let port = addr.port()
register_ws_handler(mocket, port)
let server = @http.Server(addr) catch {
err => {
println("mocket: failed to listen on \{address}: \{err}")
return
}
}
server.run_forever((request, body_reader, conn) => {
if is_websocket_upgrade(request) {
handle_websocket_request(port, mocket, request, conn)
} else {
handle_http_request(mocket, request, body_reader, conn)
}
}) catch {
err => println("mocket: native server on \{address} stopped: \{err}")
}
}
///|
fn normalize_listen_address(address : String) -> String {
if address.has_prefix(":") {
"0.0.0.0\{address}"
} else {
address
}
}
///|
pub async fn serve_ffi(mocket : Mocket, port~ : Int) -> Unit noraise {
listen_ffi(mocket, "127.0.0.1:\{port}")
}
///|
pub fn __ws_emit(
event_type : Bytes,
connection_id : Bytes,
payload : Bytes,
) -> Unit {
let peer = WebSocketPeer::{
connection_id: @utf8.decode_lossy(connection_id),
subscribed_channels: [],
}
let handler = match native_ws_handler_map.values().collect() {
[mocket, ..] =>
match mocket.ws_static_routes.values().collect() {
[handler, ..] => handler
[] => fn(_) { }
}
[] => fn(_) { }
}
match @utf8.decode_lossy(event_type) {
"open" => handler(Open(peer))
"message" => handler(Message(peer, Text(@utf8.decode_lossy(payload))))
"binary" => handler(Message(peer, Binary(payload)))
"ping" => handler(Message(peer, Ping))
"close" => handler(Close(peer))
_ => ()
}
}