moonrpc

moonrpc — a real gRPC implementation for MoonBit (← gRPC). v0.8.1 adds Channel::stream_take, which drives a streaming call inline — no background reader — and returns once it has collected a bounded number of reply messages, so a caller can consume the first N of an unbounded subscription (etcd's Watch, a long server-streaming feed) and close, keeping the whole exchange in one task. v0.8.0 completes the client channel stack. Load balancing picks a READY sub-connection per call (pick_first or round_robin) from a connection pool that tracks each address through the gRPC connectivity states (IDLE/CONNECTING/READY/TRANSIENT_FAILURE/SHUTDOWN); a ManagedChannel dials several backends over real sockets and routes calls through the picker. Name resolution self-builds a DNS message codec (RFC 1035, A/AAAA with name compression) and a UDP resolver, so dns_connect turns a hostname into the backends it dials. Retry and hedging policies drive re-issue and parallel attempts — hedging fires a fresh attempt every hedging delay and commits on the first fatal status, cancelling the rest. Client keepalive pings an idle connection and closes it on a missed ACK. A multiplexed channel (MuxChannel) carries many concurrent calls over one connection through a shared read pump with per-stream inboxes and serialized writes, drives keepalive, and supports interactive bidirectional streaming — send while receiving, replies read message by message. v0.7.0 adds per-message gzip: a self-built DEFLATE (RFC 1951, stored/fixed/dynamic-Huffman) and gzip (RFC 1952, CRC-32 + ISIZE) reader that decompresses gzip requests on the server and gzip responses on the client, and client-side retry with exponential backoff bounded by the call deadline. It also hardens every decoder against malformed input: the gRPC length prefix, HPACK integer/string, protobuf, and health decoders now reject an out-of-range or overflowing length instead of aborting; flow-control windows saturate rather than wrap; peer SETTINGS are validated; the client reassembles a response header block across CONTINUATION; and header blocks and message sizes are capped. v0.6.1 self-builds a pure protobuf wire runtime (PbWriter/PbReader for varint, zigzag, fixed32/64, and length-delimited fields) and Server Reflection: a descriptor model and the grpc.reflection.v1.ServerReflection service (with its v1alpha alias) answering ListServices, FileContainingSymbol, and FileByFilename. v0.6 added the client half — a real Channel, a long-lived multiplexed h2c connection over @socket.Tcp driven by a pure H2Client engine symmetric to the server — plus the grpc.health.v1.Health service, server interceptors, -bin metadata, google.rpc.Status rich errors, and client-side grpc-timeout enforcement. The server engine (H2Server) serves all four call kinds over the self-built HTTP/2 (h2c) transport, driving the RFC 7540 frame layer, the stream state machine, complete HPACK (RFC 7541, with Huffman + dynamic table), and connection- and stream-level flow control. Includes the shared length-prefixed framing and the 17-code gRPC status model.

grpc
rpc
protobuf
http2
moonbit
moon add Lfan-ke/moonrpc@0.8.1
Download zip
Author
Version
0.8.1
License
Apache-2.0
Last updated
16 days ago
Downloads
412

Dependencies

README

#moonrpc

A real gRPC implementation for MoonBit — ← gRPC.

Check and Test License mooncakes

moonrpc targets real gRPC — not gRPC-Web. Where the MoonBit ecosystem lacks the primitives, we build them: the north star is a self-built HTTP/2 (RFC 7540) framing layer with stream multiplexing and HPACK (RFC 7541), carrying application/grpc+proto over h2c.

v0.5 serves all four gRPC call kinds — unary, server-, client-, and bidirectional-streaming — over the self-built HTTP/2 (h2c) transport. A pure, all-backend protocol engine (H2Server) drives the frame layer, the stream state machine, complete HPACK, and connection- and stream-level flow control honoured across the whole multi-message exchange; a native moonbitlang/async socket driver (GrpcServer) pumps the bytes:

let server = @net.GrpcServer::new()

// unary: one request, one reply.
server.register("/greet.Greeter/SayHello", req => handle(req))

// server-streaming: one request, an ordered run of replies.
server.register_server_streaming("/count.C/Up", (ctx, req) => [
first(req), second(req), third(req),
])

// client-streaming: many requests collected, one reply at half-close.
server.register_client_streaming("/sum.S/Add", (ctx, msgs) => fold(msgs))

// bidi: each request echoed as it arrives, a farewell at half-close.
server.register_bidi("/chat.C/Echo", ctx => @moonrpc.BidiHandler::{
on_message: m => [reply_to(m)],
on_end: () => [b"bye"],
})

server.serve(port=50051) // a real gRPC / in-process client gets the replies

The handler sees the call's RpcContext: the request metadata, the grpc-timeout deadline in milliseconds, and slots for response initial and trailing metadata.

#The Channel client

A Channel is one long-lived h2c connection; every call multiplexes over it on its own client-allocated stream id, sharing the connection's HPACK and flow-control state.

let chan = @net.Channel::connect("127.0.0.1", 50051)

// unary.
let reply = chan.unary("/greet.Greeter/SayHello", request)
reply.messages[0] // the reply message; reply.grpc_status is 0 on success

// server-streaming: one request, every framed reply reassembled in order.
let out = chan.server_streaming("/count.C/Up", request) // out.messages

// client-streaming: many requests, one reply.
let sum = chan.client_streaming("/sum.S/Add", [a, b, c])

// a deadline: sent as grpc-timeout and enforced locally — if it elapses the
// stream is reset and grpc_status comes back DEADLINE_EXCEEDED (4).
let bounded = chan.unary("/slow.S/Wait", request, timeout_millis=Some(200))

Under the Channel, H2Client is the same kind of pure, transport-independent engine as the server: it produces the request frames and consumes the response frames, so the whole client path is exercised in-memory on every backend against H2Server, and only the socket driver is native.

#Health and interceptors

The standard grpc.health.v1.Health service registers in one call, and server interceptors wrap every handler:

let health = @moonrpc.HealthService::new()
health.set_status("greet.Greeter", @moonrpc.Serving)
server.register_health(health) // Check + Watch on /grpc.health.v1.Health/*

engine.add_unary_interceptor((ctx, req, next) => {
// inspect, then proceed — or return without calling next to short-circuit.
next(ctx, req)
})

Under the hood, everything below serve is a pure, transport-independent enginefeed turns a stream of decoded frames into the frames to write back, so the whole server path (HPACK, flow control, dispatch, HEADERS + DATA + grpc-status trailers) is exercised in-memory on every backend; only the socket driver is native.

let engine = @moonrpc.H2Server::new()
engine.register("/echo.Echo/Say", req => req)
let out = engine.feed(frame) // -> [HEADERS(:status 200), DATA(reply), HEADERS(grpc-status:0)]

It builds on the load-bearing transport primitives, all verified across every backend:

#The gRPC message framing

let frame = @moonrpc.encode_message(b"hello grpc")
// [0x00][0x00 0x00 0x00 0x0A]["hello grpc"] — 1-byte flag + 4-byte big-endian length + payload

let (compressed, payload) = @moonrpc.decode_message(frame).unwrap()
// (false, b"hello grpc")

This is the Length-Prefixed-Message framing shared by gRPC-Web (over HTTP/1.1) and real gRPC (over HTTP/2), so the transport can be swapped underneath it without touching the codec.

#The HTTP/2 frame layer (RFC 7540)

All ten frame types encode to and decode from their exact wire bytes — a byte-level, exhaustively round-trippable codec that the multiplexer will layer on top without touching:

let f = @moonrpc.Frame::Headers(
stream_id=1, fragment=block, end_stream=true, end_headers=true,
priority=None, padding=0,
)
let bytes = f.encode() // 9-octet header + payload
let (frame, consumed) = @moonrpc.decode_frame(bytes) // raises Incomplete until whole

@moonrpc.has_connection_preface(buf) // PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n (§3.5)

The stream state machine drives the §5.1 lifecycle (idle / open / half-closed / closed) off END_STREAM / push / reset, and rejects illegal transitions:

let s = @moonrpc.Stream::new(1)
s.send(@moonrpc.StreamEvent::Headers(end_stream=false)) // -> Open
s.recv(@moonrpc.StreamEvent::Data(end_stream=true)) // -> HalfClosedRemote

#Complete HPACK (RFC 7541)

The full header-compression stack: the 61-entry static table, prefix integers, Huffman coding (Appendix B), and the size-bounded dynamic table with eviction — driven by a stateful encoder/decoder pair over the six field representations. Verified byte-for-byte against the Appendix C worked examples.

let enc = @moonrpc.HpackEncoder::new(huffman=true)
let block = enc.encode([{ name: b":method", value: b"GET" },
{ name: b":authority", value: b"www.example.com" }])

let dec = @moonrpc.HpackDecoder::new()
let headers = dec.decode(block) // back to the same header list

@moonrpc.huffman_encode(b"www.example.com") // f1e3 c2e5 f23a 6ba0 ab90 f4ff — §C.4.1

#The status model

@moonrpc.Status::code(NotFound) // 5
@moonrpc.Status::name(Unauthenticated) // "UNAUTHENTICATED" (all 17 canonical grpc-status codes)

let m : @moonrpc.Method = { service: "greet.Greeter", name: "SayHello" }
m.path() // "/greet.Greeter/SayHello"

#Roadmap (the self-built stack, sequenced to completeness)

v0 = framing + status + method descriptors; v0.2 = the first HPACK primitives (static table + integer representation + non-Huffman string literals). v0.3 self-builds the load-bearing transport primitives: the HTTP/2 frame layer (all ten types — DATA / HEADERS / PRIORITY / RST_STREAM / SETTINGS / PUSH_PROMISE / PING / GOAWAY / WINDOW_UPDATE / CONTINUATION — with flags and payloads), the connection preface, the stream state machine (§5.1 + §5.1.1 id rules), and complete HPACK (Huffman coding + the dynamic table with eviction + the stateful encoder/decoder).

v0.4 makes the server actually run: the H2Server engine reads frames off a connection, demultiplexes by stream id, drives the per-stream state machine, exchanges SETTINGS, and honours connection- and stream-level flow-control windows with WINDOW_UPDATE; it receives a request stream, dispatches to a registered (Bytes) -> Bytes handler, and responds with HEADERS (:status 200, grpc-encoding) + DATA + trailer HEADERS (grpc-status). The native GrpcServer binds this engine to real moonbitlang/async TCP sockets, proven by an in-process h2c client that makes a unary call end-to-end. Note: the async TLS layer exposes no ALPN, so h2 runs via h2c (prior-knowledge) until an ALPN hook lands upstream.

v0.5 adds the streaming modes on the same engine. A method is registered as one of four cardinalities — unary, server-streaming ((ctx, req) -> [reply]), client-streaming ((ctx, [req]) -> reply), or bidi (a BidiHandler whose on_message fires per request message and whose on_end fires at half-close). The engine reassembles length-prefixed messages out of the DATA stream, routes each to the handler, and frames every produced message as its own length-prefixed DATA — flow control is honoured across the whole multi-message exchange, so a reply larger than the window splits and resumes on WINDOW_UPDATE. Request metadata and the grpc-timeout deadline are parsed and surfaced to the handler, which can set response initial and trailing metadata. Two in-process h2c clients — a server-streaming call receiving multiple framed replies in order and a client-streaming call sending many messages for one reply — prove it end-to-end, mutation-verified against the stream terminator and the flow-control windows.

v0.6 adds the client half and the gRPC cross-cutting services. A real Channel — a long-lived, multiplexed h2c connection over a real @socket.Tcp, driven by a pure H2Client engine symmetric to the server — opens streams (client-allocated odd ids), HPACK-encodes request HEADERS, frames request DATA under the send windows, and reassembles the reply. It performs unary, server- and client-streaming calls against a real GrpcServer over an actual socket. The grpc.health.v1.Health service (Check + Watch) ships with a hand-coded protobuf codec for its two messages. Server-side unary and stream interceptors wrap a handler in an outermost-first chain that can rewrite the request, post-process the reply, or short-circuit. The grpc-timeout deadline is now enforced client-side: the Channel races the read loop against the timer and, when it elapses, resets the stream and surfaces DEADLINE_EXCEEDED.

Next: Server Reflection (so grpcurl can list/describe), the full protobuf message runtime, -bin metadata round-trip, rich errors (google.rpc.Status), per-message gzip, and channelz; plus DNS/load-balancing and retry on the Channel.

#License

Apache-2.0.

#
StreamInterceptor

type StreamInterceptor = (RpcContext, Bytes, (RpcContext, Bytes) -> Array[Bytes]) -> Array[Bytes]

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

#
UnaryInterceptor

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.

#
FrameError

pub suberror FrameError {
Incomplete
FrameSizeError(String)
ProtocolError(String)
} derive(Eq)

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.
impl Show for FrameError

#
HpackError

pub suberror HpackError {
HuffmanError(String)
HpackDecodeError(String)
} derive(Eq)

A raised HPACK failure (Huffman decoding or header-block decoding).
impl Show for HpackError

#
StreamError

pub suberror StreamError {
InvalidTransition(String)
} derive(Eq)

An 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

#
BidiHandler

pub(all) struct BidiHandler {
on_message : (Bytes) -> Array[Bytes]
on_end : () -> Array[Bytes]
}

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.

#
CallReply

pub(all) struct CallReply {
status : Bytes
grpc_status : Int
headers : Array[Header]
messages : Array[Bytes]
trailers : Array[Header]
}

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.

#
ClientCall

pub(all) struct ClientCall {
id : Int
path : Bytes
authority : Bytes
metadata : Array[Header]
timeout : Bytes?
out : Bytes
out_off : Int
req_ended : Bool
req_headers_sent : Bool
req_fin_sent : Bool
send_window : Int
data :
Buffer

data_off : Int
messages : Array[Bytes]
recv_window : Int
status : Bytes
grpc_status : Int?
resp_headers : Array[Header]
resp_trailers : Array[Header]
headers_seen : Bool
done : Bool
}

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

#
DynamicTable

pub(all) struct DynamicTable {
entries : Array[(Bytes, Bytes)]
size : Int
max_size : Int
}

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

#
DynamicTable::add

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

#
DynamicTable::count

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

The number of entries currently in the dynamic table.

#
DynamicTable::current_size

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

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

#
DynamicTable::new

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

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

#
DynamicTable::set_max_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.

#
Frame

pub(all) enum Frame {
Data(stream_id~ : Int, data~ : Bytes, end_stream~ : Bool, padding~ : Int)
Headers(stream_id~ : Int, fragment~ : Bytes, end_stream~ : Bool, end_headers~ : Bool, priority~ : Priority?, padding~ : Int)
Priority(stream_id~ : Int, priority~ : Priority)
RstStream(stream_id~ : Int, error_code~ : Int)
Settings(params~ : Array[(Int, Int)], ack~ : Bool)
PushPromise(stream_id~ : Int, promised_id~ : Int, fragment~ : Bytes, end_headers~ : Bool, padding~ : Int)
Ping(payload~ : Bytes, ack~ : Bool)
GoAway(last_stream_id~ : Int, error_code~ : Int, debug~ : Bytes)
WindowUpdate(stream_id~ : Int, increment~ : Int)
Continuation(stream_id~ : Int, fragment~ : Bytes, end_headers~ : Bool)
Unknown(ftype~ : Int, flags~ : Int, stream_id~ : Int, payload~ : Bytes)
} derive(Eq)

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

#
Frame::encode

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

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

#
Frame::frame_type

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

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

#
FrameHeader

pub(all) struct FrameHeader {
length : Int
ftype : Int
flags : Int
stream_id : Int
} derive(Eq)

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.

#
FrameHeader::encode

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

Encode a FrameHeader to its 9 wire octets.

#
H2Client

pub struct H2Client {
encoder : HpackEncoder
decoder : HpackDecoder
calls : Map[Int, ClientCall]
next_id : Int
conn_send_window : Int
conn_recv_window : Int
remote_initial_window : Int
remote_max_frame : Int
}

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.

#
H2Client::close_send

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.

#
H2Client::feed

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.

#
H2Client::is_done

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.

#
H2Client::new

fn H2Client::new() -> H2Client

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

#
H2Client::open

fn H2Client::open(self : H2Client, path : String, metadata? : Array[Header], authority? : String, timeout_millis? : Int?) -> Int

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

#
H2Client::preface

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.

#
H2Client::reply

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

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

#
H2Client::send

fn H2Client::send(self : H2Client, id : Int, message : Bytes, end? : Bool) -> 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.

#
H2Client::unary

fn H2Client::unary(self : H2Client, path : String, request : Bytes, metadata? : Array[Header], authority? : String, timeout_millis? : Int?) -> (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.

#
H2Server

pub struct H2Server {
handlers : Map[String, Handler]
encoder : HpackEncoder
decoder : HpackDecoder
streams : Map[Int, SrvStream]
unary_interceptors : Array[(RpcContext, Bytes, (RpcContext, Bytes) -> Bytes) -> Bytes]
stream_interceptors : Array[(RpcContext, Bytes, (RpcContext, Bytes) -> Array[Bytes]) -> Array[Bytes]]
conn_recv_window : Int
conn_send_window : Int
remote_initial_window : Int
remote_max_frame : Int
goaway_received : Bool
}

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.

#
H2Server::add_stream_interceptor

fn H2Server::add_stream_interceptor(self : H2Server, interceptor : (RpcContext, Bytes, (RpcContext, Bytes) -> Array[Bytes]) -> Array[Bytes]) -> Unit

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

#
H2Server::add_unary_interceptor

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

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

#
H2Server::feed

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.

#
H2Server::goaway_received

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

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

#
H2Server::new

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.

#
H2Server::preface

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.

#
H2Server::register

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.

#
H2Server::register_bidi

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.

#
H2Server::register_client_streaming

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.

#
H2Server::register_handler

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

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

#
H2Server::register_server_streaming

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.

#
H2Server::register_unary

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

#
H2Server::stream_state

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

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

#
Handler

pub(all) enum Handler {
Unary((RpcContext, Bytes) -> Bytes)
ServerStreaming((RpcContext, Bytes) -> Array[Bytes])
ClientStreaming((RpcContext, Array[Bytes]) -> Bytes)
Bidi((RpcContext) -> BidiHandler)
}

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.
pub(all) struct Header {
name : Bytes
value : Bytes
} derive(Eq,
Debug
)

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

#
HealthService

pub struct HealthService {
statuses : Map[String, ServingStatus]
}

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.

#
HealthService::check

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.

#
HealthService::handlers

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.

#
HealthService::install

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

Register Check and Watch on a pure server engine.

#
HealthService::new

A health service reporting the overall server as SERVING.

#
HealthService::set_status

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

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

#
HpackDecoder

pub(all) struct HpackDecoder {
table : DynamicTable
limit : Int
}

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.

#
HpackDecoder::decode

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.

#
HpackDecoder::new

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

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

#
HpackEncoder

pub(all) struct HpackEncoder {
table : DynamicTable
huffman : Bool
}

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.

#
HpackEncoder::encode

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.

#
HpackEncoder::new

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

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

#
Method

pub(all) struct Method {
service : String
name : String
}

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

#
Method::path

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

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

#
Priority

pub(all) struct Priority {
exclusive : Bool
stream_dependency : Int
weight : Int
} derive(Eq)

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

#
RpcContext

pub(all) struct RpcContext {
path : String
metadata : Array[Header]
deadline_millis : Int?
resp_headers : Array[Header]
resp_trailers : Array[Header]
}

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.

#
RpcContext::add_header

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

#
RpcContext::add_trailer

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

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

#
RpcContext::empty

fn RpcContext::empty() -> RpcContext

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

#
RpcContext::metadata_get

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.

#
ServingStatus

pub(all) enum ServingStatus {
StatusUnknown
Serving
NotServing
ServiceUnknown
} derive(Eq)

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

#
ServingStatus::code

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

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

#
ServingStatus::from_code

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

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

#
SrvStream

pub(all) struct SrvStream {
id : Int
state : StreamState
header_block :
Buffer

data :
Buffer

req_off : Int
req_msgs : Array[Bytes]
bidi_processed : Int
headers_complete : Bool
end_stream_recv : Bool
path : String
ctx : RpcContext
recv_window : Int
send_window : Int
out : Bytes
out_off : Int
started : Bool
started_response : Bool
response_ended : Bool
finalized : Bool
trailers_only : Bool
status_code : Int
headers_sent : Bool
trailers_sent : Bool
bidi : BidiHandler?
}

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.

#
Status

pub(all) enum Status {
Ok
Cancelled
Unknown
InvalidArgument
DeadlineExceeded
NotFound
AlreadyExists
PermissionDenied
ResourceExhausted
FailedPrecondition
Aborted
OutOfRange
Unimplemented
Internal
Unavailable
DataLoss
Unauthenticated
} derive(Eq)

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

#
Status::code

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

The numeric grpc-status code.

#
Status::name

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

The canonical uppercase status name.

#
Stream

pub(all) struct Stream {
id : Int
state : StreamState
}

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

#
Stream::new

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

A fresh idle stream with the given identifier.

#
Stream::recv

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

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

#
Stream::send

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

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

#
StreamEvent

pub(all) enum StreamEvent {
Headers(end_stream~ : Bool)
Data(end_stream~ : Bool)
Reserve
RstStream
} derive(Eq)

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.

#
StreamState

pub(all) enum StreamState {
Idle
ReservedLocal
ReservedRemote
Open
HalfClosedLocal
HalfClosedRemote
Closed
} derive(Eq,
Debug
)

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

#
StreamState::on_recv

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.

#
StreamState::on_send

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.

#
connection_preface

let connection_preface : Bytes

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.

#
decode_frame

fn decode_frame(data : Bytes, offset? : Int) -> (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.

#
decode_frame_header

fn decode_frame_header(data : Bytes, offset? : Int) -> 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.

#
decode_health_request

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.

#
decode_health_response

fn decode_health_response(msg : Bytes) -> ServingStatus

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

#
decode_message

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.

#
default_max_frame_size

let default_max_frame_size : Int

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

#
default_window_size

let default_window_size : Int

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

#
encode_grpc_timeout

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.

#
encode_health_request

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.

#
encode_health_response

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.

#
encode_message

fn encode_message(payload : Bytes, compressed? : Bool) -> 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).

#
error_cancel

let error_cancel : Int

#
error_compression_error

let error_compression_error : Int

#
error_connect_error

let error_connect_error : Int

#
error_enhance_your_calm

let error_enhance_your_calm : Int

#
error_flow_control_error

let error_flow_control_error : Int

#
error_frame_size_error

let error_frame_size_error : Int

#
error_http_1_1_required

let error_http_1_1_required : Int

#
error_inadequate_security

let error_inadequate_security : Int

#
error_internal_error

let error_internal_error : Int

#
error_no_error

let error_no_error : Int

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

#
error_protocol_error

let error_protocol_error : Int

#
error_refused_stream

let error_refused_stream : Int

#
error_settings_timeout

let error_settings_timeout : Int

#
error_stream_closed

let error_stream_closed : Int

#
flag_ack

let flag_ack : Int

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

#
flag_end_headers

let flag_end_headers : Int

#
flag_end_stream

let flag_end_stream : Int

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.

#
flag_padded

let flag_padded : Int

#
flag_priority

let flag_priority : Int

#
frame_continuation

let frame_continuation : Int

#
frame_data

let frame_data : Int

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

#
frame_goaway

let frame_goaway : Int

#
frame_headers

let frame_headers : Int

#
frame_ping

let frame_ping : Int

#
frame_priority

let frame_priority : Int

#
frame_push_promise

let frame_push_promise : Int

#
frame_rst_stream

let frame_rst_stream : Int

#
frame_settings

let frame_settings : Int

#
frame_window_update

let frame_window_update : Int

#
has_connection_preface

fn has_connection_preface(data : Bytes) -> Bool

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

#
health_check_path

let health_check_path : String

The gRPC HTTP/2 path of the Check method.

#
health_watch_path

let health_watch_path : String

The gRPC HTTP/2 path of the Watch method.

#
hpack_decode_int

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.

#
hpack_decode_string

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_encode_int

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

#
hpack_encode_size_update

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.

#
hpack_encode_string

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.

#
hpack_encode_string_auto

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.

#
hpack_encode_string_huffman

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.

#
hpack_read_string

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.

#
hpack_static_entry

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.

#
hpack_static_table

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

#
hpack_string_is_huffman

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

#
huffman_decode

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.

#
huffman_encode

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.

#
huffman_encoded_length

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.

#
parse_grpc_timeout

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.

#
settings_enable_push

let settings_enable_push : Int

#
settings_header_table_size

let settings_header_table_size : Int

SETTINGS parameter identifiers (RFC 7540 §6.5.2).

#
settings_initial_window_size

let settings_initial_window_size : Int

#
settings_max_concurrent_streams

let settings_max_concurrent_streams : Int

#
settings_max_frame_size

let settings_max_frame_size : Int

#
settings_max_header_list_size

let settings_max_header_list_size : Int

#
stream_id_valid_for_initiator

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

#
stream_is_client_initiated

fn stream_is_client_initiated(id : Int) -> Bool

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

#
stream_is_server_initiated

fn stream_is_server_initiated(id : Int) -> Bool

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