///|
/// Core serving loop. Must run inside the async runtime (an async fn main,
/// or a task spawned within one). Binds `host`:`port` (default host `127.0.0.1`); raises on bind
/// failure. GET requests matching a WebSocket route are upgraded instead of
/// dispatched.
pub async fn serve_async(
port : Int32,
handler : (Request) -> Response,
ws_routes~ : Array[WsRoute] = [],
host~ : String = "127.0.0.1",
) -> Unit {
let addr = @async_socket.Addr::parse("\{host}:\{port}")
let server = @async_http.Server(addr, reuse_addr=true)
server.run_forever(
async fn(request, body, conn) {
if serve_websocket(ws_routes, request, conn) {
return
}
let body_text = body.read_all().text() catch { _err => "" }
let resp = handler(Request::from_runtime(request, body_text))
resp.write_to(conn)
},
allow_failure=true,
)
}
///|
/// Serving loop with response caching for GET requests: hits are answered
/// before dispatch (`X-Cache: HIT`) and 200 responses are stored with the
/// policy's TTL (`X-Cache: MISS`). Cache failures degrade to plain dispatch.
pub async fn[S : CacheStore] serve_cached_async(
policy : CachePolicy[S],
port : Int32,
handler : (Request) -> Response,
ws_routes~ : Array[WsRoute] = [],
host~ : String = "127.0.0.1",
) -> Unit {
let addr = @async_socket.Addr::parse("\{host}:\{port}")
let server = @async_http.Server(addr, reuse_addr=true)
server.run_forever(
async fn(request, body, conn) {
if serve_websocket(ws_routes, request, conn) {
return
}
let body_text = body.read_all().text() catch { _err => "" }
let req = Request::from_runtime(request, body_text)
let resp = if req.verb == "GET" {
serve_one(policy, req, handler)
} else {
handler(req)
}
resp.write_to(conn)
},
allow_failure=true,
)
}
///|
/// Controls the stderr warnings emitted when the cache backend fails
/// (`cache unavailable, ...`). Enabled by default; disable to run quietly,
/// e.g. in tests that exercise cache degradation.
pub fn set_cache_warnings(enabled : Bool) -> Unit {
cache_warnings.val = enabled
}
///|
let cache_warnings : Ref[Bool] = Ref(true)
///|
/// Cache lookup or dispatch-plus-store for one GET request. Any cache
/// failure degrades to a plain dispatch.
async fn[S : CacheStore] serve_one(
policy : CachePolicy[S],
req : Request,
handler : (Request) -> Response,
) -> Response {
let cached = policy.lookup(req.path) catch {
err => {
if cache_warnings.val {
eprintln("moonway: cache unavailable, serving without cache: \{err}")
}
None
}
}
match cached {
Some(hit) => {
let resp = Response::new()
resp.set_status(200)
resp.set_header("X-Cache", "HIT")
resp.set_header("Content-Type", hit.content_type)
resp.set_body(hit.body)
resp
}
None => {
let resp = handler(req)
if resp.status() == 200 {
let content_type = resp.header("Content-Type").unwrap_or("text/plain")
let payload : CachedResponse = { content_type, body: resp.body(), }
let stored = try {
policy.store(req.path, payload)
true
} catch {
err => {
if cache_warnings.val {
eprintln(
"moonway: cache unavailable, response not cached: \{err}",
)
}
false
}
}
if stored {
resp.set_header("X-Cache", "MISS")
}
}
resp
}
}
}
///|
/// Serve HTTP/1.1 on `host`:`port` (default host `127.0.0.1`) from synchronous code: drives the
/// async runtime's event loop until the process is interrupted.
///
/// Error behavior: request parsing happens inside the runtime before user
/// code runs, and a synchronous handler cannot raise (MoonBit's type system
/// rejects raising functions where `(Context) -> Context` is expected), so
/// the bridge only ever sees parsed requests and non-raising failures.
/// Should anything still fail, the runtime aborts only the affected
/// connection (`allow_failure=true`) and the server keeps serving.
/// `Response::internal_error()` is available for explicit 500 responses.
pub fn serve(
port : Int32,
handler : (Request) -> Response,
ws_routes~ : Array[WsRoute] = [],
host~ : String = "127.0.0.1",
) -> Unit {
@async.run_async_main(async fn() {
serve_async(port, handler, ws_routes~, host~) catch {
err =>
eprintln("moonway: failed to start server on \{host}:\{port}: \{err}")
}
})
}
///|
/// Like `serve`, with response caching bound to a concrete cache store.
pub fn[S : CacheStore] serve_cached(
policy : CachePolicy[S],
port : Int32,
handler : (Request) -> Response,
ws_routes~ : Array[WsRoute] = [],
host~ : String = "127.0.0.1",
) -> Unit {
@async.run_async_main(async fn() {
serve_cached_async(policy, port, handler, ws_routes~, host~) catch {
err =>
eprintln("moonway: failed to start server on \{host}:\{port}: \{err}")
}
})
}