MoonBit-dialect ASGI 3.0 — the load-bearing server↔app SEAM (Scope / Receive / Send) that the moon* full-stack web suite (mooncat, moonapi, moonrpc, moongql, moonzero) is built around.
Moved on mooncakes from Lfan-ke/moonasgi to moonbitstack/moonasgi.
flowchart LR
net["native async<br/>(sockets, TLS, HTTP/1.1, HTTP/2)"] --> cat["mooncat<br/>(ASGI server)"]
cat -->|"Scope / Receive / Send"| asgi(["**moonasgi**<br/>SEAM"])
asgi --> api["moonapi"]
asgi --> rpc["moonrpc"]
asgi --> gql["moongql"]
asgi --> zero["moonzero"]
api --> orm["moonorm"]pub enum Scope {
Http(HttpScope)
WebSocket(WebSocketScope)
Lifespan(LifespanScope)
}
pub type Receive = async () -> Event // pull the next inbound event
pub type Send = async (Event) -> Unit // push an outbound event
pub type AsgiApp = async (Scope, Receive, Send) -> Unitpub type Handler = (Request) -> Response
pub type Middleware = (Handler) -> Handlerlet client = TestClient::new(handler) // or ::from_stream / ::from_app
let r = client.post("/echo", body=b"payload")
assert_eq(r.status, 200)
assert_eq(r.text(), "payload")let client = TestClient::new(handler)
let session = client.websocket(
path="/chat",
handler=WebSocketHandler::echo(subprotocol="chat"),
send=[Text("hello"), Binary(b"\x01\x02")],
)
assert_eq(session.accepted, true)
assert_eq(session.subprotocol, Some("chat"))
assert_eq(session.texts(), ["hello"])let app = LifespanHandler::new(
on_startup=fn(scope) {
scope.state["db_pool"] = Json::string("pool://greet") // seeded in place
Complete
},
)
let out = run_lifespan(app, Lifespan(scope), [LifespanStartup])
assert_eq(out == [LifespanStartupComplete], true)run_http_scoped(fn(scope, body) {
let prefix = scope.state.get("greeting_prefix") // lifespan-seeded
[HttpResponseStart(status=200, headers=[], trailers=false),
HttpResponseBody(body, more_body=false)]
}, Http(scope), inbound)let scope = HttpScope::from_h2_headers([
(":method", "POST"), (":scheme", "https"),
(":authority", "example.test"), (":path", "/items?page=2"),
("content-type", "application/json"),
])
// -> Ok: http_method="POST", scheme="https", path="/items",
// query_string=b"page=2", headers=[("host","example.test"), ...]let report = run_conformance()
assert_eq(report.ok(), true)let bad = [HttpResponseBody(body=b"x", more_body=false)] // no start
assert_eq(validate_events(scope, bad) == Some(BodyBeforeStart), true)| Spec feature | Modelled as | Covered by |
|---|---|---|
| http scope (all fields incl. state, raw_path, root_path) | HttpScope | conformance scope/http-fields |
| websocket scope (incl. subprotocols, state) | WebSocketScope | conformance scope/ws-fields |
| lifespan scope (incl. state) | LifespanScope | conformance scope/lifespan-fields |
| asgi version / spec_version (2.5 http+ws, 2.0 lifespan) | AsgiVersion | conformance spec_version/*; test "per-subprotocol asgi spec_version defaults" |
| spec_version numeric negotiation | AsgiVersion::at_least | test "AsgiVersion negotiates spec_version numerically" |
| Every http/ws/lifespan message, both directions | Event (25 variants) | conformance event/*; test "SEAM scope and event variants construct" |
| Full scope reaches a framework (state, root_path, extensions) | run_http_scoped | conformance scoped/*; test "greet: moonapi http routing round-trips…" |
| lifespan.startup / .shutdown (complete / failed, in-place state) | LifespanHandler / run_lifespan | conformance lifespan/*; test "greet: lifespan startup seeds state…" |
| http.request drain (streamed more_body) | run_http / TestClient | test "TestClient streams a chunked request body into the handler" |
| http.disconnect | HttpDisconnect | conformance event/* |
| http.response.start + .body round-trip | Response / run_http | conformance http/roundtrip; test "run_http drains a chunked body…" |
| Response streaming (more_body: true chunks) | StreamingResponse / run_http_stream | test "TestClient reassembles a streaming multi-chunk response body" |
| websocket.connect / .accept / .receive / .send / .close | WebSocketHandler / ws_run | test "ws_run echoes text and binary frames…" |
| websocket.disconnect (2.5 reason) | WebSocketDisconnect | test "on_disconnect receives the 2.5 close code and reason" |
| Handshake reject (bare close) | WsAccept::Reject | test "ws handler can reject the handshake with a close code" |
| ext http.response.trailers | StreamingResponse.trailers | test "TestClient captures response trailers" |
| ext http.response.push | HttpResponsePush | test "TestClient captures server push and pathsend" |
| ext http.response.pathsend | HttpResponsePathSend | test "TestClient captures server push and pathsend" |
| ext http.response.zerocopysend | HttpResponseZeroCopySend | test "zerocopysend and debug extensions round-trip…" |
| ext http.response.early_hint | HttpResponseEarlyHint | test "early-hint extension round-trips through the TestClient" |
| ext http.response.debug | HttpResponseDebug | test "zerocopysend and debug extensions round-trip…" |
| ext websocket.http.response (denial) | WsAccept::DenyHttp | test "ws handler can deny the handshake with a full HTTP response" |
| ext tls | TlsExtension | test "Extensions builder advertises capabilities and TLS data" |
| Message-ordering rules (all message sets) | validate_events | conformance order/*; test "validator names the violation…" |
| HTTP/2·3 pseudo-header → scope lowering (:method/:scheme/:authority/:path, host synthesis, malformed rejection) | HttpScope::from_h2_headers / Http2HeaderError | conformance h2/* |
| http_version transport-agnostic seam ("1.1" / "2" / "3") | HttpScope.http_version | conformance h2/seam-agnostic |
fn guarantee_single_callable(app : AsgiApplication) -> (async (Scope, async () -> Event, async (Event) -> Unit) -> Unit)fn latin1_decode(bytes : Bytes) -> Stringfn latin1_encode(s : String) -> Bytesfn percent_decode(target : String) -> BytesInstall
Download zipMoonBit-dialect ASGI 3.0 — the load-bearing server↔app SEAM (Scope / Receive / Send) that the moon* full-stack web suite (mooncat, moonapi, moonrpc, moongql, moonzero) is built around.