moonrpc

A real gRPC implementation for MoonBit — not gRPC-Web. It now serves a real unary gRPC call over a self-built HTTP/2 (h2c) transport: the RFC 7540 frame layer, the stream state machine, complete HPACK (RFC 7541), and connection- and stream-level flow control — the protocol engine is pure and runs on every backend; the socket driver is native.

CItestsGitHublicense
$moon add moonbitstack/moonrpc

✶ The contract at a glance

let server = @net.GrpcServer::new()
server.register("/greet.Greeter/SayHello", req => handle(req))   // (bytes) -> bytes
server.serve(port=50051)          // a real unary gRPC call over self-built h2c

// under the hood — a pure, all-backend protocol engine over the frame + HPACK codecs:
let engine = H2Server::new()
let out = engine.feed(frame)      // frames in -> HEADERS + DATA + grpc-status trailers out

§Framing & status

encode_message / decode_message implement gRPC length-prefixed framing; Status is the 17-code grpc-status model; Method renders the /Service/Method path.

let
let max_message_size : Int = 4 * 1024 * 1024

The default cap on a single received gRPC message (4 MiB, matching gRPC's default MaxRecvMsgSize). A length prefix above this — including one whose 4 bytes decode to a negative Int because the high bit is set — is rejected rather than trusted, so a hostile prefix can neither slice out of bounds nor pin unbounded buffer.

let
let max_header_list_size : Int = 128 * 1024

The cap on one accumulated header block (HEADERS plus its CONTINUATION frames). Without it a peer could stream endless non-final CONTINUATION frames and grow the buffer without bound (a CONTINUATION flood); 128 KiB is far above any real gRPC request's headers.

fn
fn encode_message(payload : Bytes, compressed? : Bool = false) -> Bytes

Encode a payload as a gRPC *Length-Prefixed-Message*: a 1-byte compression flag, a 4-byte big-endian length, then the payload. This is the framing every gRPC transport shares (gRPC-Web over HTTP/1.1 and real gRPC over HTTP/2 alike).

fn
fn decode_message(data : Bytes) -> (Bool, Bytes)?

Decode one gRPC length-prefixed message from the front of data, returning (compressed, payload), or None if fewer than a full frame is present.

fn
fn message_len(data : BytesView, at? : Int = 0) -> Int?

The payload length of the length-prefixed message at at, or None when fewer than the five octets of a prefix are there. A prefix with its high bit set reads back negative, which is how a hostile length is told from a real one before anything is sliced on it. Every reader of this framing goes through here — there are five of them across the client, the server and the socket transport, and they have to agree.

fn
fn split_messages(data : Bytes) -> Array[Bytes]

Split a run of concatenated length-prefixed messages into their payloads, stopping at the first one that has not fully arrived.

enum
enum Status

The 17 canonical gRPC status codes (grpc-status).

fn
fn Status::code(self : Status) -> Int

The numeric grpc-status code.

fn
fn Status::name(self : Status) -> String

The canonical uppercase status name.

fn
fn Status::from_h2_error(code : Int) -> Status

The status a call ends with when the peer resets its stream instead of sending trailers, from the RST_STREAM error code (gRPC PROTOCOL-HTTP2, "HTTP2 Error Code → Status"). A refused stream was never processed, so it is UNAVAILABLE and safe to retry; anything the table does not name is INTERNAL, since the peer broke the transport rather than the call.

fn
fn Status::from_http(code : Int) -> Status

The status a non-200 HTTP response maps to when it carries no grpc-status — a proxy or a plain HTTP server answering on the gRPC port (gRPC http-grpc-status-mapping). Anything outside the table is UNKNOWN.

struct
struct Method

A fully-qualified RPC method: package.Service and the method name.

fn
fn Method::path(self : Method) -> String

The gRPC HTTP/2 :path, i.e. /package.Service/Method.

item
extend Status with Eq::

§Protobuf wire runtime

The pure protobuf binary wire codec: PbWriter / PbReader carry the four proto3 wire types (varint, fixed32/64, length-delimited) plus zigzag for the sint types, with tag packing, unknown-field skipping, and truncation / overflow / group-type rejection on decode.

enum
enum WireType

The four protobuf wire types carried by a field tag's low three bits. The two group types (3 start-group, 4 end-group) are deprecated and unsupported, so from_code rejects them.

item
impl Show for WireType with fn output(self, logger)

Prints the wire type by its protobuf name rather than its number.

fn
fn WireType::code(self : WireType) -> Int

The wire-type number (the tag's low three bits).

fn
fn WireType::from_code(n : Int) -> WireType?

The wire type for a tag's low three bits, or None for the deprecated group types (3/4) and any out-of-range value.

item
suberror PbError

A raised protobuf decode failure: Truncated when the buffer ends inside a field, BadWireType for a group or unknown wire type, Overflow for a varint longer than ten octets, and BadUtf8 for an invalid string field.

item
impl Show for PbError with fn output(self, logger)

Prints the field and offset the decode failed on.

struct
struct PbWriter

An append-only protobuf message encoder. Field writers append a tag and the field body; to_bytes yields the finished message. Fields are written in the caller's order — protobuf places no ordering requirement on distinct fields, and a message struct's encoder writes them by ascending number by convention.

fn
fn PbWriter::new() -> PbWriter

A fresh, empty message encoder.

fn
fn PbWriter::to_bytes(self : PbWriter) -> Bytes

The bytes written so far.

fn
fn PbWriter::write_varint(self : PbWriter, value : UInt64) -> Unit

Append a base-128 varint (protobuf "Base 128 Varints"): seven bits per octet, little-endian groups, the high bit marking continuation.

fn
fn PbWriter::write_tag( self : PbWriter, field : Int, wire : WireType) -> Unit

Append a field tag: (field_number << 3) | wire_type, itself a varint.

fn
fn PbWriter::write_fixed32(self : PbWriter, v : UInt) -> Unit

Append a little-endian 32-bit fixed value (wire type 5, no tag).

fn
fn PbWriter::write_fixed64(self : PbWriter, v : UInt64) -> Unit

Append a little-endian 64-bit fixed value (wire type 1, no tag).

fn
fn PbWriter::write_len_delim(self : PbWriter, body : Bytes) -> Unit

Append a length-delimited body: a varint length then the raw bytes (wire type 2, no tag).

fn
fn PbWriter::int32(self : PbWriter, field : Int, v : Int) -> Unit

Write an int32 field. Negative values sign-extend to a full ten-octet varint, exactly as the reference implementation encodes them.

fn
fn PbWriter::int64(self : PbWriter, field : Int, v : Int64) -> Unit

Write an int64 field.

fn
fn PbWriter::uint32(self : PbWriter, field : Int, v : UInt) -> Unit

Write a uint32 field.

fn
fn PbWriter::uint64(self : PbWriter, field : Int, v : UInt64) -> Unit

Write a uint64 field.

fn
fn PbWriter::sint32(self : PbWriter, field : Int, v : Int) -> Unit

Write a sint32 field (zigzag-encoded so small-magnitude negatives stay short).

fn
fn PbWriter::sint64(self : PbWriter, field : Int, v : Int64) -> Unit

Write a sint64 field (zigzag-encoded).

fn
fn PbWriter::bool_(self : PbWriter, field : Int, v : Bool) -> Unit

Write a bool field.

fn
fn PbWriter::enum_(self : PbWriter, field : Int, v : Int) -> Unit

Write an enum field (its integer value, as a varint).

fn
fn PbWriter::fixed32(self : PbWriter, field : Int, v : UInt) -> Unit

Write a fixed32/sfixed32/float field.

fn
fn PbWriter::fixed64(self : PbWriter, field : Int, v : UInt64) -> Unit

Write a fixed64/sfixed64/double field.

fn
fn PbWriter::bytes_(self : PbWriter, field : Int, v : Bytes) -> Unit

Write a bytes field.

fn
fn PbWriter::string_(self : PbWriter, field : Int, v : String) -> Unit

Write a string field (UTF-8 encoded).

fn
fn PbWriter::message_(self : PbWriter, field : Int, v : Bytes) -> Unit

Write an embedded-message field: the pre-encoded sub-message as a length-delimited body.

struct
struct PbReader

A forward cursor over an encoded protobuf message. read_tag pulls the next field's number and wire type; the typed readers then consume its body. skip discards an unknown field's body so a decoder tolerates fields it does not know (protobuf forward compatibility).

fn
fn PbReader::new(data : Bytes) -> PbReader

A reader positioned at the start of data.

fn
fn PbReader::eof(self : PbReader) -> Bool

Whether the whole message has been consumed.

fn
fn PbReader::read_varint(self : PbReader) -> UInt64 raise PbError

Read a base-128 varint. Raises Overflow past ten octets and Truncated if the buffer ends mid-varint.

fn
fn PbReader::read_tag(self : PbReader) -> (Int, WireType) raise PbError

Read a field tag, returning (field_number, wire_type). Raises BadWireType for a group or unknown wire type.

fn
fn PbReader::read_fixed32(self : PbReader) -> UInt raise PbError

Read a little-endian 32-bit fixed value.

fn
fn PbReader::read_fixed64(self : PbReader) -> UInt64 raise PbError

Read a little-endian 64-bit fixed value.

fn
fn PbReader::read_len_delim(self : PbReader) -> Bytes raise PbError

Read a length-delimited body's raw bytes.

fn
fn PbReader::read_int32(self : PbReader) -> Int raise PbError

Read an int32 field body (the low 32 bits of the varint).

fn
fn PbReader::read_int64(self : PbReader) -> Int64 raise PbError

Read an int64 field body.

fn
fn PbReader::read_uint32(self : PbReader) -> UInt raise PbError

Read a uint32 field body.

fn
fn PbReader::read_uint64(self : PbReader) -> UInt64 raise PbError

Read a uint64 field body.

fn
fn PbReader::read_sint32(self : PbReader) -> Int raise PbError

Read a sint32 field body (zigzag-decoded).

fn
fn PbReader::read_sint64(self : PbReader) -> Int64 raise PbError

Read a sint64 field body (zigzag-decoded).

fn
fn PbReader::read_bool(self : PbReader) -> Bool raise PbError

Read a bool field body.

fn
fn PbReader::read_bytes(self : PbReader) -> Bytes raise PbError

Read a bytes field body.

fn
fn PbReader::read_string(self : PbReader) -> String raise PbError

Read a string field body, decoding UTF-8. Raises BadUtf8 on invalid bytes.

fn
fn PbReader::skip(self : PbReader, wire : WireType) -> Unit raise PbError

Discard the body of a field whose number the decoder does not recognise, given its wire type — the mechanism behind protobuf's forward compatibility.

fn
fn PbWriter::map_string_string( self : PbWriter, field : Int, key : String, value : String) -> Unit

Write one map<string, string> entry for field: a length-delimited submessage { key = 1, value = 2 }, emitted once per pair (a proto map is repeated entries).

fn
fn PbWriter::packed_int32( self : PbWriter, field : Int, values : Array[Int]) -> Unit

Write a packed repeated int32 field: all values as back-to-back varints inside one length-delimited field (proto3's default for scalar repeated).

fn
fn PbReader::read_map_string_string( self : PbReader) -> (String, String) raise PbError

Read one map<string, string> entry (a length-delimited { key = 1, value = 2 } submessage) as (key, value). Call once the tag for the map field has been read.

fn
fn PbReader::read_packed_int32(self : PbReader) -> Array[Int] raise PbError

Read a packed repeated int32 field: the length-delimited body decoded as back-to-back int32 varints.

item
extend PbError with Eq::
item
extend PbError with Show::
item
extend WireType with Eq::
item
extend WireType with Show::

§Descriptor model

The descriptor model for services and messages and its codec to the FileDescriptorProto / FileDescriptorSet wire bytes of descriptor.proto — the unit Server Reflection returns, encoding from a programmatic model and decoding a protoc-produced FileDescriptorSet through the same types.

enum
enum FieldType

The FieldDescriptorProto.Type enum (proto3's scalar and composite field types). TypeGroup is intentionally absent — groups are removed from proto3.

fn
fn FieldType::code(self : FieldType) -> Int

The descriptor.proto enum number of a field type.

fn
fn FieldType::from_code(n : Int) -> FieldType

The field type for a descriptor.proto enum number; an unrecognised number (including the removed group type 10) reads as TypeMessage.

enum
enum FieldLabel

The FieldDescriptorProto.Label: proto3 fields are LabelOptional unless repeated.

fn
fn FieldLabel::code(self : FieldLabel) -> Int

The descriptor.proto enum number of a label.

fn
fn FieldLabel::from_code(n : Int) -> FieldLabel

The label for a descriptor.proto enum number; anything else reads as LabelOptional.

struct
struct FieldDescriptor

One field of a message (FieldDescriptorProto). type_name is the fully-qualified name of the referenced message or enum for TypeMessage / TypeEnum, and empty for scalars.

fn
fn FieldDescriptor::scalar( name : String, number : Int, type_ : FieldType, label? : FieldLabel = LabelOptional) -> FieldDescriptor

A field with a scalar type and no composite type_name.

fn
fn FieldDescriptor::encode(self : FieldDescriptor) -> Bytes

Encode a FieldDescriptorProto.

fn
fn FieldDescriptor::decode(body : Bytes) -> FieldDescriptor raise PbError

Decode a FieldDescriptorProto.

struct
struct MessageDescriptor

A message type (DescriptorProto): its (simple) name and its fields.

fn
fn MessageDescriptor::encode(self : MessageDescriptor) -> Bytes

Encode a DescriptorProto.

fn
fn MessageDescriptor::decode( body : Bytes) -> MessageDescriptor raise PbError

Decode a DescriptorProto.

struct
struct MethodDescriptor

One RPC method (MethodDescriptorProto): its name, the fully-qualified request and response message names, and the two streaming flags that together pick the call cardinality.

fn
fn MethodDescriptor::encode(self : MethodDescriptor) -> Bytes

Encode a MethodDescriptorProto.

fn
fn MethodDescriptor::decode(body : Bytes) -> MethodDescriptor raise PbError

Decode a MethodDescriptorProto.

struct
struct ServiceDescriptor

A service (ServiceDescriptorProto): its (simple) name and its methods.

fn
fn ServiceDescriptor::encode(self : ServiceDescriptor) -> Bytes

Encode a ServiceDescriptorProto.

fn
fn ServiceDescriptor::decode( body : Bytes) -> ServiceDescriptor raise PbError

Decode a ServiceDescriptorProto.

struct
struct FileDescriptor

A single .proto file (FileDescriptorProto): its filename, package, the message and service types it defines, and the syntax level. This is the unit Server Reflection returns.

fn
fn FileDescriptor::new( name : String, package_ : String, messages? : Array[MessageDescriptor] = [], services? : Array[ServiceDescriptor] = []) -> FileDescriptor

A proto3 file with the given filename and package.

fn
fn FileDescriptor::encode(self : FileDescriptor) -> Bytes

Encode a FileDescriptorProto.

fn
fn FileDescriptor::decode(body : Bytes) -> FileDescriptor raise PbError

Decode a FileDescriptorProto.

fn
fn FileDescriptor::symbols(self : FileDescriptor) -> Array[String]

The fully-qualified names this file defines: package.Service for each service and package.Message for each message. These are the symbols a FileContainingSymbol reflection request can resolve to this file.

fn
fn FileDescriptor::service_names(self : FileDescriptor) -> Array[String]

The fully-qualified names of the services this file defines (package.Service).

fn
fn encode_file_descriptor_set(files : Array[FileDescriptor]) -> Bytes

Encode a FileDescriptorSet (protoc --descriptor_set_out): the concatenation of FileDescriptorProtos under repeated field 1.

fn
fn decode_file_descriptor_set( body : Bytes) -> Array[FileDescriptor] raise PbError

Decode a FileDescriptorSet into its files.

item
extend FieldDescriptor with Eq::
item
extend FieldLabel with Eq::
item
extend FieldType with Eq::
item
extend FileDescriptor with Eq::
item
extend MessageDescriptor with Eq::
item
extend MethodDescriptor with Eq::
item
extend ServiceDescriptor with Eq::

§HTTP/2 stream state machine

The RFC 7540 §5.1 stream lifecycle (idle / open / half-closed / closed) and the §5.1.1 stream-identifier parity rules.

enum
enum StreamState

The lifecycle state of a single HTTP/2 stream (RFC 7540 §5.1).

item
impl Show for StreamState with fn output(self, logger)

Prints the RFC 7540 §5.1 state name.

enum
enum StreamEvent

A state-changing stream event: the arrival or departure of the frames that drive §5.1 transitions. Headers/Data carry the END_STREAM flag; Reserve is a PUSH_PROMISE reserving this (promised) stream. Frames that never change stream state (PRIORITY, WINDOW_UPDATE, SETTINGS, PING) are intentionally absent.

item
suberror StreamError

An illegal stream transition (RFC 7540 §5.1): a frame not permitted in the current state (typically a STREAM_CLOSED or PROTOCOL_ERROR condition).

item
impl Show for StreamError with fn output(self, logger)

Prints which transition was refused.

fn
fn StreamState::on_send( self : StreamState, ev : StreamEvent) -> StreamState raise StreamError

The next state after *sending* ev from this state (RFC 7540 §5.1, local side). Raises InvalidTransition for a frame illegal in the current state.

fn
fn StreamState::on_recv( self : StreamState, ev : StreamEvent) -> StreamState raise StreamError

The next state after *receiving* ev in this state (RFC 7540 §5.1, remote side — the mirror of on_send). Raises InvalidTransition on an illegal frame.

struct
struct Stream

A mutable stream: its identifier and current lifecycle state. send/recv advance the state in place, raising on an illegal transition.

fn
fn Stream::new(id : Int) -> Stream

A fresh idle stream with the given identifier.

fn
fn Stream::send( self : Stream, ev : StreamEvent) -> StreamState raise StreamError

Advance this stream by sending ev, returning the new state.

fn
fn Stream::recv( self : Stream, ev : StreamEvent) -> StreamState raise StreamError

Advance this stream by receiving ev, returning the new state.

fn
fn stream_is_client_initiated(id : Int) -> Bool

Whether id is a client-initiated stream: a non-zero odd identifier.

fn
fn stream_is_server_initiated(id : Int) -> Bool

Whether id is a server-initiated (pushed) stream: a non-zero even identifier.

fn
fn stream_id_valid_for_initiator(id : Int, by_client~ : Bool) -> Bool

Whether a peer that is a client (by_client = true) or server may legally *open* stream id: clients use odd ids, servers use even ids, and 0 is the connection control stream, openable by neither (RFC 7540 §5.1.1).

item
extend StreamError with Eq::
item
extend StreamError with Show::
item
extend StreamEvent with Eq::
item
extend StreamState with Debug::
item
extend StreamState with Eq::
item
extend StreamState with Show::

§gRPC server engine

The pure, transport-independent server core: H2Server::feed turns a stream of decoded frames into the frames to send back — driving the stream state machine, the stateful HPACK codec, and connection- and stream-level flow control (RFC 7540 §6.9), routing a completed application/grpc request to a handler of any of the four call kinds and framing each produced message as its own length-prefixed DATA, closed by grpc-status trailers. Exercised in-memory on every backend.

let
let default_window_size : Int = 65535

The HTTP/2 default flow-control window and initial SETTINGS_INITIAL_WINDOW_SIZE (RFC 7540 §6.9.2): 65 535 octets.

let
let default_max_frame_size : Int = 16384

The HTTP/2 default (and minimum) SETTINGS_MAX_FRAME_SIZE (RFC 7540 §6.5.2).

let
let default_max_streams : Int = 100

The SETTINGS_MAX_CONCURRENT_STREAMS the server advertises and enforces unless a caller says otherwise: 100, the value gRPC and nghttp2 both settle on. RFC 9113 §5.1.2 leaves the setting unset by default, which means unbounded — and every live stream holds a request buffer, so a peer that opens streams and never finishes them pins memory for as long as the connection lasts.

struct
struct SrvStream

One server-side stream: its lifecycle state, the accumulating request header block and DATA (with a cursor over the length-prefixed messages already pulled out of it), the per-stream flow-control windows, and the response side — the bytes still to send, whether the initial HEADERS and the trailers have gone out, and any live bidi call state.

struct
struct H2Server

The server side of one HTTP/2 connection: the HPACK codec pair, the live streams, the connection-level flow-control windows, and the peer's settings that bound what we may send. Persistent across the whole connection because HPACK and flow control are stateful.

fn
fn H2Server::new() -> H2Server

A fresh server engine with no registered handlers. Flow-control windows start at the HTTP/2 defaults until the peer's SETTINGS adjust them.

fn
fn H2Server::register( self : H2Server, path : String, handler : (Bytes) -> Bytes) -> Unit

Register a unary handler for a fully-qualified gRPC path (/pkg.Service/Method): one request message in, one reply message out. An unmatched path gets a trailers-only grpc-status: 12 (UNIMPLEMENTED) response.

fn
fn H2Server::release(self : H2Server, id : Int) -> Unit

Drop everything the engine still holds for a finished stream. The map is only ever inserted into otherwise, so a long-lived connection would keep one full request and response per RPC it has ever served. An unknown id is a no-op.

fn
fn H2Server::register_handler( self : H2Server, path : String, handler : Handler) -> Unit

Register a handler of any of the four gRPC call kinds.

fn
fn H2Server::register_unary( self : H2Server, path : String, handler : (RpcContext, Bytes) -> Bytes) -> Unit

Register a unary handler that also sees the call context (metadata, deadline, and the response metadata slots).

fn
fn H2Server::register_server_streaming( self : H2Server, path : String, handler : (RpcContext, Bytes) -> Array[Bytes]) -> Unit

Register a server-streaming handler: one request message, an ordered sequence of reply messages, each framed as its own gRPC message.

fn
fn H2Server::register_client_streaming( self : H2Server, path : String, handler : (RpcContext, Array[Bytes]) -> Bytes) -> Unit

Register a client-streaming handler: every request message the client sends is collected, and after the client half-closes the handler returns one reply.

fn
fn H2Server::register_bidi( self : H2Server, path : String, factory : (RpcContext) -> BidiHandler) -> Unit

Register a bidirectional-streaming handler. The factory runs once per call and returns a BidiHandler whose on_message fires per request message (its replies stream out immediately) and whose on_end fires at half-close.

fn
fn H2Server::goaway_received(self : H2Server) -> Bool

Whether the peer has sent GOAWAY; the driver stops accepting new streams once this is set.

fn
fn H2Server::preface(self : H2Server) -> Array[@http2.Frame]

The server's opening frames (RFC 7540 §3.5): a SETTINGS frame disabling server push and naming how many streams this connection will run at once. Sent immediately after the client connection @http2.preface is validated, before any request frame is read.

fn
fn H2Server::set_max_streams(self : H2Server, n : Int) -> Unit

Set how many streams this connection may run at once. The value is both advertised as SETTINGS_MAX_CONCURRENT_STREAMS and enforced on arrival, so set it before @http2.preface and the peer is told exactly what it will be held to.

fn
fn H2Server::set_clock(self : H2Server, clock : () -> Int64) -> Unit

Install the clock the engine measures grpc-timeout deadlines against: a function returning the current time in milliseconds on any monotonic scale. The engine is a pure state machine with no clock of its own, so until one is installed every deadline is inert; the native driver installs @async.now.

fn
fn H2Server::goaway( self : H2Server, last_stream_id : Int) -> Array[@http2.Frame]

Frames for a graceful shutdown: a GOAWAY announcing last_stream_id — the highest client stream the server will still process (RFC 7540 §6.8) — with NO_ERROR, so the peer opens no new streams while in-flight ones finish. The driver writes these before closing the connection. A stream opened above last_stream_id afterwards is declined with RST_STREAM(REFUSED_STREAM), which tells the client it was never processed and can be re-issued elsewhere.

fn
fn H2Server::closing(self : H2Server) -> Bool

Whether a connection error has been reported. The GOAWAY carrying it came back from the feed that found it, so the driver writes that and then closes; the engine processes nothing more (RFC 9113 §5.4.1).

fn
fn H2Server::feed( self : H2Server, frame : @http2.Frame) -> Array[@http2.Frame]

Feed one decoded incoming frame to the engine, advancing all state and returning the frames to write back (SETTINGS ack, PING pong, WINDOW_UPDATE, and — as the request stream progresses — the framed gRPC response messages and trailers). A protocol fault comes back as the frames that report it rather than as a raise the driver can only discard: RST_STREAM for a stream error, and for a connection error a GOAWAY, after which closing is set and every later frame is ignored (RFC 9113 §5.4).

fn
fn H2Server::tick(self : H2Server) -> Array[@http2.Frame]

Expire every call whose deadline has passed, returning the frames that end them. The driver calls this on whatever schedule it keeps time on; the engine also checks the deadline whenever a stream advances, so a call that is making progress never needs a tick to be cut off.

fn
fn H2Server::highest_stream(self : H2Server) -> Int

The highest client stream id this connection has opened, which is what a GOAWAY has to name so the peer knows which of its streams were accepted.

fn
fn H2Server::active_streams(self : H2Server) -> Int

How many streams are still running: the ones RFC 9113 §5.1.2 counts against SETTINGS_MAX_CONCURRENT_STREAMS, which is open and both half-closed states. A graceful shutdown waits for this to reach zero before closing the connection. Finished streams stay in the map with their state but are not counted, and neither is one the engine only knows of because a WINDOW_UPDATE named it — that stream was never opened, so it holds nothing and occupies no slot.

fn
fn H2Server::stream_state(self : H2Server, id : Int) -> StreamState

The lifecycle state of stream id, or Idle if the engine has never seen it.

§Streaming call kinds & context

The four gRPC cardinalities (Unary / ServerStreaming / ClientStreaming / Bidi) and the per-call RpcContext: request metadata, the grpc-timeout deadline, and response initial/trailing metadata the handler can set.

struct
struct RpcContext

The context surfaced to a handler for one RPC call: the invoked :path, the request metadata (custom HEADERS, minus the pseudo- and reserved gRPC headers), the deadline parsed from grpc-timeout (in milliseconds, None when absent), and mutable slots for response initial metadata and trailing metadata the handler can set before it returns.

fn
fn RpcContext::empty() -> RpcContext

A context with no path, metadata, or deadline — the placeholder a stream holds until its request HEADERS are decoded.

fn
fn RpcContext::fail( self : RpcContext, code : Int, message : String) -> Unit

End the call with a non-OK gRPC status. A handler calls this (e.g. ctx.fail(Status::code(NotFound), "user 42 does not exist")) instead of returning a normal reply; the server sends the grpc-status / grpc-message trailer and drops the reply message. The five common codes have named helpers on [Status]; any code is accepted.

fn
fn RpcContext::add_error_detail(self : RpcContext, detail : Bytes) -> Unit

Attach a rich-error detail (a serialized google.protobuf.Any) to a failing call. The server packs the status code, message, and every detail into a google.rpc.Status and sends it base64-encoded in the grpc-status-details-bin trailer — gRPC's rich-error channel. Combine with fail, which sets the base grpc-status / grpc-message.

fn
fn RpcContext::metadata_get(self : RpcContext, name : Bytes) -> Bytes?

The value of request metadata name, or None. Names are matched byte-for-byte (gRPC lowercases header names on the wire), so binary -bin metadata works too.

fn
fn RpcContext::add_header( self : RpcContext, name : Bytes, value : Bytes) -> Unit

Add an initial-metadata header to the response. Only takes effect if called before the response HEADERS are flushed (any point inside a unary / server- / client-streaming handler, or inside a bidi factory before the first message).

fn
fn RpcContext::add_trailer( self : RpcContext, name : Bytes, value : Bytes) -> Unit

Add a trailing-metadata header, sent in the trailer HEADERS alongside grpc-status.

struct
struct BidiHandler

A live bidirectional call. on_message is invoked once per fully-received request message and returns the reply messages to send right then; on_end runs after the client half-closes and returns the final replies. Both feed the same flow-controlled response stream, so responses interleave with requests.

enum
enum Handler

A registered method, in one of gRPC's four cardinalities. The reply shape mirrors the request shape: streaming handlers produce an ordered Array[Bytes] of messages, each framed as its own length-prefixed gRPC message on the wire.

fn
fn parse_grpc_timeout(v : Bytes) -> Int?

Parse a grpc-timeout value (RFC: up to 8 ASCII digits then a unit — H/M/S/m/u/n) to whole milliseconds, flooring sub-millisecond units. None for a malformed value.

§gRPC client engine

The pure client core, symmetric to H2Server: H2Client allocates client stream ids, builds request HEADERS and length-prefixed DATA honouring the send windows, and turns the response frames back into a CallReply — the :status, grpc-status, reply messages, and initial/trailing metadata. Runs in-memory on every backend.

struct
struct ClientCall

One client-side call: its stream id, the request side (the length-prefixed request body still to send, the send window, and whether the request has been half-closed), and the response side (the accumulating DATA with a cursor over the messages already pulled out, the recv window, the captured :status and grpc-status, and the response initial and trailing metadata).

struct
struct CallReply

The completed result of a call: the HTTP :status, the numeric grpc-status (-1 if the peer never sent one), the response initial metadata, the reply messages in order, and the trailing metadata.

struct
struct H2Client

The client side of one HTTP/2 connection: the HPACK codec pair (stateful across every call on the connection), the live calls keyed by stream id, the next odd stream id to allocate (§5.1.1), and the connection-level flow-control state bounded by the peer's SETTINGS.

fn
fn H2Client::new() -> H2Client

A fresh client engine with no live calls. The first call takes stream id 1.

fn
fn H2Client::preface(self : H2Client) -> Array[@http2.Frame]

The client's opening frames (RFC 7540 §3.5): a SETTINGS frame disabling server push. Written right after the 24-octet connection @http2.preface bytes, before any request.

fn
fn H2Client::open( self : H2Client, path : String, metadata? : Array[@header.Header] = [], authority? : String = "127.0.0.1", timeout_millis? : Int) -> Int

Open a new call on this connection for path (/pkg.Service/Method), returning its freshly allocated stream id. metadata is sent as custom request HEADERS; timeout_millis, when set, becomes the grpc-timeout header. The request body is added with send and half-closed with close_send.

fn
fn H2Client::send( self : H2Client, id : Int, message : Bytes, end? : Bool = false) -> Array[@http2.Frame]

Append one request message to a call and return the frames to write now (the request HEADERS the first time, then as much length-prefixed DATA as the send windows allow). Set end on the last message to half-close the request.

fn
fn H2Client::close_send(self : H2Client, id : Int) -> Array[@http2.Frame]

Half-close the request side of a call (no more request messages) and return any frames that completes — the trailing END_STREAM.

fn
fn H2Client::unary( self : H2Client, path : String, request : Bytes, metadata? : Array[@header.Header] = [], authority? : String = "127.0.0.1", timeout_millis? : Int) -> (Int, Array[@http2.Frame])

Open a unary call and return (stream_id, frames_to_write) in one step: the request HEADERS and the single length-prefixed request message with END_STREAM.

fn
fn H2Client::feed( self : H2Client, frame : @http2.Frame) -> Array[@http2.Frame] raise

Feed one decoded response frame to the engine, advancing all state and returning the frames to write back (SETTINGS ack, PING pong, WINDOW_UPDATE replenishing a receive window, and — once a WINDOW_UPDATE lifts back-pressure — any remaining request DATA). Captures :status, grpc-status, response metadata, and the reassembled reply messages.

fn
fn H2Client::release(self : H2Client, id : Int) -> Unit

Drop everything the engine still holds for a finished call: its buffered body, messages, header block and trailers. The map is only ever inserted into otherwise, so a long-lived connection would keep one full request and response per RPC it has ever made, and every WINDOW_UPDATE and SETTINGS frame would walk that whole history. An unknown or already-released id is a no-op.

fn
fn H2Client::is_done(self : H2Client, id : Int) -> Bool

Whether a call has fully completed (its response ended). An unknown id counts as done so a driver loop terminates.

fn
fn H2Client::goaway_received(self : H2Client) -> Bool

Whether the peer has sent GOAWAY; a draining connection opens no new stream, so the driver picks a fresh one for further calls. Mirrors H2Server::goaway_received.

fn
fn H2Client::goaway_last_stream_id(self : H2Client) -> Int

The peer's last processed stream id from its GOAWAY (0 if none was seen); a call whose stream id is above it was never handled by the server.

fn
fn H2Client::is_retryable(self : H2Client, id : Int) -> Bool

Whether call id was left retryable by a GOAWAY: its stream id is above the peer's last processed id, so the server never saw it and re-issuing the RPC on a fresh connection is safe (gRPC transport GOAWAY / RFC 7540 §6.8). False for a call at or below that id — it may have been processed — and when no GOAWAY arrived.

fn
fn H2Client::reply(self : H2Client, id : Int) -> CallReply

The completed result of a call. Meaningful once is_done is true.

fn
fn H2Client::has_messages(self : H2Client, id : Int) -> Bool

Whether a call has reply messages buffered but not yet taken — for reading a streaming response incrementally as it arrives, rather than all at once via reply.

fn
fn H2Client::take_messages(self : H2Client, id : Int) -> Array[Bytes]

Take every reply message received so far, clearing the call's buffer — the incremental counterpart to reply, for a server- or bidi-streaming response read message by message.

fn
fn encode_grpc_timeout(millis : Int) -> Bytes

Encode whole millis as a grpc-timeout header value (RFC gRPC HTTP/2 mapping): the m (millisecond) unit when the count fits the 8-digit field, else seconds with the S unit.

fn
fn percent_decode(v : Bytes) -> String

Percent-decode a received grpc-message value — the inverse of the server's percent_encode (gRPC spec §"Responses"): each %XX escape becomes the byte its two hex digits name, every other byte passes through, and a malformed escape is left literal; the result is read as UTF-8.

§Server interceptors

Unary and server-streaming interceptor chains wrapped around a method handler, folded outermost-first: each interceptor sees the context and request, calls next to proceed, or returns without it to short-circuit.

type
type UnaryInterceptor = (RpcContext, Bytes, (RpcContext, Bytes) -> Bytes) -> Bytes

A unary server interceptor: (ctx, request, next) -> reply, where next is the remainder of the chain. Call next(ctx, request) to proceed, or return without calling it to short-circuit.

type
type StreamInterceptor = (

A server-streaming interceptor: (ctx, request, next) -> replies. It can pre-process the request, post-process the reply sequence, or short-circuit.

fn
fn H2Server::add_unary_interceptor( self : H2Server, interceptor : UnaryInterceptor) -> Unit

Add a unary interceptor to the server's chain. Applies to every unary method; interceptors run in registration order, outermost first.

fn
fn H2Server::add_stream_interceptor( self : H2Server, interceptor : StreamInterceptor) -> Unit

Add a server-streaming interceptor to the server's chain.

§Health service

The grpc.health.v1.Health service (Check + Watch) with a hand-coded protobuf codec for its two messages and a per-service ServingStatus table.

enum
enum ServingStatus

The grpc.health.v1.HealthCheckResponse.ServingStatus enum.

fn
fn ServingStatus::code(self : ServingStatus) -> Int

The wire value of a serving status (the protobuf enum number).

fn
fn ServingStatus::from_code(n : Int) -> ServingStatus

The serving status for a protobuf enum number; out-of-range numbers read as StatusUnknown.

fn
fn encode_health_request(service : Bytes) -> Bytes

Encode a HealthCheckRequest: field 1 (service, a length-delimited string). An empty service name encodes to the empty message, the wire form of the overall-server check.

fn
fn decode_health_request(msg : Bytes) -> Bytes

Decode the service field of a HealthCheckRequest, or empty bytes when the field is absent (the overall-server check). Unknown fields are skipped.

fn
fn encode_health_response(status : ServingStatus) -> Bytes

Encode a HealthCheckResponse: field 1 (status, a varint enum). The SERVING default 0 still encodes to the empty message per protobuf default-omission.

fn
fn decode_health_response(msg : Bytes) -> ServingStatus

Decode the status field of a HealthCheckResponse; an absent field reads as the 0 default (StatusUnknown).

let
let health_check_path : String = "/grpc.health.v1.Health/Check"

The gRPC HTTP/2 path of the Check method.

let
let health_watch_path : String = "/grpc.health.v1.Health/Watch"

The gRPC HTTP/2 path of the Watch method.

struct
struct HealthService

A grpc.health.v1.Health service backed by a per-service status table. The empty key "" is the overall-server status; a fresh service reports the whole server SERVING.

fn
fn HealthService::new() -> HealthService

A health service reporting the overall server as SERVING.

fn
fn HealthService::set_status( self : HealthService, service : String, status : ServingStatus) -> Unit

Set the serving status of a named service (or the overall server with "").

fn
fn HealthService::check( self : HealthService, service : String) -> ServingStatus

The serving status of a named service: its set status, or ServiceUnknown when the service was never registered.

fn
fn HealthService::handlers( self : HealthService) -> Array[(String, Handler)]

The (path, handler) pairs implementing the service: Check as a unary method and Watch as a server-streaming method that emits the current status. A live Watch that also pushes on every later change needs a streaming source the pure engine's eager ServerStreaming shape does not model, so this emits the status at subscribe time — the first message a real Watch always sends.

fn
fn HealthService::install(self : HealthService, server : H2Server) -> Unit

Register Check and Watch on a pure server engine.

item
extend ServingStatus with Eq::

§Server Reflection service

The grpc.reflection.v1.ServerReflection service (and its v1alpha alias) as a bidi stream: ListServices enumerates registered services, FileContainingSymbol and FileByFilename return the real FileDescriptorProto bytes, so a reflection client such as grpcurl can list and describe a service.

let
let reflection_v1_path : String = "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"

The gRPC HTTP/2 path of the v1 reflection stream.

let
let reflection_v1alpha_path : String = "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo"

The gRPC HTTP/2 path of the legacy v1alpha reflection stream. grpcurl tries v1 first and falls back to this, so a server registers both.

enum
enum ReflectionRequest

A decoded ServerReflectionRequest, reduced to the one message_request oneof arm that was set. Extension-number queries are surfaced as Unsupported, which the service answers with an UNIMPLEMENTED error response.

fn
fn decode_reflection_request( body : Bytes) -> ReflectionRequest raise PbError

Decode a ServerReflectionRequest down to its message_request oneof arm. host (field 1) is accepted and ignored; an unset oneof reads as Unsupported.

fn
fn encode_reflection_request(req : ReflectionRequest) -> Bytes

Encode a ServerReflectionRequest carrying a single oneof arm. Used by an in-process reflection client (and by the round-trip tests).

enum
enum ReflectionResponse

A decoded ServerReflectionResponse, reduced to the one message_response arm.

fn
fn decode_reflection_response( body : Bytes) -> ReflectionResponse raise PbError

Decode a ServerReflectionResponse down to its message_response arm — the half an in-process reflection client (and the tests) needs to read an answer.

struct
struct ReflectionService

A grpc.reflection.v1.ServerReflection service backed by an in-memory descriptor database. Each added FileDescriptor is indexed by filename and by every symbol (package.Service / package.Message) it defines, so a FileContainingSymbol or FileByFilename query resolves to the right file, and ListServices enumerates every registered service.

fn
fn ReflectionService::new() -> ReflectionService

An empty reflection service. Add the descriptors of the services you serve with add_file.

fn
fn ReflectionService::add_file( self : ReflectionService, file : FileDescriptor) -> Unit

Register a file descriptor: index it by filename and by each symbol it defines, and add its services to the ListServices set.

fn
fn ReflectionService::handle( self : ReflectionService, request : Bytes) -> Bytes

Answer one ServerReflectionRequest (raw bytes) with the encoded ServerReflectionResponse. A decode failure or an unknown symbol/filename yields an ErrorResponse rather than raising, since it rides a non-raising stream handler.

fn
fn ReflectionService::handler(self : ReflectionService) -> Handler

The bidi handler backing ServerReflectionInfo: one response per request, none at half-close.

fn
fn ReflectionService::install( self : ReflectionService, server : H2Server) -> Unit

Register ServerReflectionInfo at both the v1 and v1alpha paths on a pure server engine.

item
extend ReflectionRequest with Eq::
item
extend ReflectionResponse with Eq::

§h2c socket transport (native)

The native driver that pumps bytes between a real @socket.Tcp connection and the H2Server engine: GrpcServer registers handlers of every call kind and serves them over the self-built HTTP/2 (h2c) transport. Native-only — real sockets and moonbitlang/async have no JS/Wasm backend.

struct
struct GrpcServer

A gRPC server: a registry of handlers keyed by /pkg.Service/Method path, served over the self-built HTTP/2 (h2c) transport. Handlers may be any of the four gRPC call kinds. Each accepted connection gets its own @moonrpc.H2Server engine (HPACK and flow control are per-connection state).

fn
fn GrpcServer::new() -> GrpcServer

A gRPC server with no registered methods.

fn
fn GrpcServer::register( self : GrpcServer, path : String, handler : (Bytes) -> Bytes) -> Unit

Register a unary handler (request) -> reply (messages without their gRPC length prefix) for a fully-qualified path /pkg.Service/Method.

fn
fn GrpcServer::register_unary( self : GrpcServer, path : String, handler : (@moonrpc.RpcContext, Bytes) -> Bytes) -> Unit

Register a unary handler that receives the [RpcContext], so it can read request metadata / the deadline and end the call with a non-OK status via ctx.fail.

fn
fn GrpcServer::register_server_streaming( self : GrpcServer, path : String, handler : (@moonrpc.RpcContext, Bytes) -> Array[Bytes]) -> Unit

Register a server-streaming handler: one request message in, an ordered sequence of reply messages out.

fn
fn GrpcServer::register_client_streaming( self : GrpcServer, path : String, handler : (@moonrpc.RpcContext, Array[Bytes]) -> Bytes) -> Unit

Register a client-streaming handler: every request message is collected, and after the client half-closes the handler returns one reply.

fn
fn GrpcServer::register_bidi( self : GrpcServer, path : String, factory : (@moonrpc.RpcContext) -> @moonrpc.BidiHandler) -> Unit

Register a bidirectional-streaming handler.

fn
fn GrpcServer::register_reflection( self : GrpcServer, refl : @moonrpc.ReflectionService) -> Unit

Register the grpc.reflection.v1.ServerReflection service (and its v1alpha alias) so a reflection client such as grpcurl can list and describe the services in refl's descriptor database.

let
let port : Int = 12000

The port a gRPC server listens on unless told otherwise. gRPC runs on HTTP/2, which shares a TCP listener with HTTP/1.1 and WebSocket, so it takes the family's number rather than gRPC's conventional 50051: one port for every protocol a server speaks.

item
async fn GrpcServer::serve( self : GrpcServer, host? : String = "127.0.0.1", port? : Int = port) -> Unit

Serve gRPC over h2c on host:port, blocking in the accept loop until the running task is cancelled or graceful_stop drains the server. Each connection is driven by drive; a failure on one connection is isolated and does not stop the server.

item
async fn GrpcServer::graceful_stop( self : GrpcServer, grace? : Int = 10000) -> Unit

Stop serving without cutting anyone off: stop accepting new connections, let the calls that are already running finish, and send every connection a GOAWAY naming the last stream it opened, so clients know what was accepted and can reconnect elsewhere. Returns once the last connection is gone, or after grace milliseconds — a client that received the GOAWAY and then went quiet without closing cannot be waited on forever. serve returns on its own once the accept loop and the last driver are done, so the task running it ends without being cancelled; if the grace period ran out with connections still open, cancelling that task is what releases them.

§Channel client (native)

The native Channel: a long-lived, multiplexed h2c connection over a real @socket.Tcp, driven by the H2Client engine, performing unary, server- and client-streaming calls and enforcing the grpc-timeout deadline by racing the read loop against a timer.

struct
struct Channel

A connected gRPC channel. One TCP connection carries every call, each on its own client-allocated (odd) stream id, so calls multiplex over the shared HPACK and flow-control state the engine keeps.

item
async fn Channel::connect(host : String, port : Int) -> Channel

Open a channel to host:port: connect, send the 24-octet connection @http2.preface and the client SETTINGS. The server's SETTINGS are read and acknowledged lazily on the first call.

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

Close the underlying connection. Outstanding calls are abandoned.

item
async fn Channel::unary( self : Channel, path : String, request : Bytes, metadata? : Array[@header.Header] = [], timeout_millis? : Int, retry? : @moonrpc.RetryPolicy) -> @moonrpc.CallReply

Make a unary call: one request message, one reply. timeout_millis, when set, is sent as grpc-timeout and enforced locally. With a retry policy the call is re-issued on a new stream after a backoff sleep while its status is retryable and attempts remain, with the whole retry sequence bounded by the one deadline. A call is retried only while uncommitted — here, while no reply message has arrived; committing on received initial metadata as well (gRPC A6) is a refinement, and matters only for the rare retryable failure that follows a non-trailers-only response.

item
async fn Channel::server_streaming( self : Channel, path : String, request : Bytes, metadata? : Array[@header.Header] = [], timeout_millis? : Int) -> @moonrpc.CallReply

Make a server-streaming call: one request message, then read the ordered run of reply messages (in CallReply::messages) until the server closes the stream.

item
async fn Channel::stream_take( self : Channel, path : String, request : Bytes, count : Int, metadata? : Array[@header.Header] = [], timeout_millis? : Int, half_close? : Bool = true) -> Array[Bytes]

Open a streaming call and collect at least count reply messages read inline off the socket — no background reader — returning as soon as that many have arrived or the server ends the stream. This bounds an otherwise unbounded stream (a subscription such as etcd's Watch, or a long server-streaming feed) to the messages the caller wants, then the caller closes the channel; reading inline keeps the whole exchange in one task with a clean close, avoiding a demux pump that a blocked socket read can't be cancelled out of. Pass half_close=false for a bidi subscription that keeps its request side open.

item
async fn Channel::client_streaming( self : Channel, path : String, requests : Array[Bytes], metadata? : Array[@header.Header] = [], timeout_millis? : Int) -> @moonrpc.CallReply

Make a client-streaming call: send every message in requests, half-close, then read the single reply.

item
async fn Channel::bidi( self : Channel, path : String, requests : Array[Bytes], metadata? : Array[@header.Header] = [], timeout_millis? : Int) -> @moonrpc.CallReply

Make a bidirectional-streaming call: send every message in requests, half-close, then read the ordered run of reply messages the server produced (in CallReply::messages). This is the eager form the engine's synchronous core models — every request is sent before the replies are collected, which pairs with the eager server BidiHandler. A truly interactive exchange (sending while receiving) or several concurrent calls multiplexed on one channel need the shared read-demux loop on the roadmap; until then a Channel drives one call at a time.

fn
fn GrpcServer::register_health( self : GrpcServer, health : @moonrpc.HealthService) -> Unit

Register the grpc.health.v1.Health service (Check + Watch) on this server.

§Call policy

What a client does around a call rather than inside it: the retry policy and its backoff, hedged attempts across backends, and the keepalive pings that keep an idle connection from being reaped.

struct
struct RetryPolicy

A method's retry policy. max_attempts counts the first try plus every retry (gRPC caps it at 5); a call is retried only while attempts remain and its status is in retryable. Backoff starts at initial_backoff_millis and grows by backoff_multiplier each retry, capped at max_backoff_millis.

fn
fn RetryPolicy::default() -> RetryPolicy

A conventional default: up to 3 attempts, 100 ms initial backoff doubling to a 1 s cap, retrying only UNAVAILABLE — the code a transient, safe-to-retry transport failure carries.

fn
fn RetryPolicy::should_retry( self : RetryPolicy, status_code : Int, attempts_made : Int) -> Bool

Whether a call that has made attempts_made attempts (the first is 1) and got status_code should be tried again: attempts must remain, and the status must be non-OK and listed as retryable.

fn
fn RetryPolicy::backoff_millis( self : RetryPolicy, retry_index : Int) -> Int

The backoff cap before the retry_index-th retry (the first retry is 1): min(initial * multiplier^(retry_index-1), max). gRPC sleeps a value drawn uniformly from [0, cap]; the async driver applies that jitter, so this returns the deterministic upper bound (which is also what a jitter-free driver sleeps).

struct
struct HedgingPolicy

A method's hedging policy. max_attempts counts every parallel attempt (gRPC caps it at 5); a new attempt originates every hedging_delay_millis while attempts remain. A finished attempt commits the whole call unless its status is one of non_fatal, in which case hedging continues.

fn
fn HedgingPolicy::default() -> HedgingPolicy

A conventional default: up to 3 parallel attempts, a fresh one every 500 ms, treating only UNAVAILABLE as non-fatal — so a transient transport failure keeps hedging while a real application error commits at once.

fn
fn HedgingPolicy::attempt_cap(self : HedgingPolicy) -> Int

The effective attempt cap: the configured max_attempts, but never above gRPC's ceiling of 5.

fn
fn HedgingPolicy::can_start_another( self : HedgingPolicy, attempts_started : Int) -> Bool

Whether another hedged attempt may originate given attempts_started (the number already launched): only while attempts remain under the cap.

fn
fn HedgingPolicy::should_commit( self : HedgingPolicy, status_code : Int) -> Bool

Whether a finished attempt with status_code commits the whole call — stopping hedging and returning this result. OK commits (success), and so does any error *not* listed as non-fatal (a real failure retrying won't fix). A non-fatal error does not commit; hedging continues.

fn
fn HedgingPolicy::delay_millis(self : HedgingPolicy) -> Int

The delay between originating hedged attempts. The async driver waits this long before launching the next parallel attempt; 0 fires them all at once.

struct
struct Keepalive

Keepalive state for one connection: the idle threshold that triggers a PING, the ACK deadline that declares the connection dead, and the timers tracking the last activity and any in-flight PING.

fn
fn Keepalive::new( keepalive_time~ : Int64, keepalive_timeout~ : Int64, now? : Int64 = 0L) -> Keepalive

A keepalive that pings after keepalive_time of idleness and gives a PING keepalive_timeout to be acknowledged. now seeds the activity clock.

fn
fn Keepalive::on_activity(self : Keepalive, now : Int64) -> Unit

Record connection activity (a frame sent or received) at now, resetting the idle timer.

fn
fn Keepalive::should_ping(self : Keepalive, now : Int64) -> Bool

Whether a keepalive PING should be sent at now: the connection has been idle at least keepalive_time and no PING is already awaiting its ACK.

fn
fn Keepalive::on_ping_sent(self : Keepalive, now : Int64) -> Unit

Record that a keepalive PING was sent at now.

fn
fn Keepalive::on_ping_ack(self : Keepalive, now : Int64) -> Unit

Record that the PING's ACK arrived at now — the connection is alive, and this counts as activity.

fn
fn Keepalive::is_timed_out(self : Keepalive, now : Int64) -> Bool

Whether the connection is dead at now: a PING is outstanding and its ACK has not arrived within keepalive_timeout of sending it.

§Load balancing and naming

Picking a backend and knowing whether it is reachable: the balancers, the connectivity state machine gRPC defines, DNS resolution, and the channel pool.

enum
enum LbPolicy

A client-side load-balancing policy (← gRPC loadBalancingConfig).

struct
struct LoadBalancer

A load balancer over a resolver's addresses, applying an LbPolicy to choose the next one.

fn
fn LoadBalancer::new( addresses : Array[String], policy? : LbPolicy = PickFirst) -> LoadBalancer

A balancer over addresses (each a host:port) using policy (default PickFirst).

fn
fn LoadBalancer::pick(self : LoadBalancer) -> String?

The address the next call should use, or None when the resolver produced none. PickFirst always returns the first address; RoundRobin returns each in turn, wrapping around.

fn
fn LoadBalancer::size(self : LoadBalancer) -> Int

The number of addresses the balancer is choosing among.

item
extend LbPolicy with Debug::
item
extend LbPolicy with Eq::
enum
enum ConnectivityState

A sub-connection's connectivity state (← gRPC connectivity.State).

struct
struct SubConn

One address the Channel dials, tracking its connectivity state.

fn
fn SubConn::new(address : String) -> SubConn

A fresh sub-connection to address, starting IDLE.

fn
fn SubConn::connect(self : SubConn) -> Unit

Begin connecting (IDLE or TRANSIENT_FAILURE → CONNECTING). A no-op once SHUTDOWN or already connecting/ready.

fn
fn SubConn::on_connected(self : SubConn) -> Unit

The connection attempt succeeded (CONNECTING → READY).

fn
fn SubConn::on_failure(self : SubConn) -> Unit

A connection attempt or an established connection failed (→ TRANSIENT_FAILURE), unless shut down.

fn
fn SubConn::on_idle(self : SubConn) -> Unit

A READY connection went idle (READY → IDLE).

fn
fn SubConn::shutdown(self : SubConn) -> Unit

Shut the sub-connection down permanently (→ SHUTDOWN, a terminal state).

fn
fn SubConn::is_ready(self : SubConn) -> Bool

Whether this sub-connection can carry RPCs right now.

item
extend ConnectivityState with Debug::
item
extend ConnectivityState with Eq::
let
let dns_type_a : Int = 1

The DNS record type for an IPv4 address.

let
let dns_type_aaaa : Int = 28

The DNS record type for an IPv6 address.

struct
struct DnsRecord

One answer resource record, carrying the textual address for A/AAAA records (empty for other types).

struct
struct DnsResponse

A decoded DNS response: the transaction id echoed back, the response code (0 = NOERROR), and the answer records.

fn
fn dns_encode_query(id : Int, name : String, qtype : Int) -> Bytes

Encode a standard recursion-desired query for name of type qtype (dns_type_a or dns_type_aaaa) in class IN, with transaction id id.

fn
fn dns_decode_response(msg : Bytes) -> DnsResponse

Decode a DNS response: echoed id, response code, and every answer record (A/AAAA records carry their textual address). Questions are skipped; authority and additional sections are ignored.

fn
fn DnsResponse::addresses(self : DnsResponse) -> Array[String]

Every resolved A/AAAA address in the response, in record order.

item
extend DnsRecord with Debug::
item
extend DnsRecord with Eq::
item
extend DnsResponse with Debug::
item
extend DnsResponse with Eq::
struct
struct ConnectionPool

A pool of sub-connections, one per resolved address (in resolver order), that picks a READY one per an LbPolicy.

fn
fn ConnectionPool::new( addresses : Array[String], policy? : LbPolicy = PickFirst) -> ConnectionPool

A pool over addresses (each a host:port), each sub-connection starting IDLE, choosing among the READY ones with policy (default PickFirst).

fn
fn ConnectionPool::subconn( self : ConnectionPool, address : String) -> SubConn?

The sub-connection for address, if the pool has one — the handle the Channel drives.

fn
fn ConnectionPool::ready_addresses(self : ConnectionPool) -> Array[String]

The addresses of every READY sub-connection, in resolver order.

fn
fn ConnectionPool::pick(self : ConnectionPool) -> String?

Pick a READY sub-connection's address for the next call, applying the LB policy over just the ready ones (← gRPC picker). None when no sub-connection is ready.

§Channels over a socket

The native client transports: a multiplexing channel that demultiplexes concurrent calls on one connection, a managed channel that dials several backends and routes around the dead ones, and the resolver behind it.

struct
struct MuxChannel

A multiplexed channel over one connection: the engine keeps every call's stream state, inboxes wakes a waiting call when its stream is done, and write_lock serializes frame writes across concurrent callers.

item
async fn MuxChannel::connect(host : String, port : Int) -> MuxChannel

Open a multiplexed channel to host:port: connect and send the @http2.preface and client SETTINGS. Spawn read_pump into a task group to drive it before making calls.

item
async fn MuxChannel::read_pump(self : MuxChannel) -> Unit

The shared read loop: read a frame, feed it to the engine, write back any control response (SETTINGS ack, PING pong, WINDOW_UPDATE), and wake every call whose stream has completed. Returns when the connection closes. Spawn this once per channel.

item
async fn MuxChannel::unary( self : MuxChannel, path : String, request : Bytes, metadata? : Array[@header.Header] = [], timeout_millis? : Int) -> @moonrpc.CallReply

Make a unary call, concurrent with any other calls on this channel: register an inbox, send the request under the write lock, and wait for the pump to signal the stream done, then take the reply.

struct
struct BidiStream

A bidirectional stream on a multiplexed channel: send request messages with send, half-close with close_send, and read reply messages one at a time with recv — all interleaved, sharing the connection with other calls. This is truly interactive streaming (send while receiving), which the one-shot Channel can't do.

fn
fn MuxChannel::open_bidi( self : MuxChannel, path : String, metadata? : Array[@header.Header] = []) -> BidiStream

Open a bidirectional stream to path. Send request messages with send/close_send and read replies with recv.

item
async fn BidiStream::send(self : BidiStream, message : Bytes) -> Unit

Send one request message on the stream.

item
async fn BidiStream::close_send(self : BidiStream) -> Unit

Half-close the request side: no more messages will be sent.

item
async fn BidiStream::recv(self : BidiStream) -> Bytes?

Read the next reply message, blocking until one arrives. None once the server has finished the stream and every message has been read.

item
async fn MuxChannel::run_keepalive( self : MuxChannel, keepalive_time~ : Int64, keepalive_timeout~ : Int64) -> Unit

Drive keepalive on this channel: after keepalive_time ms of no traffic, send an HTTP/2 PING; if its ACK doesn't come back within keepalive_timeout ms the connection is declared dead and closed (← gRPC keepalive_time/keepalive_timeout). The read pump records incoming activity and PING ACKs. Spawn this alongside read_pump.

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

Close the connection. Outstanding calls are abandoned; the read pump exits.

struct
struct ManagedChannel

A load-balanced channel over several backends. The pool holds one sub-connection per address (its connectivity state); channels holds the live Channel for each address that dialed successfully.

item
async fn ManagedChannel::connect( targets : Array[(String, Int)], policy? : @moonrpc.LbPolicy = PickFirst) -> ManagedChannel

Dial every backend in targets (each a host, port), marking each sub-connection READY on a successful connect and TRANSIENT_FAILURE on a dial error, then load-balance calls across the ones that came up. Addresses keep their given order, which is the order RoundRobin cycles and PickFirst prefers.

item
async fn ManagedChannel::dns_connect( dns_server : String, dns_port : Int, name : String, target_port : Int, policy? : @moonrpc.LbPolicy = PickFirst, id? : Int = 0x2a2a) -> ManagedChannel

Resolve name's A records via the DNS server at dns_server:dns_port, then dial each resolved address at target_port and load-balance across them — gRPC's dns:/// target end to end (name resolution feeding the connection pool).

fn
fn ManagedChannel::ready_addresses(self : ManagedChannel) -> Array[String]

The addresses of every backend currently carrying calls, in resolver order — the READY subset the picker draws from.

item
async fn ManagedChannel::unary( self : ManagedChannel, path : String, request : Bytes, metadata? : Array[@header.Header] = [], timeout_millis? : Int, retry? : @moonrpc.RetryPolicy) -> @moonrpc.CallReply

Make a unary call on a picked backend. Returns an UNAVAILABLE reply when no sub-connection is READY.

item
async fn ManagedChannel::unary_hedged( self : ManagedChannel, path : String, request : Bytes, hedging : @moonrpc.HedgingPolicy, metadata? : Array[@header.Header] = [], timeout_millis? : Int) -> @moonrpc.CallReply

Make a unary call under a hedging policy: fire attempts in parallel — a fresh one every delay_millis, each on a picked backend — and return the first whose status commits (OK or a fatal error), cancelling the rest. If every attempt finishes without committing (all non-fatal), the last reply is returned.

item
async fn ManagedChannel::server_streaming( self : ManagedChannel, path : String, request : Bytes, metadata? : Array[@header.Header] = [], timeout_millis? : Int) -> @moonrpc.CallReply

Make a server-streaming call on a picked backend.

item
async fn ManagedChannel::client_streaming( self : ManagedChannel, path : String, requests : Array[Bytes], metadata? : Array[@header.Header] = [], timeout_millis? : Int) -> @moonrpc.CallReply

Make a client-streaming call on a picked backend.

item
async fn ManagedChannel::bidi( self : ManagedChannel, path : String, requests : Array[Bytes], metadata? : Array[@header.Header] = [], timeout_millis? : Int) -> @moonrpc.CallReply

Make a bidirectional-streaming call on a picked backend.

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

Close every live backend connection.

item
async fn resolve_a( server : String, server_port : Int, name : String, id? : Int = 0x2a2a) -> Array[String]

Resolve name's A records by querying the DNS server at server:server_port over UDP, returning the dotted-decimal addresses (empty on a mismatched-id reply or an answer with no A records). id is the query's transaction id, echoed in the reply.

§Metadata

The metadata rules that decide which keys travel base64 encoded. The gzip compression gRPC negotiates is moonzip's, and the header compression is moonhttp's.

fn
fn is_binary_metadata(name : Bytes) -> Bool

True when a metadata key carries binary, which gRPC spells by suffixing the key with -bin.

fn
fn metadata_value_from_wire(name : Bytes, value : Bytes) -> Bytes

Decode a metadata value coming off the wire: base64 for a -bin key, verbatim otherwise. Padding is optional on the wire, and a peer may pad or not; anything outside the alphabet is skipped rather than rejected, which is what the gRPC spec asks a receiver to tolerate.

fn
fn metadata_value_to_wire(name : Bytes, value : Bytes) -> Bytes

Encode a metadata value for the wire: base64 for a -bin key, verbatim otherwise.