moonasgiASGI 3.0

ASGI 3.0 for MoonBit

moonasgi

One typed contract between a server and an application. A scope goes in, events come out, and the transport stays out of your code.

$ moon add moonbitstack/moonasgi

The contract

Three scopes cover everything a server can hand an application. Three callables cover everything the two say to each other. Nothing else crosses the boundary.

src/spec/asgi.mbt
pub(all) enum Scope {
  Http(HttpScope)
  WebSocket(WebSocketScope)
  Lifespan(LifespanScope)
}

pub type Receive = async () -> Event          // pull the next event in
pub type Send = async (Event) -> Unit         // push the next event out
pub type AsgiApp = async (Scope, Receive, Send) -> Unit

An application is anything with that shape. A handler is the short version, and the server lifts it on the way in:

src/http/http.mbt
pub type Handler = (Request) -> Response
pub type Middleware = (Handler) -> Handler

Numbers

The whole surface, counted.

3 scopes

http, websocket, lifespan. Each one a typed record, not a dictionary.

25 events

Every message in both directions, extensions included, plus an Other for your own.

5 synchronous cores

run_http, run_http_stream, run_http_scoped, ws_run, run_lifespan. No socket, no event loop.

7 middleware

CORS, gzip, trusted hosts, HTTPS redirect, proxy headers, body limit, server errors.

The pieces

Eleven packages, but you import one. Each is a slice of the same seam, and none of them carries a transport.

The contract

spec holds the scopes, the events, the extension flags and the HTTP/2 lowering. It has no dependencies.

Request and response

http gives you a Request to read and a Response to return, then lowers them onto events.

WebSocket and lifespan

A handler written as connect / receive / disconnect; a service written as startup / shutdown. Both run in a test.

Middleware

mid wraps any application, whatever built it. Write one as a plain function and it stacks with the rest.

One server, many applications

dispatch picks by path prefix or host name, and runs one lifespan across all of them.

Applications from before

wsgi runs a synchronous application on the same events. spec does the same for the older double-callable shape.

One request

A handler, lifted onto the seam and driven start to finish:

moonbit
fn greet(request : @moonasgi.Request) -> @moonasgi.Response {
  @moonasgi.Response::text("hello from the seam")
}

let app : @moonasgi.AsgiApp = @moonasgi.to_asgi(greet)
what crosses the seamscope GET /notes · HTTP/1.1 · http receive http.request body 0 B · more_body false application the handler runs GET /notes send http.response.start 200 · 1 headers send http.response.body body 19 B

Two events out, one in, and the handler never learned there was a socket. The event timeline plays the same recording for eight more shapes of request, one event at a time.