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 Lfan-ke/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.

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.

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.

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.

§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)
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)
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.

§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.

§HPACK primitives

The RFC 7541 header-compression primitives: the 61-entry static table, the prefix-integer representation (§5.1), and non-Huffman string literals (§5.2).

fn
fn hpack_static_table() -> Array[(String, String)]

The 61-entry HPACK static header table (RFC 7541, Appendix A) as (name, value) pairs in RFC index order, i.e. result[0] is index 1 (:authority) and result[60] is index 61 (www-authenticate).

fn
fn hpack_static_entry(index : Int) -> (String, String)?

Look up an HPACK static-table entry by its 1-based RFC index (1..=61), returning (name, value) or None when the index is out of range.

fn
fn hpack_encode_int(value : Int, prefix_bits : Int) -> Bytes

Encode value as an HPACK integer with an prefix_bits-bit prefix (RFC 7541 §5.1). The high 8 - prefix_bits bits of the first octet are left zero for the caller to OR in any flag bits. Examples: 10 on a 5-bit prefix is [0x0A]; 1337 on a 5-bit prefix is [0x1F, 0x9A, 0x0A].

fn
fn hpack_decode_int( data : Bytes, offset : Int, prefix_bits : Int) -> (Int, Int)

Decode an HPACK integer with an prefix_bits-bit prefix from data starting at offset (RFC 7541 §5.1), returning (value, bytes_consumed). Any flag bits above the prefix in the first octet are masked off and ignored.

fn
fn hpack_string_is_huffman(data : Bytes, offset : Int) -> Bool

Whether the string literal at offset is Huffman-coded, i.e. the H bit (the top bit of the length octet) is set (RFC 7541 §5.2).

fn
fn hpack_encode_string(octets : Bytes) -> Bytes

Encode octets as a non-Huffman HPACK string literal (RFC 7541 §5.2): the length as a 7-bit-prefix integer with the H bit clear, followed by the raw octets.

fn
fn hpack_decode_string(data : Bytes, offset : Int) -> (Bytes, Int)

Decode an HPACK string literal from data at offset, returning (octets, bytes_consumed). The length is read as a 7-bit-prefix integer (the H bit is masked off); this is the inverse of hpack_encode_string for H = 0. Use hpack_string_is_huffman first if the literal may be Huffman-coded, as Huffman decoding is not applied here.

§HPACK Huffman coding

The RFC 7541 Appendix B canonical Huffman code table with a prefix-trie decoder and an EOS-padding encoder (§5.2).

item
suberror HpackError

A raised HPACK failure (Huffman decoding or header-block decoding).

item
impl Show for HpackError with fn output(self, logger)
fn
fn huffman_encode(input : Bytes) -> Bytes

Huffman-encode input (RFC 7541 §5.2): each octet becomes its code, and the final partial octet is padded with the most-significant bits of the EOS code (all ones). The inverse of huffman_decode for valid inputs.

fn
fn huffman_encoded_length(input : Bytes) -> Int

The number of octets input occupies when Huffman-encoded, without building the output — used to choose the shorter of raw vs. Huffman string literals.

fn
fn huffman_decode(input : Bytes) -> Bytes raise HpackError

Huffman-decode input (RFC 7541 §5.2). Raises HuffmanError if the input contains the EOS symbol, if the trailing padding is not a run of fewer than 8 one-bits, or if the bit stream leaves the code space. The inverse of huffman_encode.

§HPACK dynamic table & codec

The size-bounded dynamic table with eviction (§4), the six header-field representations (§6), and a stateful HpackEncoder / HpackDecoder pair.

struct
struct Header

A decoded header field: name and value as raw octet strings (HTTP/2 header names and values are byte sequences, and gRPC -bin metadata is binary).

fn
fn hpack_encode_string_huffman(octets : Bytes) -> Bytes

Encode octets as a Huffman-coded HPACK string literal (RFC 7541 §5.2): the H bit set, the Huffman length as a 7-bit-prefix integer, then the code.

fn
fn hpack_encode_string_auto(octets : Bytes) -> Bytes

Encode octets as an HPACK string literal, choosing the shorter of the raw (H = 0) and Huffman (H = 1) forms — the standard encoder heuristic.

fn
fn hpack_read_string( data : Bytes, offset : Int) -> (Bytes, Int) raise HpackError

Read an HPACK string literal at offset, resolving Huffman coding when the H bit is set, returning (octets, bytes_consumed). Unlike hpack_decode_string, this applies Huffman decoding. Raises on bad Huffman.

struct
struct DynamicTable

The HPACK dynamic table: a FIFO of recently seen (name, value) entries, newest first (entries[0]), bounded by max_size octets where each entry costs name.len + value.len + 32 (RFC 7541 §4.1). Adding evicts the oldest entries until the newcomer fits; an entry larger than max_size empties the table and is not stored (§4.4).

fn
fn DynamicTable::new(max_size? : Int = 4096) -> DynamicTable

A new empty dynamic table bounded by max_size octets (default 4096, the HTTP/2 initial SETTINGS_HEADER_TABLE_SIZE).

fn
fn DynamicTable::set_max_size(self : DynamicTable, new_max : Int) -> Unit

Resize the table (a dynamic table size update, RFC 7541 §4.2), evicting to fit.

fn
fn DynamicTable::add( self : DynamicTable, name : Bytes, value : Bytes) -> Unit

Insert (name, value) at the front, evicting oldest entries to make room. If the entry alone exceeds max_size the table ends up empty (RFC 7541 §4.4).

fn
fn DynamicTable::count(self : DynamicTable) -> Int

The number of entries currently in the dynamic table.

fn
fn DynamicTable::current_size(self : DynamicTable) -> Int

The current total size of the dynamic table in octets (§4.1 accounting).

fn
fn hpack_encode_size_update(new_max : Int) -> Bytes

Encode a dynamic table size update (RFC 7541 §6.3): 001 prefix with the new maximum size as a 5-bit-prefix integer.

struct
struct HpackDecoder

A stateful HPACK decoder: it owns a dynamic table that persists across the header blocks of a connection. limit is the peer-agreed hard cap (SETTINGS_HEADER_TABLE_SIZE) a size update may not exceed.

fn
fn HpackDecoder::new(max_size? : Int = 4096) -> HpackDecoder

A new decoder whose dynamic table is bounded by max_size octets (also the hard cap enforced on dynamic table size updates).

fn
fn HpackDecoder::decode( self : HpackDecoder, block : Bytes) -> Array[Header] raise HpackError

Decode one complete header block into its header list (RFC 7541 §6), mutating the dynamic table for incrementally indexed fields and size updates. Raises HpackDecodeError/HuffmanError on any malformed representation.

struct
struct HpackEncoder

A stateful HPACK encoder: it owns a dynamic table mirroring the decoder's, and prefers indexed representations. huffman selects Huffman string literals when they are shorter.

fn
fn HpackEncoder::new( max_size? : Int = 4096, huffman? : Bool = true) -> HpackEncoder

A new encoder bounded by max_size octets; huffman (default true) enables the shorter-of-two string-literal heuristic.

fn
fn HpackEncoder::encode( self : HpackEncoder, headers : Array[Header]) -> Bytes

Encode a header list into a header block (RFC 7541 §6), using indexed fields where possible and literal-with-incremental-indexing otherwise (mutating the dynamic table to mirror what the peer decoder will build). The output decodes back to the same header list via HpackDecoder.

§HTTP/2 frame layer

The RFC 7540 frame codec: the 9-octet header and all ten frame types (DATA / HEADERS / PRIORITY / RST_STREAM / SETTINGS / PUSH_PROMISE / PING / GOAWAY / WINDOW_UPDATE / CONTINUATION) with their flags and payloads.

let
let frame_data : Int = 0x0

HTTP/2 frame type codes (RFC 7540 §6).

let
let frame_headers : Int = 0x1
let
let frame_priority : Int = 0x2
let
let frame_rst_stream : Int = 0x3
let
let frame_settings : Int = 0x4
let
let frame_push_promise : Int = 0x5
let
let frame_ping : Int = 0x6
let
let frame_goaway : Int = 0x7
let
let frame_window_update : Int = 0x8
let
let frame_continuation : Int = 0x9
let
let flag_end_stream : Int = 0x1

HTTP/2 frame flags (RFC 7540 §6). Flags are type-specific; the same bit carries different meaning per frame type, hence the shared numeric values.

let
let flag_ack : Int = 0x1

ACK on SETTINGS and PING shares bit 0x1 with END_STREAM.

let
let flag_end_headers : Int = 0x4
let
let flag_padded : Int = 0x8
let
let flag_priority : Int = 0x20
let
let settings_header_table_size : Int = 0x1

SETTINGS parameter identifiers (RFC 7540 §6.5.2).

let
let settings_enable_push : Int = 0x2
let
let settings_max_concurrent_streams : Int = 0x3
let
let settings_initial_window_size : Int = 0x4
let
let settings_max_frame_size : Int = 0x5
let
let settings_max_header_list_size : Int = 0x6
let
let error_no_error : Int = 0x0

HTTP/2 error codes (RFC 7540 §7), carried by RST_STREAM and GOAWAY.

let
let error_protocol_error : Int = 0x1
let
let error_internal_error : Int = 0x2
let
let error_flow_control_error : Int = 0x3
let
let error_settings_timeout : Int = 0x4
let
let error_stream_closed : Int = 0x5
let
let error_frame_size_error : Int = 0x6
let
let error_refused_stream : Int = 0x7
let
let error_cancel : Int = 0x8
let
let error_compression_error : Int = 0x9
let
let error_connect_error : Int = 0xA
let
let error_enhance_your_calm : Int = 0xB
let
let error_inadequate_security : Int = 0xC
let
let error_http_1_1_required : Int = 0xD
struct
struct Priority

The stream-priority block shared by PRIORITY frames and the optional priority section of HEADERS (RFC 7540 §5.3.2). weight is the raw wire byte (0..255); the effective priority weight is weight + 1 (§6.3).

item
suberror FrameError

A raised decode failure. Incomplete is the normal "need more bytes" signal a streaming reader catches to wait for the rest; the others are protocol errors.

item
impl Show for FrameError with fn output(self, logger)
enum
enum Frame

A decoded HTTP/2 frame (RFC 7540 §6). Each variant carries the semantic payload with padding already stripped; padding is the number of padding bytes to (re)emit. Unknown preserves extension/unrecognised frames verbatim so a reader can forward or ignore them (RFC 7540 §4.1).

struct
struct FrameHeader

The fixed 9-octet frame header (RFC 7540 §4.1): a 24-bit payload length, an 8-bit type, 8-bit flags, a reserved bit, and a 31-bit stream identifier.

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

Encode a FrameHeader to its 9 wire octets.

fn
fn decode_frame_header( data : Bytes, offset? : Int = 0) -> FrameHeader raise FrameError

Decode the 9-octet frame header at offset. Raises Incomplete when fewer than 9 octets are available. The reserved bit is masked off the stream id.

fn
fn Frame::frame_type(self : Frame) -> Int

The numeric frame-type code of this frame (RFC 7540 §6).

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

Encode this frame to its complete wire representation (9-octet header + payload), the exact inverse of decode_frame.

fn
fn decode_frame( data : Bytes, offset? : Int = 0) -> (Frame, Int) raise FrameError

Decode exactly one frame at offset, returning (frame, bytes_consumed) where bytes_consumed is 9 + payload_length. Raises Incomplete when the buffer does not yet hold the whole frame, or a protocol error when the payload is malformed for its type. The inverse of Frame::encode.

§HTTP/2 connection preface

The fixed 24-octet client connection preface (RFC 7540 §3.5).

let
let connection_preface : Bytes = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

The HTTP/2 client connection preface (RFC 7540 §3.5): the fixed, case-sensitive 24-octet sequence PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n a client sends before its first frame, which must be a SETTINGS frame. Its bytes deliberately form a malformed HTTP/1.1 request so an HTTP/1.1-only server rejects it cleanly.

fn
fn has_connection_preface(data : Bytes) -> Bool

Whether data begins with the exact 24-octet HTTP/2 connection preface.

§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)
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)
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).

§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).

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::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[Frame]

The server's opening frames (RFC 7540 §3.5): a SETTINGS frame disabling server push. Sent immediately after the client connection preface is validated, before any request frame is read.

fn
fn H2Server::feed(self : H2Server, frame : Frame) -> Array[Frame] raise

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). Raises on an illegal stream transition or a malformed header block.

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::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[Frame]

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

fn
fn H2Client::open( self : H2Client, path : String, metadata? : Array[Header] = [], authority? : String = "127.0.0.1", timeout_millis? : Int? = None) -> 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[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[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] = [], authority? : String = "127.0.0.1", timeout_millis? : Int? = None) -> (Int, Array[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 : Frame) -> Array[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::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::reply(self : H2Client, id : Int) -> CallReply

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

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.

§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.

§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.

§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_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.

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

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

§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 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[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply

Make a unary call: one request message, one reply. timeout_millis, when set, is sent as grpc-timeout and enforced locally.

item
async fn Channel::server_streaming( self : Channel, path : String, request : Bytes, metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @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::client_streaming( self : Channel, path : String, requests : Array[Bytes], metadata? : Array[@moonrpc.Header] = [], timeout_millis? : Int? = None) -> @moonrpc.CallReply

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

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

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