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.
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 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 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 Status
The 17 canonical gRPC status codes (grpc-status).
fn Status::code(self : Status) -> Int
The numeric grpc-status code.
fn Status::name(self : Status) -> String
The canonical uppercase status name.
struct Method
A fully-qualified RPC method: package.Service and the method name.
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 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.
impl Show for WireType with fn output(self, logger)
fn WireType::code(self : WireType) -> Int
The wire-type number (the tag's low three bits).
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.
suberror PbErrorA 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.
impl Show for PbError with fn output(self, logger)
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 PbWriter::new() -> PbWriter
A fresh, empty message encoder.
fn PbWriter::to_bytes(self : PbWriter) -> Bytes
The bytes written so far.
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 PbWriter::write_tag( self : PbWriter, field : Int, wire : WireType) -> Unit
Append a field tag: (field_number << 3) | wire_type, itself a varint.
fn PbWriter::write_fixed32(self : PbWriter, v : UInt) -> Unit
Append a little-endian 32-bit fixed value (wire type 5, no tag).
fn PbWriter::write_fixed64(self : PbWriter, v : UInt64) -> Unit
Append a little-endian 64-bit fixed value (wire type 1, no tag).
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 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 PbWriter::int64(self : PbWriter, field : Int, v : Int64) -> Unit
Write an int64 field.
fn PbWriter::uint32(self : PbWriter, field : Int, v : UInt) -> Unit
Write a uint32 field.
fn PbWriter::uint64(self : PbWriter, field : Int, v : UInt64) -> Unit
Write a uint64 field.
fn PbWriter::sint32(self : PbWriter, field : Int, v : Int) -> Unit
Write a sint32 field (zigzag-encoded so small-magnitude negatives stay short).
fn PbWriter::sint64(self : PbWriter, field : Int, v : Int64) -> Unit
Write a sint64 field (zigzag-encoded).
fn PbWriter::bool_(self : PbWriter, field : Int, v : Bool) -> Unit
Write a bool field.
fn PbWriter::enum_(self : PbWriter, field : Int, v : Int) -> Unit
Write an enum field (its integer value, as a varint).
fn PbWriter::fixed32(self : PbWriter, field : Int, v : UInt) -> Unit
Write a fixed32/sfixed32/float field.
fn PbWriter::fixed64(self : PbWriter, field : Int, v : UInt64) -> Unit
Write a fixed64/sfixed64/double field.
fn PbWriter::bytes_(self : PbWriter, field : Int, v : Bytes) -> Unit
Write a bytes field.
fn PbWriter::string_(self : PbWriter, field : Int, v : String) -> Unit
Write a string field (UTF-8 encoded).
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 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 PbReader::new(data : Bytes) -> PbReader
A reader positioned at the start of data.
fn PbReader::eof(self : PbReader) -> Bool
Whether the whole message has been consumed.
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 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 PbReader::read_fixed32(self : PbReader) -> UInt raise PbError
Read a little-endian 32-bit fixed value.
fn PbReader::read_fixed64(self : PbReader) -> UInt64 raise PbError
Read a little-endian 64-bit fixed value.
fn PbReader::read_len_delim(self : PbReader) -> Bytes raise PbError
Read a length-delimited body's raw bytes.
fn PbReader::read_int32(self : PbReader) -> Int raise PbError
Read an int32 field body (the low 32 bits of the varint).
fn PbReader::read_int64(self : PbReader) -> Int64 raise PbError
Read an int64 field body.
fn PbReader::read_uint32(self : PbReader) -> UInt raise PbError
Read a uint32 field body.
fn PbReader::read_uint64(self : PbReader) -> UInt64 raise PbError
Read a uint64 field body.
fn PbReader::read_sint32(self : PbReader) -> Int raise PbError
Read a sint32 field body (zigzag-decoded).
fn PbReader::read_sint64(self : PbReader) -> Int64 raise PbError
Read a sint64 field body (zigzag-decoded).
fn PbReader::read_bool(self : PbReader) -> Bool raise PbError
Read a bool field body.
fn PbReader::read_bytes(self : PbReader) -> Bytes raise PbError
Read a bytes field body.
fn PbReader::read_string(self : PbReader) -> String raise PbError
Read a string field body, decoding UTF-8. Raises BadUtf8 on invalid bytes.
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 FieldType
The FieldDescriptorProto.Type enum (proto3's scalar and composite field types). TypeGroup is intentionally absent — groups are removed from proto3.
fn FieldType::code(self : FieldType) -> Int
The descriptor.proto enum number of a field type.
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 FieldLabel
The FieldDescriptorProto.Label: proto3 fields are LabelOptional unless repeated.
fn FieldLabel::code(self : FieldLabel) -> Int
The descriptor.proto enum number of a label.
fn FieldLabel::from_code(n : Int) -> FieldLabel
The label for a descriptor.proto enum number; anything else reads as LabelOptional.
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 FieldDescriptor::scalar( name : String, number : Int, type_ : FieldType, label? : FieldLabel = LabelOptional) -> FieldDescriptor
A field with a scalar type and no composite type_name.
fn FieldDescriptor::encode(self : FieldDescriptor) -> Bytes
Encode a FieldDescriptorProto.
fn FieldDescriptor::decode(body : Bytes) -> FieldDescriptor raise PbError
Decode a FieldDescriptorProto.
struct MessageDescriptor
A message type (DescriptorProto): its (simple) name and its fields.
fn MessageDescriptor::encode(self : MessageDescriptor) -> Bytes
Encode a DescriptorProto.
fn MessageDescriptor::decode( body : Bytes) -> MessageDescriptor raise PbError
Decode a DescriptorProto.
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 MethodDescriptor::encode(self : MethodDescriptor) -> Bytes
Encode a MethodDescriptorProto.
fn MethodDescriptor::decode(body : Bytes) -> MethodDescriptor raise PbError
Decode a MethodDescriptorProto.
struct ServiceDescriptor
A service (ServiceDescriptorProto): its (simple) name and its methods.
fn ServiceDescriptor::encode(self : ServiceDescriptor) -> Bytes
Encode a ServiceDescriptorProto.
fn ServiceDescriptor::decode( body : Bytes) -> ServiceDescriptor raise PbError
Decode a ServiceDescriptorProto.
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 FileDescriptor::new( name : String, package_ : String, messages? : Array[MessageDescriptor] = [], services? : Array[ServiceDescriptor] = []) -> FileDescriptor
A proto3 file with the given filename and package.
fn FileDescriptor::encode(self : FileDescriptor) -> Bytes
Encode a FileDescriptorProto.
fn FileDescriptor::decode(body : Bytes) -> FileDescriptor raise PbError
Decode a FileDescriptorProto.
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 FileDescriptor::service_names(self : FileDescriptor) -> Array[String]
The fully-qualified names of the services this file defines (package.Service).
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 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 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 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 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 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 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 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 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).
suberror HpackErrorA raised HPACK failure (Huffman decoding or header-block decoding).
impl Show for HpackError with fn output(self, logger)
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 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 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 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 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 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 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 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 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 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 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 DynamicTable::count(self : DynamicTable) -> Int
The number of entries currently in the dynamic table.
fn DynamicTable::current_size(self : DynamicTable) -> Int
The current total size of the dynamic table in octets (§4.1 accounting).
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 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 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 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 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 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 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 frame_data : Int = 0x0
HTTP/2 frame type codes (RFC 7540 §6).
let frame_headers : Int = 0x1
let frame_priority : Int = 0x2
let frame_rst_stream : Int = 0x3
let frame_settings : Int = 0x4
let frame_push_promise : Int = 0x5
let frame_ping : Int = 0x6
let frame_goaway : Int = 0x7
let frame_window_update : Int = 0x8
let frame_continuation : Int = 0x9
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 flag_ack : Int = 0x1
ACK on SETTINGS and PING shares bit 0x1 with END_STREAM.
let flag_end_headers : Int = 0x4
let flag_padded : Int = 0x8
let flag_priority : Int = 0x20
let settings_header_table_size : Int = 0x1
SETTINGS parameter identifiers (RFC 7540 §6.5.2).
let settings_enable_push : Int = 0x2
let settings_max_concurrent_streams : Int = 0x3
let settings_initial_window_size : Int = 0x4
let settings_max_frame_size : Int = 0x5
let settings_max_header_list_size : Int = 0x6
let error_no_error : Int = 0x0
HTTP/2 error codes (RFC 7540 §7), carried by RST_STREAM and GOAWAY.
let error_protocol_error : Int = 0x1
let error_internal_error : Int = 0x2
let error_flow_control_error : Int = 0x3
let error_settings_timeout : Int = 0x4
let error_stream_closed : Int = 0x5
let error_frame_size_error : Int = 0x6
let error_refused_stream : Int = 0x7
let error_cancel : Int = 0x8
let error_compression_error : Int = 0x9
let error_connect_error : Int = 0xA
let error_enhance_your_calm : Int = 0xB
let error_inadequate_security : Int = 0xC
let error_http_1_1_required : Int = 0xD
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).
suberror FrameErrorA 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.
impl Show for FrameError with fn output(self, logger)
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 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 FrameHeader::encode(self : FrameHeader) -> Bytes
Encode a FrameHeader to its 9 wire octets.
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 Frame::frame_type(self : Frame) -> Int
The numeric frame-type code of this frame (RFC 7540 §6).
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 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 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 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 StreamState
The lifecycle state of a single HTTP/2 stream (RFC 7540 §5.1).
impl Show for StreamState with fn output(self, logger)
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.
suberror StreamErrorAn illegal stream transition (RFC 7540 §5.1): a frame not permitted in the current state (typically a STREAM_CLOSED or PROTOCOL_ERROR condition).
impl Show for StreamError with fn output(self, logger)
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 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 Stream
A mutable stream: its identifier and current lifecycle state. send/recv advance the state in place, raising on an illegal transition.
fn Stream::new(id : Int) -> Stream
A fresh idle stream with the given identifier.
fn Stream::send( self : Stream, ev : StreamEvent) -> StreamState raise StreamError
Advance this stream by sending ev, returning the new state.
fn Stream::recv( self : Stream, ev : StreamEvent) -> StreamState raise StreamError
Advance this stream by receiving ev, returning the new state.
fn stream_is_client_initiated(id : Int) -> Bool
Whether id is a client-initiated stream: a non-zero odd identifier.
fn stream_is_server_initiated(id : Int) -> Bool
Whether id is a server-initiated (pushed) stream: a non-zero even identifier.
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 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 default_max_frame_size : Int = 16384
The HTTP/2 default (and minimum) SETTINGS_MAX_FRAME_SIZE (RFC 7540 §6.5.2).
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 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 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 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 H2Server::register_handler( self : H2Server, path : String, handler : Handler) -> Unit
Register a handler of any of the four gRPC call kinds.
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 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 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 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 H2Server::goaway_received(self : H2Server) -> Bool
Whether the peer has sent GOAWAY; the driver stops accepting new streams once this is set.
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 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 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 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 RpcContext::empty() -> RpcContext
A context with no path, metadata, or deadline — the placeholder a stream holds until its request HEADERS are decoded.
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 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 RpcContext::add_trailer( self : RpcContext, name : Bytes, value : Bytes) -> Unit
Add a trailing-metadata header, sent in the trailer HEADERS alongside grpc-status.
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 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 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 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 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 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 H2Client::new() -> H2Client
A fresh client engine with no live calls. The first call takes stream id 1.
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 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 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 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 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 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 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 H2Client::reply(self : H2Client, id : Int) -> CallReply
The completed result of a call. Meaningful once is_done is true.
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 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 StreamInterceptor = (
A server-streaming interceptor: (ctx, request, next) -> replies. It can pre-process the request, post-process the reply sequence, or short-circuit.
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 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 ServingStatus
The grpc.health.v1.HealthCheckResponse.ServingStatus enum.
fn ServingStatus::code(self : ServingStatus) -> Int
The wire value of a serving status (the protobuf enum number).
fn ServingStatus::from_code(n : Int) -> ServingStatus
The serving status for a protobuf enum number; out-of-range numbers read as StatusUnknown.
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 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 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 decode_health_response(msg : Bytes) -> ServingStatus
Decode the status field of a HealthCheckResponse; an absent field reads as the 0 default (StatusUnknown).
let health_check_path : String = "/grpc.health.v1.Health/Check"
The gRPC HTTP/2 path of the Check method.
let health_watch_path : String = "/grpc.health.v1.Health/Watch"
The gRPC HTTP/2 path of the Watch method.
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 HealthService::new() -> HealthService
A health service reporting the overall server as SERVING.
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 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 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 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 reflection_v1_path : String = "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"
The gRPC HTTP/2 path of the v1 reflection stream.
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 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 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 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 ReflectionResponse
A decoded ServerReflectionResponse, reduced to the one message_response arm.
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 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 ReflectionService::new() -> ReflectionService
An empty reflection service. Add the descriptors of the services you serve with add_file.
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 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 ReflectionService::handler(self : ReflectionService) -> Handler
The bidi handler backing ServerReflectionInfo: one response per request, none at half-close.
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 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 GrpcServer::new() -> GrpcServer
A gRPC server with no registered methods.
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 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 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 GrpcServer::register_bidi( self : GrpcServer, path : String, factory : (@moonrpc.RpcContext) -> @moonrpc.BidiHandler) -> Unit
Register a bidirectional-streaming handler.
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.
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 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.
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 Channel::close(self : Channel) -> Unit
Close the underlying connection. Outstanding calls are abandoned.
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.
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.
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 GrpcServer::register_health( self : GrpcServer, health : @moonrpc.HealthService) -> Unit
Register the grpc.health.v1.Health service (Check + Watch) on this server.