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.

let
let max_message_size : Int = 4 * 1024 * 1024

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

let
let max_header_list_size : Int = 128 * 1024

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

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

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

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

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

enum
enum Status

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

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

The numeric grpc-status code.

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

The canonical uppercase status name.

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

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

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

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

struct
struct Method

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

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

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

§Protobuf wire runtime

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

enum
enum WireType

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

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

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

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

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

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

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

item
suberror PbError

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

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

Prints the field and offset the decode failed on.

struct
struct PbWriter

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

fn
fn PbWriter::new() -> PbWriter

A fresh, empty message encoder.

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

The bytes written so far.

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

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

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

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

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

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

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

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

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

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

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

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

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

Write an int64 field.

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

Write a uint32 field.

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

Write a uint64 field.

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

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

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

Write a sint64 field (zigzag-encoded).

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

Write a bool field.

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

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

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

Write a fixed32/sfixed32/float field.

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

Write a fixed64/sfixed64/double field.

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

Write a bytes field.

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

Write a string field (UTF-8 encoded).

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

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

struct
struct PbReader

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

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

A reader positioned at the start of data.

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

Whether the whole message has been consumed.

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

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

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

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

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

Read a little-endian 32-bit fixed value.

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

Read a little-endian 64-bit fixed value.

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

Read a length-delimited body's raw bytes.

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

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

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

Read an int64 field body.

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

Read a uint32 field body.

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

Read a uint64 field body.

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

Read a sint32 field body (zigzag-decoded).

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

Read a sint64 field body (zigzag-decoded).

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

Read a bool field body.

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

Read a bytes field body.

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

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

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

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

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

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

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

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

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

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

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

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

§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) raise HpackError

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) raise HpackError

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)

Prints the header block and the offset it gave up at.

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

HEADERS: opens a stream and carries its HPACK header block.

let
let frame_priority : Int = 0x2

PRIORITY: the deprecated stream-dependency hint; accepted and ignored.

let
let frame_rst_stream : Int = 0x3

RST_STREAM: abort one stream without touching the connection.

let
let frame_settings : Int = 0x4

SETTINGS: the connection parameters each peer announces, and their ack.

let
let frame_push_promise : Int = 0x5

PUSH_PROMISE: server push, which gRPC never uses.

let
let frame_ping : Int = 0x6

PING: the round-trip probe keepalive is built on.

let
let frame_goaway : Int = 0x7

GOAWAY: stop opening streams, and here is the last id I accepted.

let
let frame_window_update : Int = 0x8

WINDOW_UPDATE: hand the peer more flow-control credit.

let
let frame_continuation : Int = 0x9

CONTINUATION: the rest of a header block too large for one frame.

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

END_HEADERS: no CONTINUATION follows this header block.

let
let flag_padded : Int = 0x8

PADDED: a length byte and that many padding bytes wrap the payload.

let
let flag_priority : Int = 0x20

PRIORITY: this HEADERS frame carries a stream-dependency block.

let
let settings_header_table_size : Int = 0x1

SETTINGS parameter identifiers (RFC 7540 §6.5.2).

let
let settings_enable_push : Int = 0x2

SETTINGS_ENABLE_PUSH: whether the peer may send PUSH_PROMISE.

let
let settings_max_concurrent_streams : Int = 0x3

SETTINGS_MAX_CONCURRENT_STREAMS: how many streams may be open at once.

let
let settings_initial_window_size : Int = 0x4

SETTINGS_INITIAL_WINDOW_SIZE: the flow-control credit a new stream starts with.

let
let settings_max_frame_size : Int = 0x5

SETTINGS_MAX_FRAME_SIZE: the largest payload the peer will accept in one frame.

let
let settings_max_header_list_size : Int = 0x6

SETTINGS_MAX_HEADER_LIST_SIZE: the advisory ceiling on a decoded header list.

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

PROTOCOL_ERROR: the peer broke the protocol in a way not covered below.

let
let error_internal_error : Int = 0x2

INTERNAL_ERROR: the endpoint failed for reasons of its own.

let
let error_flow_control_error : Int = 0x3

FLOW_CONTROL_ERROR: the peer sent more than its credit allowed.

let
let error_settings_timeout : Int = 0x4

SETTINGS_TIMEOUT: a SETTINGS frame went unacknowledged too long.

let
let error_stream_closed : Int = 0x5

STREAM_CLOSED: a frame arrived for a stream that was already finished.

let
let error_frame_size_error : Int = 0x6

FRAME_SIZE_ERROR: the frame's length is invalid for its type.

let
let error_refused_stream : Int = 0x7

REFUSED_STREAM: the stream was declined before any processing, so it is safe to retry.

let
let error_cancel : Int = 0x8

CANCEL: the stream is no longer wanted.

let
let error_compression_error : Int = 0x9

COMPRESSION_ERROR: the HPACK context is corrupt, which kills the connection.

let
let error_connect_error : Int = 0xA

CONNECT_ERROR: the TCP connection behind a CONNECT stream failed.

let
let error_enhance_your_calm : Int = 0xB

ENHANCE_YOUR_CALM: the peer is generating too much load.

let
let error_inadequate_security : Int = 0xC

INADEQUATE_SECURITY: the transport does not meet the minimum this endpoint requires.

let
let error_http_1_1_required : Int = 0xD

HTTP_1_1_REQUIRED: the request cannot be served over HTTP/2.

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)

Frame errors print as the wire fault they are, so a log line names the frame and the field that was wrong.

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)

Prints the RFC 7540 §5.1 state name.

enum
enum StreamEvent

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

item
suberror StreamError

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

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

Prints which transition was refused.

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

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

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

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

struct
struct Stream

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

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

A fresh idle stream with the given identifier.

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

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

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

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

fn
fn stream_is_client_initiated(id : Int) -> Bool

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

fn
fn stream_is_server_initiated(id : Int) -> Bool

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

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

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

§gRPC server engine

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

let
let default_window_size : Int = 65535

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

let
let default_max_frame_size : Int = 16384

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

let
let default_max_streams : Int = 100

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

struct
struct SrvStream

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

struct
struct H2Server

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

fn
fn H2Server::new() -> H2Server

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

§Streaming call kinds & context

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

struct
struct RpcContext

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

fn
fn RpcContext::empty() -> RpcContext

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

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

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

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

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

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

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

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

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

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

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

struct
struct BidiHandler

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

enum
enum Handler

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

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

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

§gRPC client engine

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

struct
struct ClientCall

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

struct
struct CallReply

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

struct
struct H2Client

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

fn
fn H2Client::new() -> H2Client

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

fn
fn H2Client::preface(self : H2Client) -> Array[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::release(self : H2Client, id : Int) -> Unit

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

fn
fn encode_grpc_timeout(millis : Int) -> Bytes

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

fn
fn percent_decode(v : Bytes) -> String

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

§Server interceptors

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

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

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

type
type StreamInterceptor = (

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

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

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

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

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

§Health service

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

enum
enum ServingStatus

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

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

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

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

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

fn
fn encode_health_request(service : Bytes) -> Bytes

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

fn
fn decode_health_request(msg : Bytes) -> Bytes

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

fn
fn encode_health_response(status : ServingStatus) -> Bytes

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

fn
fn decode_health_response(msg : Bytes) -> ServingStatus

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

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

The gRPC HTTP/2 path of the Check method.

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

The gRPC HTTP/2 path of the Watch method.

struct
struct HealthService

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

fn
fn HealthService::new() -> HealthService

A health service reporting the overall server as SERVING.

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

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

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

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

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

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

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

Register Check and Watch on a pure server engine.

§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_unary( self : GrpcServer, path : String, handler : (@moonrpc.RpcContext, Bytes) -> Bytes) -> Unit

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

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

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

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

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

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

Register a bidirectional-streaming handler.

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

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

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 or graceful_stop drains the server. Each connection is driven by drive; a failure on one connection is isolated and does not stop the server.

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

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

§Channel client (native)

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

struct
struct Channel

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

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

Open a channel to host:port: connect, send the 24-octet connection 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, retry? : @moonrpc.RetryPolicy? = None) -> @moonrpc.CallReply

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

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

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

item
async fn Channel::client_streaming( self : Channel, path : String, requests : Array[Bytes], metadata? : Array[@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.

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

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

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

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

§Call policy

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

struct
struct RetryPolicy

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

fn
fn RetryPolicy::default() -> RetryPolicy

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

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

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

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

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

struct
struct HedgingPolicy

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

fn
fn HedgingPolicy::default() -> HedgingPolicy

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

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

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

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

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

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

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

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

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

struct
struct Keepalive

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

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

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

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

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

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

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

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

Record that a keepalive PING was sent at now.

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

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

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

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

§Load balancing and naming

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

enum
enum LbPolicy

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

struct
struct LoadBalancer

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

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

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

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

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

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

The number of addresses the balancer is choosing among.

enum
enum ConnectivityState

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

struct
struct SubConn

One address the Channel dials, tracking its connectivity state.

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

A fresh sub-connection to address, starting IDLE.

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

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

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

The connection attempt succeeded (CONNECTING → READY).

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

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

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

A READY connection went idle (READY → IDLE).

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

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

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

Whether this sub-connection can carry RPCs right now.

let
let dns_type_a : Int = 1

The DNS record type for an IPv4 address.

let
let dns_type_aaaa : Int = 28

The DNS record type for an IPv6 address.

struct
struct DnsRecord

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

struct
struct DnsResponse

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

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

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

fn
fn dns_decode_response(msg : Bytes) -> DnsResponse

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

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

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

struct
struct ConnectionPool

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

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

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

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

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

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

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

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

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

§Channels over a socket

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

struct
struct MuxChannel

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

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

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

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

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

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

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

struct
struct BidiStream

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

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

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

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

Send one request message on the stream.

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

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

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

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

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

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

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

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

struct
struct ManagedChannel

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

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

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

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

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

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

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

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

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

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

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

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

Make a server-streaming call on a picked backend.

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

Make a client-streaming call on a picked backend.

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

Make a bidirectional-streaming call on a picked backend.

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

Close every live backend connection.

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

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

§Codecs

The gzip message compression gRPC negotiates and the base64 used by binary metadata, written here so the wire format needs no native binding.

item
suberror GzError

A gzip / DEFLATE decode failure: a malformed member, an unsupported feature, or a checksum mismatch.

fn
fn gunzip(data : Bytes) -> Bytes raise GzError

Decode a single gzip member (RFC 1952): validate the 10-byte header, skip the optional EXTRA / NAME / COMMENT / HCRC fields, inflate the DEFLATE body, and verify the trailing CRC-32 and ISIZE. Raises [GzError] on any malformation.

fn
fn base64_encode(data : Bytes) -> Bytes

Standard base64 encode with = padding.

fn
fn base64_decode(data : Bytes) -> Bytes

Standard base64 decode. Non-alphabet bytes (padding, whitespace) are skipped, so both padded and unpadded input decode.

fn
fn is_binary_metadata(name : Bytes) -> Bool

Whether a metadata key names binary content: it ends in -bin (gRPC's convention for base64-on-the-wire values).

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

Decode a metadata value coming off the wire: base64-decoded for a -bin key, verbatim otherwise.

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

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