moonasgi

    ASGI 3.0 for MoonBit: the interface between servers and frameworks.

    asgi
    web
    server
    Download zip
    Version
    0.10.4
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    990

    #moonasgi

    The ASGI 3.0 seam for MoonBit. A scope goes in, an event stream comes out, and the transport stays out of your code.

     Documentation     Guide     API reference     mooncakes 


    A server receives bytes and produces events. An application reads events and produces more. moonasgi is the agreement between them: three scopes, one event set, and the callables that carry them.

    Nothing here owns a socket. That means the same application runs behind a real server, inside a test, and on every backend a MoonBit program compiles to.

    #Install

    moon add moonbitstack/moonasgi

    #A first handler

    let greet : @moonasgi.Handler = fn(_request) {
    @moonasgi.Response::text("hello")
    }

    let client = @moonasgi.TestClient::new(greet)
    let response = client.get("/")
    println("\{response.status} \{response.text()}")

    200 hello

    TestClient runs the handler in-process: it builds a scope, feeds the request in, and reassembles what comes out. No socket, no async runtime, no server.

    #The interface

    NameWhat it is
    ScopeHttp · WebSocket · Lifespan — the connection, as typed data
    EventEvery message in both directions, one variant each, extensions included
    Receiveasync () -> Event — pull the next event in
    Sendasync (Event) -> Unit — push the next event out
    AsgiAppasync (Scope, Receive, Send) -> Unit — an application
    Handler(Request) -> Response — the short form a server lifts onto AsgiApp
    Middleware(Handler) -> Handler — one handler wrapped around another

    The scopes and the event set are the specification itself: HttpScope carries the method, path, raw query, headers as byte pairs, the peers, root_path, state and extensions; WebSocketScope adds the subprotocols on offer; LifespanScope carries the state a startup seeds.

    #Running without a server

    Five cores drive an application synchronously, so a whole request can happen inside a test:

    CoreDrives
    run_httpa Handler over a scope and an inbound event list
    run_http_scopeda scope-level application, with the full HttpScope
    run_http_streama StreamingResponse, lowered to the wire order
    ws_runa WebSocket connection, connect to close
    run_lifespanstartup and shutdown, in the order a server runs them

    let answer = @moonasgi.run_http(greet, scope, inbound) // Array[Event]

    #Responses

    @moonasgi.Response::text("hello") // 200, text/plain
    @moonasgi.Response::json(status=201, { "id": 233 }) // any status
    @moonasgi.Response::new(204, [("x-trace", "1")], b"") // headers and raw bytes

    A body that arrives in pieces, hints before the headers, or trailers at the end is a StreamingResponse:

    @moonasgi.StreamingResponse::new(
    status=200,
    headers=[("content-type", "text/event-stream")],
    chunks=[b"data: 233\n\n", b"data: done\n\n"],
    trailers=[("x-digest", "9f2c")],
    early_hints=[["</style.css>; rel=preload"]],
    )

    events() lowers it to the exact sequence a server writes: 103, then the start, then each chunk, then the trailers.

    #WebSocket

    let handler = @moonasgi.WebSocketHandler::new(
    on_receive=fn(message) {
    match message {
    Text(text) => [SendText("echo:" + text)]
    Binary(bytes) => [SendBinary(bytes)]
    }
    },
    )

    Connect can answer Accept (with a subprotocol and headers), Reject (a close code), or DenyHttp (a whole HTTP response). ws_run drives a connection; TestClient::websocket captures it as a WsTestSession.

    #Lifespan

    let lifecycle = @moonasgi.LifespanHandler::new(
    on_startup=fn(scope) {
    scope.state["db_pool"] = Json::string("pool://main")
    Complete
    },
    on_shutdown=fn(_scope) { Complete },
    )

    Startup seeds scope.state in place. A server copies that map onto every request scope, so a pool or a loaded config reaches a handler without a global. A failed startup ends the boot.

    #Middleware

    mid wraps any application, whatever built it. Seven ship with the package:

    MiddlewareWhat it doesDefault
    corsAnswers preflights, echoes the origin, exposes headersno origin, GET
    gzipCompresses a single-message body that is large enough500 octets, level 9
    trusted_hostsRefuses other Host values, redirects to www.any host, redirect on
    https_redirecthttp to https, ws to wss, by 307—
    proxy_headersRestores the client and scheme from a trusted proxytrusts 127.0.0.1
    body_limitAnswers 413 over the limit, by length or as it streams1 MiB
    server_errorsA 500 when the answer has not started, then the error onno detail

    let app : @moonasgi.AsgiApp = async fn(_, receive, send) {
    ignore(receive())
    send(HttpResponseStart(status=200, headers=[], trailers=false))
    send(HttpResponseBody(body=b"ok", more_body=false))
    }

    let stacked = @mid.stack(
    [
    @mid.server_errors,
    @mid.TrustedHosts::new(hosts=["example.com"]),
    @mid.cors,
    @mid.gzip,
    ],
    app,
    )

    Each is a record, so a preset can be adjusted in place — { ..@mid.gzip, level: 1 } says the same as Gzip::new(level=1). Your own middleware is a Wrap, or a record implementing Layer, and stacks with the rest.

    #Several applications behind one

    let app = @dispatch.mount([
    ("/api", api_app),
    ("/mcp", mcp_app),
    ])
    let by_host = @dispatch.hosts([("docs.example.com", docs_app)], fallback=app)

    mount matches whole path segments and prefers the longest prefix; the chosen application sees the path below its prefix, with root_path extended. hosts matches the Host header, and *.example.com covers a domain. A path or host nothing takes is answered 404, or a declined handshake on a WebSocket.

    The lifespan reaches every application behind the dispatcher: startup in order, shutdown in reverse, and one failed startup fails the whole boot.

    #Synchronous applications

    let legacy : @wsgi.WsgiApp = (environ, start_response) => {
    ignore(start_response("200 OK", [("Content-Type", "text/plain")]))
    [@utf8.encode("hello from "), @utf8.encode(environ.path_info)].iter()
    }

    let app : @moonasgi.AsgiApp = @wsgi.to_asgi(legacy)

    Environ carries the CGI variables as fields, and to_map gives them under their usual names. The request body is read before the application runs; each chunk it yields is sent as it comes.

    #Extensions

    The optional parts of the specification are typed, not stringly keyed: http.response.push, pathsend, zerocopysend, early_hint, debug, trailers, websocket.http.response, and tls. A server advertises them on the scope's extensions, and an application checks the flag before emitting the matching event. An extension the specification does not name goes in the same map, by name.

    #HTTP/2 and HTTP/3

    http_version on a scope is "1.0", "1.1", "2" or "3", and an application runs unchanged on any of them. The request line is the one place the transport shows through: on a multiplexed connection it arrives as pseudo-headers.

    let scope = @moonasgi.HttpScope::from_h2_headers([
    (":method", "POST"),
    (":scheme", "https"),
    (":authority", "example.test"),
    (":path", "/items?page=2"),
    ])

    The pseudo-headers are consumed into the typed fields, host is synthesised from :authority, and a malformed set is refused with the first thing wrong with it.

    #Packages

    PackageWhat is in it
    moonbitstack/moonasgiEverything below, re-exported under one import
    …/specThe scopes, the events, the extensions, the HTTP/2 lowering
    …/httpRequest, Response, StreamingResponse, the HTTP cores
    …/wsWebSocketHandler, WsAccept, WsMessage, ws_run
    …/lifespanLifespanHandler and run_lifespan
    …/clientTestClient for handlers and WebSocket sessions
    …/midThe middleware stack and the seven above
    …/dispatchmount, hosts and a shared lifespan
    …/wsgiA synchronous application on the same events
    …/testingA real AsgiApp driven in-process, event by event
    …/conformThe conformance harness and the ordering rules

    The seam itself has no dependencies. mid uses a compression package; testing and dispatch use an async runtime for their event loop.

    #Examples

    Each is a runnable package under src/examples:

    moon run src/examples/00-hello

    #ExampleShows
    00helloA handler, a test client, and the same handler on the raw seam
    01responsesText, JSON, bytes, custom status and headers
    02request-methodsEvery verb, the request line, a streamed body
    03middlewareA chain of handler transformers, outermost first
    04streamingChunks, trailers and 103 Early Hints
    05scopeThe full scope a framework binds to
    06extensionsCapability flags and the extension events
    07websocketAccept, reject, deny, echo
    08lifespanStartup seeding state, and a failed boot
    09validateNaming the first ordering violation
    10http2Lowering a HEADERS block
    11headersThe latin-1 header codec
    12conformanceThe harness, run
    13legacyThe older double-callable shape
    14stackReal middleware around a real application
    15testingAn async application tested in-process
    16dispatchThree applications behind one
    17wsgiA synchronous application, mounted
    18traceEvery event of a request, in order

    #License

    Apache-2.0.

    AsgiApp

    The load-bearing ASGI callable: (scope, receive, send). Every server binds to this shape and every framework in the suite ultimately compiles down to it.

    AsgiAppInstance

    type AsgiAppInstance = async (async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit

    The asynchronous instance a legacy app's first callable returns: already bound to the scope, it drives the connection with receive / send.

    AsgiApplication

    An application in either calling convention — the current 3.0 single-callable form or the legacy 2.0 double-callable form. A server holds one of these and normalizes it with guarantee_single_callable; the enum is the typed, reflection-free way to name which convention an application is written in.

    AsgiVersion

    The asgi sub-dict every scope carries: the ASGI protocol version ("3.0") and the spec_version of the concrete http / websocket / lifespan sub-spec the server implements. Applications negotiate optional behaviour against spec_version with at_least.

    ConformanceReport

    The outcome of the ASGI 3.0 conformance harness: how many round-trip checks ran, how many passed, and the names of any that failures. A clean run has an empty failures. Reusable beyond the test suite — a server or framework built on the seam can call run_conformance to self-verify the wiring.

    Drain

    Why a body drain stopped: the request body finished, the client went away mid-request, or the stream ran out without either. A long-polling or streaming handler cares about the difference — Disconnected means nothing it sends will arrive, so cleanup is all that is left to do.

    Event

    A protocol event flowing between server and application. Replaces ASGI's stringly-typed message dicts with a typed sum covering the http / websocket / lifespan message sets in both directions, including the standard extension messages (server push, path-send, zero-copy send, response trailers, early hints, debug, and WebSocket denial with a full HTTP response).

    EventOrderError

    Why an outbound event stream is malformed. ASGI pins the order an application may emit messages in — a body before the response starts, a second start, a websocket frame before the handshake is accepted, and so on are all protocol violations. validate_events walks a stream and names the first violation it finds; a well-formed stream returns None. A server can run this over its own emissions to catch a buggy app early, and the conformance harness drives a negative table through it.

    Extensions

    The scope's extensions map, modelled as typed capability flags plus the one extension that carries data (tls). A server sets a flag to advertise that it will honour the matching outbound event — an application checks the flag before emitting HttpResponsePush / HttpResponsePathSend / HttpResponseEarlyHint / HttpResponseTrailers / WebSocketHttpResponseStart, exactly as an ASGI app tests "http.response.push" in scope["extensions"].

    Handler

    The ergonomic request→response function most handlers are written as. The synchronous sugar the suite lifts onto @spec.AsgiApp at the server boundary.

    Http2HeaderError

    Why a HEADERS frame does not map onto an HttpScope field-by-field: an HTTP/2 (RFC 7540 §8.1.2.3) or HTTP/3 request carries its request line as four pseudo-headers — :method, :scheme, :authority, :path — interleaved with the ordinary fields, and ASGI reserves no slot for them in scope["headers"]. A conforming server must consume the pseudo-headers into the scope's typed fields and hand the application only the ordinary headers, with host synthesised from :authority. moonasgi owns that lowering so every h2/h2c/h3 transport produces the same scope from the same frame, and an app never sees a :-prefixed header.

    A malformed pseudo-header set — a missing required one, a duplicate, an unknown :foo, or a pseudo-header after an ordinary field — is rejected with the exact Http2HeaderError, matching RFC 7540's "malformed request" treatment, so the transport can answer RST_STREAM(PROTOCOL_ERROR) instead of forwarding a bad scope.

    HttpScope

    HTTP connection scope (ASGI type == "http"). raw_path / query_string stay Bytes because they are not guaranteed valid UTF-8; header names follow ASGI's lowercased-latin1 convention. asgi carries the version handshake, extensions the advertised server capabilities, and state is per-connection scratch copied from the lifespan state.

    LegacyAsgiApp

    A legacy ASGI 2.0 double-callable application: a synchronous application(scope) that returns the AsgiAppInstance.

    LifespanHandler

    A synchronous lifespan application in the startup / shutdown shape, the lifespan analog of the http Handler and the WebSocket WebSocketHandler. on_startup runs once at boot: it seeds the scope's state in place (the map the server then copies onto every request scope — a DB pool handle, a loaded config) and returns Complete or Failed. on_shutdown runs once at teardown to release those resources. Driven in-process by run_lifespan, so boot/teardown logic is testable on every backend without an async runtime — the synchronous core a server lifts onto the lifespan protocol.

    LifespanReply

    An application's answer to a lifespan phase: Complete when startup or shutdown succeeded, or Failed with a message the server logs and (for startup) aborts the boot on. Mirrors the two replies ASGI allows to each lifespan message — lifespan.startup.complete / .failed and lifespan.shutdown.complete / .failed.

    LifespanScope

    Lifespan scope (ASGI type == "lifespan"): a single run spanning process startup and shutdown, whose state seeds every request scope. Carries its own asgi handshake — lifespan versions independently (sub-spec 2.0), so it does not borrow the http/websocket spec_version.

    Middleware

    A handler transformer that wraps a downstream Handler to add cross-cutting behaviour. Composed as an onion where the first registered is outermost.

    PushPromise

    A server-side push promise captured by the TestClient: the pushed path and the request headers the server would send for it (http.response.push).

    Receive

    type Receive = async () ->
    Event

    Pull the next inbound event. The async awaitable an application calls to read request body chunks, websocket frames, or lifespan signals.

    Request

    An inbound HTTP request in ergonomic form: the request line, headers, and the fully-read body. The sugar over @spec.HttpScope plus a drained Receive, so a Handler never touches the async transport directly.

    Response

    An outbound HTTP response: status, headers, and the full body. Mutable so middleware can decorate it before the server serialises it into HttpResponseStart + HttpResponseBody.

    Scope

    The connection scope: one value per HTTP request, WebSocket connection, or lifespan run. It carries the immutable connection metadata a server hands to an application, mirroring ASGI 3.0's scope dict as a typed sum.

    Send

    type Send = async (
    Event
    ) -> Unit

    Push an outbound event. The async awaitable an application calls to emit response start/body, websocket frames, or lifespan completion.

    StreamHandler

    A streaming handler: produces a StreamingResponse (multi-chunk body, optional trailers) instead of a single buffered Response. Driven by run_http_stream and the TestClient.

    StreamingResponse

    A streamed outbound response: the status line, headers, an ordered list of body chunks each emitted as its own HttpResponseBody, and optional trailing trailers. Models ASGI response streaming (multiple body messages with more_body: true) and the http.response.trailers extension without an async transport. events lowers it to the exact event sequence a server would send. early_hints (the http.response.early_hint extension) are the 103 Early Hints messages emitted ahead of the final response, each element a list of Link header values.

    TestClient

    An in-process application driver — the ASGI TestClient — built on the synchronous @http.run_http_app core, so it needs no socket and runs on every backend. It builds a synthetic http scope, feeds the request body in as HttpRequest events, runs the application, and reassembles the outbound stream into a TestResponse. app is the event-emitting application; the new / from_stream constructors adapt a @http.Handler / @http.StreamHandler.

    TestResponse

    The materialised result of driving an application in-process: the response status and headers, the body reassembled from every HttpResponseBody chunk, the trailing headers gathered from HttpResponseTrailers, any push promises, the pathsend path if the app used the path-send extension, the last zerocopysend handoff, every debug info payload, and the early_hints (each a 103 message's Link values). The captured, sans-transport analog of what a real client would observe.

    TlsExtension

    The tls extension's per-connection data (ASGI TLS extension). Present in a scope's extensions only when the connection is TLS-terminated by the server. Certificates are PEM text; tls_version / cipher_suite are the numeric IANA identifiers, None when the server does not expose them.

    WebSocketHandler

    A synchronous WebSocket application in the connect / receive / disconnect shape, the WebSocket analog of the http Handler. on_connect decides the handshake from the scope; on_receive maps each inbound WsMessage to the frames to send back (it may Close); on_disconnect runs when the client goes away, carrying the close code and the optional close reason (ASGI 2.5). Driven in-process by ws_run and the TestClient, so accept/subprotocol/echo/close logic is testable on every backend without an async socket — the faithful synchronous core the async server lifts onto AsgiApp.

    WebSocketScope

    WebSocket connection scope (ASGI type == "websocket"). scheme is "ws"/"wss"; subprotocols are the client-offered values. Carries the same asgi handshake and extensions capabilities as an http scope.

    WsAccept

    The application's answer to a WebSocketConnect, mirroring the three replies ASGI allows to an opening handshake: Accept it (optionally choosing a subprotocol and adding response headers), Reject it with a bare close code (ASGI closes the handshake with websocket.close), or DenyHttp it with a full HTTP response (the websocket.http.response extension — a real status
    • headers + body instead of a bare close).

    WsMessage

    A message crossing a WebSocket in either direction: a UTF-8 Text frame or a Binary frame. The ergonomic form of WebSocketReceive / WebSocketSendText / WebSocketSendBytes, so a WebSocketHandler reasons over messages instead of raw events.

    WsSend

    An outbound action a WebSocketHandler takes while the connection is open: send a SendText / SendBinary frame, or Close the connection with a code and reason. A Close ends the handler's send stream — the driver stops pulling further inbound messages, mirroring a server that has closed the socket.

    WsTestSession

    The materialised result of driving a WebSocket connection in-process: whether the handshake was accepted, the negotiated subprotocol and accept_headers, every server→client message (messages), whether the server closed and with what close_code / close_reason, and — when the handshake was denied with the websocket.http.response extension — the captured HTTP denial response. The WebSocket analog of TestResponse.

    ZeroCopySend

    A http.response.zerocopysend message captured by the TestClient: the open file descriptor the app handed off, and the optional byte offset / count. The sans-transport analog of the server performing the zero-copy send.

    double_to_single_callable

    Wrap a legacy 2.0 double-callable app as a 3.0 single-callable AsgiApp: call the first callable with the scope to get the instance, then drive it with receive / send.

    guarantee_single_callable

    Normalize any application to a single-callable AsgiApp: a 3.0 app passes straight through, and a legacy 2.0 app is wrapped. A server calls this once and then only ever drives single-callables.

    headers_from_wire

    fn headers_from_wire(wire : Array[(Bytes, Bytes)]) -> Array[(String, String)]

    Decode a wire header list (raw ASGI byte-string pairs, as they arrive off an HTTP/1.1 or HPACK frame) into the (String, String) pairs the seam uses.

    headers_to_wire

    fn headers_to_wire(headers : Array[(String, String)]) -> Array[(Bytes, Bytes)]

    Encode the seam's header list back to raw ASGI byte-string pairs for the wire. The exact inverse of headers_from_wire, byte for byte.

    latin1_decode

    fn latin1_decode(bytes : Bytes) -> String

    Decode a raw byte string to a latin-1 String: each byte becomes the character with that code point. Total — every byte 0x00..0xFF is a valid code point.

    latin1_encode

    fn latin1_encode(s : String) -> Bytes

    Encode a String back to a header byte string. The inverse of latin1_decode for anything that came from the wire, since HTTP header values are latin-1 by the protocol's own rules.

    A character above U+00FF never came off a wire — it was put there by a framework setting, say, a content-disposition filename — and truncating it to its low byte would silently corrupt it. Those are written as their UTF-8 bytes instead, which is what real servers emit and what RFC 6266 §4.3 expects a recipient to decode.

    percent_decode

    fn percent_decode(target : String) -> Bytes

    Percent-decode a request target's path into the characters ASGI's path carries, leaving the undecoded bytes for raw_path. %2F inside a segment therefore reaches the application as a slash in path, which is why a router that cares about segment boundaries reads raw_path.

    A stray % or a truncated escape is passed through as written rather than raising: the target is attacker-controlled, and a server that refuses to build a scope cannot answer 400 either.

    run_conformance

    Run the ASGI 3.0 conformance harness: a table-driven battery that drives every @spec.Event variant and every Scope field through @http.run_http, ws_run, and the @client.TestClient, asserting round-trip fidelity — a value put in comes back unchanged, and no two distinct events or scope shapes are confused — and a negative table that asserts validate_events rejects every malformed ordering (a body before the response starts, a websocket frame before the handshake, a lifespan shutdown reply before startup, and the rest) with the exact violation. Returns a ConformanceReport naming any check that failed, so the suite (and any downstream server) can assert ok().

    run_http

    The synchronous counterpart of to_asgi: drive a Handler over an already materialised inbound event sequence and return the outbound events a server would send — [HttpResponseStart, HttpResponseBody] for an http scope, [] otherwise. A thin run_http_app wrapper over Response::events, the sans-transport core to_asgi mirrors with async receive/send.

    run_http_app

    The Request-level sans-transport core: drain the http request body from inbound, hand the assembled Request to app, and return the outbound events app emits. The ergonomic wrapper over run_http_scoped for apps that only need the request line, headers, and body — every server and the TestClient drive a Handler through it. Non-http scopes yield [].

    run_http_scoped

    The scope-aware sans-transport core: drain the http request body from inbound (accumulating HttpRequest chunks until more_body is false) and hand app the full @spec.HttpScope alongside the assembled body, returning the outbound events app emits. This is the seam a framework binds to — a framework reads what the ergonomic Request drops: root_path (for mounted sub-apps), extensions (to gate a feature on what the server advertises), client/server peers, the asgi handshake, and state (the map a lifespan startup seeds and the server copies onto every request scope). It is the synchronous analog of the ASGI (scope, receive, send) callable for the common drain-then-handle shape; run_http_app is the Request-level wrapper over it. Non-http scopes yield [].

    run_http_scoped_drain

    run_http_scoped, but the app also learns why the body drain stopped. A handler that holds a connection open — long polling, a slow upload — needs to tell "the client sent everything" from "the client went away", which the plain form cannot: it runs the handler to completion either way and the response goes nowhere. Non-http scopes yield [].

    run_http_stream

    Drive a streaming StreamHandler over an inbound event sequence, returning the full outbound stream — HttpResponseStart, one HttpResponseBody per chunk (more_body: true on all but the last), and a trailing HttpResponseTrailers when the response carries trailers. The streaming counterpart of run_http.

    run_legacy

    The synchronous core of the legacy two-call convention, testable without an async runtime the way run_http is for to_asgi: the first callable takes the scope and returns an instance that folds the inbound event stream into the outbound one. double_to_single_callable is the async lift of exactly this two-step shape, so a green run_legacy covers the convention's semantics that the async wrapper relies on.

    run_lifespan

    Drive a LifespanHandler over a materialised inbound event stream, folding it into the outbound replies a server would send. LifespanStartup runs on_startup and emits LifespanStartupComplete or LifespanStartupFailed; a failed startup ends the run, since ASGI has the server abort the boot and never send lifespan.shutdown. LifespanShutdown runs on_shutdown and emits the matching shutdown reply. The handler mutates scope.state in place during startup, exactly as an ASGI app populates scope["state"]. This is the synchronous lifespan core mirroring run_http / ws_run; a non-lifespan scope yields [].

    to_asgi

    Lift a synchronous Handler onto the load-bearing @spec.AsgiApp the server binds to. For an http scope it drains the request body — looping receive() and accumulating HttpRequest chunks until more_body is false — assembles a Request, runs the handler, then emits HttpResponseStart followed by a single HttpResponseBody. Non-http scopes (websocket, lifespan) are no-ops: this sugar covers request→response handlers only. Shares its drain, request assembly, and response serialisation with run_http, which tests the same logic without the async transport.

    validate_events

    Validate an outbound event stream against the ordering rules for its scope, dispatching to the http / websocket / lifespan validator. The public entry a server calls to check an application's emissions before writing them to the wire.

    validate_http_response

    Validate the outbound event stream an application emits for an http request, against ASGI's http response ordering: at most one HttpResponseStart, no body or pathsend/zerocopysend before it, early hints only ahead of it, a body stream that terminates once (more_body: false), trailers only when the start promised them and only after the body completes, and nothing after the response is fully sent. HttpResponseDebug must come once and before the start; HttpResponsePush only after it; HttpResponsePathSend cannot be mixed with a body. Returns the first violation, or None for a well-formed complete response.

    validate_inbound

    Check an inbound event an application is about to act on. ASGI requires exactly one of bytes or text to be set on a websocket.receive, and a frame that sets neither, or both, is a server bug the application should not have to guess about. Returns None for a well-formed event.

    validate_lifespan_replies

    Validate the outbound replies an application emits for a lifespan run: a startup reply (LifespanStartupComplete / LifespanStartupFailed) before a shutdown reply (LifespanShutdownComplete / LifespanShutdownFailed), each phase answered at most once. Returns the first violation, or None.

    validate_ws_response

    Validate the outbound event stream an application emits for a websocket connection, against ASGI's handshake ordering: the connect must be answered with an WebSocketAccept, a WebSocketClose, or a denial (WebSocketHttpResponseStart + body); frames are only legal once accepted; nothing follows a close or a completed denial; a denial cannot mix with an accept. Returns the first violation, or None for a well-formed stream.

    ws_run

    Drive a WebSocketHandler over an inbound event sequence, returning the outbound events a server would send. The WebSocket counterpart of run_http: a WebSocket scope is driven through drive_ws; any other scope yields [], since this sugar covers websocket connections only.

    ws_run_app

    The general sans-transport WebSocket core: hand the whole inbound event stream and the @spec.WebSocketScope to app and return the outbound events it emits. The escape hatch under ws_run for applications whose control flow is not the connect/receive/disconnect fold — an app is free to inspect every scope field (subprotocols, headers, extensions, state) and emit any sequence. Non-websocket scopes yield []. The WebSocket analog of run_http_app.

    Source Files

    Powered by MoonBit

    Site sourceReport issueModulesSkillsBuild queueStatistics

    © 2026 mooncakes.io