mooncat

A native ASGI 3.0 server for MoonBit. It runs a moonasgi app over a real network socket — accept a connection, turn each request into a Scope / Receive / Send, and drive your app — exactly the role uvicorn plays for Python.

CItestsGitHublicense
$moon add moonbitstack/mooncat

✶ The contract at a glance

pub async fn serve(app : AsgiApp, host? : String, port? : Int, backlog? : Int) -> Unit
pub async fn serve_config(app : AsgiApp, config : Config) -> Unit
pub async fn serve_tls(app : AsgiApp, certificate_file~, private_key_file~, pfx_file~, ...) -> Unit
pub async fn serve_graceful(app : AsgiApp, config : Config, handle? : ShutdownHandle) -> Unit
pub async fn serve_h2c(app : AsgiApp, host? : String, port? : Int) -> Unit

// One socket -> Scope / Receive / Send -> your moonasgi app:
//   HTTP/1.1 request  ->  Http scope     ->  HttpResponseStart + HttpResponseBody
//   HTTP/2 (h2c)      ->  Http scope     ->  HEADERS + DATA, flow-controlled
//   Upgrade: websocket ->  WebSocket scope -> connect/accept/receive/send/close
//   process boundary  ->  Lifespan scope ->  startup ... shutdown
//   handle.shutdown() ->  stop accepting -> drain -> lifespan shutdown -> close

§Serving

The accept loop that turns each connection into a Scope / Receive / Send and drives your moonasgi app: the Config record uvicorn's settings map onto, the lifespan protocol run around the listener, the graceful shutdown path that drains in-flight requests before it closes, the reload watcher, the Date header stamped on every response, and the forwarded headers a trusted proxy is believed on. HTTP/1.1 messages are read and written by moonhttp/http1; what is here is the connection's inbox that keeps a pipelined request from being lost between them.

item
async fn serve( app : @moonasgi.AsgiApp, host? : String = "127.0.0.1", port? : Int = port, backlog? : Int = 2048) -> Unit

Serve a moonasgi ASGI application over native HTTP/1.1 + WebSocket (← uvicorn). Convenience wrapper over serve_config that builds a Config from host / port / backlog. Blocks in a keep-alive accept loop until the running task is cancelled.

item
async fn serve_config(app : @moonasgi.AsgiApp, config : Config) -> Unit

Serve a moonasgi ASGI application under an explicit Config. Runs the full lifespan protocol around the accept loop: the app is invoked once under a Lifespan scope and startup() is driven **before** the listener is bound, and shutdown() is driven on the way out — even when the serving task is cancelled, via protect_from_cancel. Each accepted request is bridged by dispatch, which diverts WebSocket upgrades to the echo path.

struct
struct WsConfig

The WebSocket knobs uvicorn exposes on its own config (--ws-max-size). max_size is the ceiling on a received payload, in bytes: it bounds a single frame and, across continuation frames, a reassembled message, and a peer that announces more fails the connection with 1009. It is the knob with teeth on the receive side — an unbounded length is exactly what a hostile client sends. uvicorn's remaining WebSocket knobs are not here rather than here-and-ignored: --ws-ping-interval / --ws-ping-timeout need a heartbeat task alongside a reader that can be interrupted between messages and resumed, --ws-max-queue needs that same reader pumping a bounded queue, and neither is expressible while a native blocking read cannot be cancelled and resumed mid-stream; --ws-per-message-deflate needs a raw DEFLATE codec, which the async library's gzip package (gzip-framed streams only) does not provide. Accepting the extension without one would corrupt every frame.

fn
fn WsConfig::new(max_size? : Int = @ws.limit) -> WsConfig

Build a WsConfig, defaulting max_size to uvicorn's own 16 MiB.

struct
struct Config

Server configuration (← uvicorn Config): the bind address, the listen backlog, and the HTTP/1.1 transport knobs the moonbitlang/async server exposes. Constructed once and handed to serve_config. backlog mirrors uvicorn's listen(2) backlog. The current moonbitlang/async transport does not expose a backlog setter on its TCP listener — TcpServer calls listen() itself with a fixed depth and offers no way to re-listen — so the value is recorded on the config for parity and future use but is not applied to the socket. This is a transport-capability gap, not a behavioural choice, and it is the one field here that does nothing. The remaining fields map one-to-one onto knobs mooncat *does* honour: dual_stack / reuse_addr on the listening socket, server_headers stamped onto every response (uvicorn's Server: header lives here), max_connections for the parallel-client ceiling, and allow_failure for whether a handler error tears the whole server down.

let
let port : Int = 12000

The port every server in this family listens on unless told otherwise. Not 8000, which is uvicorn's, and not 8080: on a development machine those are taken by whatever ran last. One number of our own means every example, every README and every round of integration testing agree without anyone having to say which port they meant. HTTP, WebSocket, HTTP/2 and gRPC all share it — one listener tells them apart — and QUIC takes the same number on UDP, which is a different port space.

fn
fn Config::new( host? : String = "127.0.0.1", port? : Int = port, backlog? : Int = 2048, dual_stack? : Bool = false, reuse_addr? : Bool = true, server_headers? : Map[String, String] = Map([("server", "mooncat")]), max_connections? : Int, allow_failure? : Bool = true, graceful_timeout? : Int, root_path? : String = "", logger? : Logger = Logger::new(), timeout_keep_alive? : Int = 5000, limit_max_requests? : Int, limit_concurrency? : Int, max_head_size? : Int = 16 * 1024, date_header? : Bool = true, proxy_headers? : Bool = true, forwarded_allow_ips? : Array[String] = ["127.0.0.1"], ws? : WsConfig = WsConfig::new()) -> Config

Build a Config, defaulting to uvicorn's own defaults: host 127.0.0.1, port 12000, backlog 2048, a 5-second idle keep-alive timeout, a 16 KiB request-head cap, the Date header on, proxy headers trusted from 127.0.0.1, and no request-count or concurrency limit. Transport knobs default to the async server's own defaults: reuse_addr on (uvicorn sets SO_REUSEADDR), single-stack binding, no extra response headers, an unbounded connection ceiling, and allow_failure on so a single failing handler never crashes the listener. graceful_timeout bounds how long serve_graceful waits for in-flight requests to drain before it runs lifespan shutdown anyway (← uvicorn's timeout_graceful_shutdown); None waits until the last request finishes, as uvicorn does by default.

fn
fn Config::bind(self : Config) -> String

The host:port string used to resolve the listen address.

item
suberror LifespanError

Raised when an application reports lifespan.startup.failed or lifespan.shutdown.failed, carrying the failure message the app supplied.

struct
struct Lifespan

Drives an ASGI application's lifespan protocol (← uvicorn LifespanOn). A single long-lived invocation of the app runs under a Lifespan scope. The server and that invocation exchange messages through two async queues: the server pushes lifespan.startup / lifespan.shutdown onto inbox (which backs the app's Receive), and the app pushes its *.complete / *.failed replies onto outbox (which the app's Send writes to). state seeds the lifespan scope and is shared into request scopes by the caller.

fn
fn Lifespan::new(app : @moonasgi.AsgiApp) -> Lifespan

Create a lifespan driver for app, with empty unbounded message queues and empty lifespan state.

fn
fn Lifespan::spawn( self : Lifespan, g : @async.TaskGroup[Unit]) -> @async.Task[Unit]

Spawn the application under a Lifespan scope as a task in g, returning its handle. The task blocks on receive() until startup() / shutdown() push signals; a lifespan-aware app therefore stays parked for the server's whole life, while an app that ignores the lifespan scope simply returns at once (detected via the returned task in startup / shutdown).

item
async fn Lifespan::startup( self : Lifespan, task : @async.Task[Unit]) -> Unit

Run ASGI lifespan **startup**: push lifespan.startup and await the app's reply. Returns once the app sends lifespan.startup.complete, or once the app returns without lifespan support (its task finishing wins the race, and startup is treated as a no-op, matching uvicorn's lifespan="auto"). Raises LifespanError if the app reports lifespan.startup.failed.

item
async fn Lifespan::shutdown( self : Lifespan, task : @async.Task[Unit]) -> Unit

Run ASGI lifespan **shutdown**: push lifespan.shutdown and await the app's reply, or return immediately if the app invocation has already finished. Raises LifespanError if the app reports lifespan.shutdown.failed.

struct
struct ShutdownHandle

A handle for driving a graceful shutdown of serve_graceful from outside the serving task (← uvicorn's Server.should_exit / handle_exit). Hand one to serve_graceful, then call shutdown() from any other task to stop the server: it stops accepting, waits for in-flight requests to drain, runs the ASGI lifespan shutdown, and closes the listener, in that order. Two async queues carry the handshake. request receives the shutdown trigger (a signal delivered through the runtime's global cancellation reaches the server the same way — see serve_graceful). done is posted once the server has finished the whole shutdown sequence, so shutdown() can block until the port is actually free.

fn
fn ShutdownHandle::new() -> ShutdownHandle

Create an idle shutdown handle with empty unbounded signal queues.

item
async fn ShutdownHandle::shutdown(self : ShutdownHandle) -> Unit

Request a graceful shutdown and block until the server has drained in-flight requests, run lifespan shutdown, and closed the listener. Returns once the listen port is free again.

item
async fn ShutdownHandle::request_stop(self : ShutdownHandle) -> Unit

Request a graceful shutdown without waiting for it to finish.

item
async fn serve_graceful( app : @moonasgi.AsgiApp, config : Config, handle? : ShutdownHandle = ShutdownHandle::new()) -> Unit

Serve a moonasgi application with a uvicorn-style process model: a graceful shutdown path over a single acceptor that spawns a concurrent handler per connection. The lifespan protocol runs as in serve_config — startup before the listener binds, shutdown on the way out. A single acceptor task then drives accepted connections through the same dispatch path serve uses (over a hand-built @http.ServerConnection), spawning one handler task per connection so requests are served concurrently — and, because it is the real ServerConnection, WebSocket upgrades bridge here too. Shutdown is triggered by handle.shutdown() / handle.request_stop(), by reaching Config::limit_max_requests, or by a signal the runtime turns into global cancellation (see the boundary note below). All three converge on one sequence, run under protect_from_cancel so a signal can't abort it midway: stop accepting (cancel the acceptor), drain in-flight requests (bounded by Config::graceful_timeout), run the ASGI lifespan shutdown, then close the listener. The acceptor honours the same ceilings the other serve paths get from run_forever: max_connections gates accepting, so a full server leaves the next client in the listen queue rather than accepting it and sitting on it, and allow_failure decides whether one bad connection is contained or brings the server down. limit_concurrency is the other kind of ceiling — a request arriving past it is answered 503 rather than queued. ## Multi-worker boundary uvicorn's --workers forks N OS processes that each bind the same port with SO_REUSEPORT for multi-core parallelism. moonbitlang/async exposes neither SO_REUSEPORT on TcpServer nor a fork primitive, and its event loop allows only one outstanding accept per listener handle (wait_read guards on a single waiter), so even N in-process acceptor tasks on one shared listener aren't expressible — a second concurrent accept on the same listener aborts. mooncat therefore serves from one acceptor that spawns a concurrent handler per connection, which is exactly the concurrency a single uvicorn worker provides on its single event loop. Multi-process fan-out is a transport limit, not a behavioural choice; it lands when the async layer exposes SO_REUSEPORT or a fork primitive. ## Signal boundary The only signal hook moonbitlang/async exposes is @signal.set_global_cancellation_signals, which cancels the whole task tree on SIGINT/SIGTERM. mooncat catches that cancellation and still runs lifespan shutdown and closes the listener under protect_from_cancel; but a signal also cancels the in-flight handler tasks, so drain-before-close is only fully honoured on the programmatic ShutdownHandle path. That matches uvicorn's own escalation: a first signal drains, a second forces exit.

item
async fn reload_watch(path : String, on_reload : async () -> Unit) -> Unit

Watch path for source changes and fire on_reload on each batch of events (← uvicorn's --reload file watcher). Backed by @fs.Watcher, which debounces and reports child-file events, so a save to any file under path triggers one reload. on_reload is where a supervisor re-execs the server; wiring it to a ShutdownHandle turns a file save into a graceful restart. Loops until its task is cancelled, always closing the watcher.

fn
fn http_date(ms : Int64) -> String

Format a Unix-epoch millisecond count as an HTTP-date in the IMF-fixdate form RFC 7231 §7.1.1.1 makes mandatory for a Date header: Sun, 06 Nov 1994 08:49:37 GMT, always GMT, always fixed-width. The calendar and the format are moondate's; what is here is the millisecond clock mooncat reads, brought down to the whole second the header is written to.

fn
fn http_date_now() -> String

The current instant as an HTTP-date, reformatted only when the epoch second has moved on.

fn
fn proxy_rewrite( headers : Map[String, String], client : (String, Int)?, scheme : String, trusted~ : Array[String], websocket? : Bool = false) -> ((String, Int)?, String)

Rewrite a request's client and scheme from a trusted proxy's forwarding headers (← uvicorn's ProxyHeadersMiddleware). Nothing is rewritten unless the peer itself is trusted: these headers are attacker-controlled, and believing them from an arbitrary client lets that client name any address it likes for a rate limiter, an allowlist or an audit log. Behind a proxy the peer is the proxy, which is why forwarded_allow_ips defaults to 127.0.0.1 and not to *. X-Forwarded-For yields the chain's leftmost entry with port 0, since a forwarded chain carries no port. X-Forwarded-Proto replaces the scheme, mapped to wss/ws for a WebSocket scope, whose scheme names differ from the https/http the header spells.

struct
struct Logger

How much the server says, and where it goes.

fn
fn Logger::new( level? : @moonlog.Level = Info, access? : Bool = true, write? : (String) -> Unit = line => println(line)) -> Logger

A logger printing to stdout at level, with the access log on — uvicorn's defaults. access off silences the per-request lines while keeping the server's own.

fn
fn Logger::silent() -> Logger

A logger that says nothing, for an embedder that does its own reporting or a test that would rather not have output.

fn
fn Logger::enabled(self : Logger, level : @moonlog.Level) -> Bool

Whether a line at level would be written.

fn
fn Logger::log( self : Logger, level : @moonlog.Level, message : String) -> Unit

Write one server-lifecycle line (uvicorn's uvicorn.error logger, which carries ordinary startup messages as well as failures).

fn
fn Logger::access_line( self : Logger, client : String, verb : String, target : String, http_version : String, status : Int) -> Unit

Write the one-line-per-request access log (uvicorn's uvicorn.access), in the same shape: peer, request line, status.

§WebSocket

The frame-to-Event bridge that drives a websocket Scope through the SEAM: connect / accept / receive / send / disconnect / close. The RFC 6455 frames and the opening handshake are moonhttp's ws and upgrade packages.

item
async fn read_frame( reader : &@io.Reader, client? : Bool = true, limit? : Int = @ws.limit) -> @ws.Frame

One frame off a stream (RFC 6455 §5.2). The header is read in the order the format is laid out — two bytes, then the extended length, then the mask — and @ws.check runs on the header before the payload is read, so an enormous announced length is refused rather than allocated. client is the server's position by default: a client's frames must be masked (§5.1), so reading a server's frames takes client=false. Raises @ws.Refused carrying the status the connection is then failed with, and passes the stream's end through.

item
async fn read_message( reader : &@io.Reader, writer : &@io.Writer, limit? : Int = @ws.limit) -> @ws.Message

One complete message off a stream, with the control frames between its fragments handled: a ping is answered on writer, a pong is dropped, and a close is echoed before the message comes back as that close. This is the server's receive path, so every frame must arrive masked. The reply frames go out unmasked because a server must not mask (§5.1).

§HTTP/2 (h2c)

Serving HTTP/2 over cleartext: the frame pump, stream multiplexing and the send windows, bound to the same dispatch path HTTP/1.1 uses. The frames are moonhttp/http2 and the header compression moonhttp/hpack.

item
async fn serve_h2c( app : @moonasgi.AsgiApp, host? : String = "127.0.0.1", port? : Int = port) -> Unit

Serve a moonasgi ASGI application over **HTTP/2 cleartext (h2c)** — the prior-knowledge, no-TLS HTTP/2 profile (RFC 7540 §3.4) — reusing moonrpc's self-built HTTP/2 + HPACK engine as the transport. Convenience wrapper over serve_h2c_config that builds a Config from host / port. Blocks in the accept loop until the running task is cancelled. h2c rather than h2-over-TLS because the moonbitlang/async TLS layer exposes no ALPN, so the protocol can't be negotiated on a TLS connection yet; h2c is the direct, ALPN-free path a client reaches with prior knowledge (as curl --http2-prior-knowledge or a gRPC client does).

item
async fn serve_h2c_config(app : @moonasgi.AsgiApp, config : Config) -> Unit

Serve a moonasgi ASGI application over h2c under an explicit Config. Mirrors serve_config / serve_tls_config: the ASGI lifespan protocol runs around the accept loop (startup before the listener binds, shutdown on the way out — even under cancellation, guarded by protect_from_cancel), and each accepted connection is driven by drive_h2c over the self-built HTTP/2 transport.

§TLS

What is left of TLS in a server once the protocol is moontls: holding the certificate and key, choosing the suite, and driving the handshake over an accepted socket. Every algorithm is mooncrypt's; this binds the choices TLS 1.3 and QUIC make so the protocol code names an operation rather than restating the suite on every call.

fn
fn tls13_x25519_public(private_key : Bytes) -> Bytes

The ephemeral x25519 public key for a private scalar (RFC 7748 §5).

fn
fn tls13_ecdhe_shared(private_key : Bytes, peer_public : Bytes) -> Bytes

The x25519 shared secret with a peer's public key. A low-order peer share makes the secret all zeroes, which mooncrypt refuses; here that becomes an empty answer, because the handshake's response to it is an alert rather than a crash.

fn
fn tls13_handshake_secret_from_ecdhe( private_key : Bytes, peer_public : Bytes) -> Bytes

The Handshake Secret from our scalar and the peer's key share: run the ECDHE, then RFC 8446 §7.1's ladder.

fn
fn tls13_server_hs_secret_from_client_hello( client_hello : Bytes, server_private : Bytes, transcript_hash : Bytes) -> Bytes?

A server's handshake traffic secret from the ClientHello it received: pull the client's x25519 key_share and run the ECDHE with server_private over the ClientHello..ServerHello transcript. None if the message is not a ClientHello or offers no x25519 share — the case a server answers with a HelloRetryRequest rather than a secret.

fn
fn tls13_certificate_verify_sign( key : @ecdsa.PrivateKey, context : String, transcript_hash : Bytes) -> Bytes

Sign a CertificateVerify over the transcript with an ES256 key (RFC 8446 §4.4.3): the message body, its scheme and signature framed by moontls, over the signed content moontls builds.

fn
fn tls13_certificate_verify_check( key : @ecdsa.PublicKey, context : String, transcript_hash : Bytes, body : Bytes) -> Bool

Check a CertificateVerify body against the peer's ES256 public key. False on a scheme this server does not verify, a malformed body, or a signature that does not check out — one answer for every way of being wrong, because which way it was wrong is not the peer's business.

fn
fn tls13_quic_encrypted_extensions( alpn : String, params : Array[@conn.Param]) -> Bytes

A QUIC server's EncryptedExtensions body: the negotiated ALPN protocol and the server's transport parameters. RFC 9001 §8.2 makes quic_transport_parameters mandatory for a QUIC server, and the parameters themselves are QUIC's — moontls carries the extension without reading it, so encoding them is this side's job.

fn
fn tls_hello_quic_transport_params( extensions : Array[@ext.Ext]) -> Array[@conn.Param]

The QUIC transport parameters an extension list carries, in order. Empty when the extension is absent or its block is truncated: a QUIC endpoint treats a missing block as no parameters and falls back to the defaults RFC 9000 §18.2 gives.

fn
fn tls13_ecdhe_handshake_traffic_secret( private_key : Bytes, peer_public : Bytes, transcript_hash : Bytes, is_client : Bool) -> Bytes

A handshake traffic secret straight from an ECDHE exchange and a transcript. is_client picks which side's secret: the two are derived from the same handshake secret under different labels (RFC 8446 §7.1), so one call serves both ends of a test and both ends of a connection.

fn
fn tls13_client_hello_x25519(client_hello : Bytes) -> Bytes?

The x25519 public key a ClientHello's key_share offers, or None when it carries none for that group.

fn
fn tls13_ecdhe_secrets_from_client_hello( client_hello : Bytes, server_private : Bytes, transcript_hash : Bytes) -> (Bytes, Bytes)?

Both handshake traffic secrets from a ClientHello's key share: the server's and the client's, in that order. None when the ClientHello offers no x25519 share, which is the one case a server has to answer with a HelloRetryRequest rather than a secret.

fn
fn tls_server_hello_key_share(sh : @msg.Server) -> (Int, Bytes)?

The group and key a ServerHello's key_share selected.

fn
fn tls_selected_alpn(extensions : Array[@ext.Ext]) -> String?

The single ALPN protocol a server's extension list selected, or None.

struct
struct TlsCert

TLS certificate material for HTTPS serving (← uvicorn's ssl_certfile / ssl_keyfile). The backend the moonbitlang/async TLS layer uses is platform-specific, so both forms are carried: * certificate_file + private_key_file — PEM files, used by the OpenSSL backend (Linux / macOS, the CI platforms); * pfx_file — a PKCS#12 bundle, used by the SChannel backend (Windows). serve_tls picks the right pair at compile time. Supply whichever your deployment targets; a self-signed certs/ pair for tests is generated with scripts/gen_test_cert.sh.

item
async fn serve_tls( app : @moonasgi.AsgiApp, certificate_file~ : String, private_key_file~ : String, pfx_file~ : String, host? : String = "127.0.0.1", port? : Int = port, backlog? : Int = 2048) -> Unit

Serve a moonasgi ASGI application over native HTTP/1.1 **over TLS** (HTTPS, ← uvicorn's --ssl-certfile/--ssl-keyfile). Convenience wrapper over serve_tls_config that builds the Config from host/port/backlog and the certificate paths. Runs the full ASGI lifespan protocol around the accept loop (startup before the listener binds, shutdown on exit — even under cancellation), then, for every accepted connection, completes a TLS handshake and drives the app through the encrypted HTTP/1.1 codec. WebSocket-over-TLS (wss://) is not bridged here: the async websocket upgrade requires an @http.ServerConnection, which is welded to @socket.Tcp and cannot wrap a @tls.Tls stream. Plaintext serve retains full WebSocket support; this is a transport-capability boundary, not a behavioural choice (README §TLS).

item
async fn serve_tls_config( app : @moonasgi.AsgiApp, config : Config, cert : TlsCert) -> Unit

Serve a moonasgi ASGI application over HTTPS under an explicit Config and TlsCert. Mirrors serve_config: lifespan startup is driven before the listener binds and shutdown on the way out (guarded by protect_from_cancel so it still runs when the serving task is cancelled), and each accepted connection is handled by handle_https_conn.

fn
fn ec_private(scalar : String) -> @ecdsa.PrivateKey

A P-256 private key from the hex of its scalar. Hex is what a fixture and an example carry. A key from a file is PEM or DER, which is mooncrypt/key's to read when that package lands; until then a deployment reads its own file and hands the scalar in.

§HTTP/3 over QUIC

The UDP half: the socket, and the event loop that demultiplexes datagrams onto connections. QUIC itself is moonquic, and HTTP/3 with QPACK is moonhttp's.

fn
fn quic_now() -> Int64

The event loop's clock, in the microseconds the recovery and idle timers are measured in. @async.now() counts milliseconds since the epoch.

item
async fn quic_serve_endpoint( ep : QuicUdpEndpoint, server : @conn.Endpoint[@socket.Addr], tick_ms? : Int = 20) -> Unit

Serve QUIC on ep until the enclosing task group is cancelled: one task reads datagrams and demultiplexes them onto connections, the other runs the idle, probe and closing-period timers every tick_ms milliseconds, and both push out whatever that left ready to send. A datagram that names no connection or fails to authenticate is dropped rather than killing the loop — an endpoint cannot tell a stray packet from an attack.

item
async fn quic_serve( addr : String, server : @conn.Endpoint[@socket.Addr], tick_ms? : Int = 20) -> Unit

Bind a UDP socket at addr (e.g. "0.0.0.0:443") and serve server on it. The socket outlives the loop deliberately: closing one a task is parked in recvfrom on wedges it rather than waking it, so shutdown is cancellation of the task group, not a close.

struct
struct QuicUdpEndpoint

A QUIC UDP endpoint: a bound datagram socket.

item
async fn QuicUdpEndpoint::bind(addr : String) -> QuicUdpEndpoint

Bind an endpoint to addr (e.g. "0.0.0.0:443", or "127.0.0.1:0" for an OS-assigned port). The bound address is available from local_addr.

fn
fn QuicUdpEndpoint::local_addr(self : QuicUdpEndpoint) -> @socket.Addr

The address the endpoint is bound to (the concrete port when bound to port 0).

item
async fn QuicUdpEndpoint::recv_datagram( self : QuicUdpEndpoint, max? : Int = 2048) -> (Bytes, @socket.Addr)

Receive the next datagram and its sender's address, copying up to max bytes.

item
async fn QuicUdpEndpoint::send_datagram( self : QuicUdpEndpoint, data : Bytes, to : @socket.Addr) -> Unit

Send data as a single datagram to to.

fn
fn QuicUdpEndpoint::close(self : QuicUdpEndpoint) -> Unit

Close the endpoint's socket.