moonrpc

    gRPC for MoonBit.

    grpc
    rpc
    protobuf
    protocol-buffers
    http2
    microservice
    streaming
    unary
    bidirectional-streaming
    interceptor
    load-balancing
    health-check
    reflection
    tls
    compression
    gzip
    deflate
    deadline
    retry
    keepalive
    annotations
    stats
    channelz
    service-config
    wait-for-ready
    interop
    thrift
    thrift-binary
    thrift-compact
    tcompactprotocol
    tframedtransport
    thrift-header
    theader
    oneway
    idl
    grpc-go
    trpc
    trpc-go
    trpc-python
    trpc-io
    typescript
    connect
    connect-protocol
    connectrpc
    conformance
    wasm
    moonbit
    Download zip
    Version
    0.22.0
    License
    Apache-2.0
    Last updated
    20 hours ago
    Downloads
    2K

    #moonrpc

    Real gRPC for MoonBit — ← gRPC.

    Check and Test License mooncakes

    Moved on mooncakes from Lfan-ke/moonrpc to moonbitstack/moonrpc.

    moonrpc is gRPC over HTTP/2, not gRPC-Web. HTTP/2 framing (RFC 9113) and HPACK (RFC 7541) come from moonhttp; this module is everything gRPC puts on top: all four call kinds, metadata, deadlines, status and rich errors, flow control, TLS with ALPN, message compression, interceptors on both ends, concurrent dispatch on one connection, health checking, server reflection, retries, hedging, backoff and load balancing, a stats observable and a channelz registry, and service-config driven retries with WaitForReady. Beside it sit four more protocol stacks: Thrift (framed, buffered, zlib, HTTP and header transports; binary, compact and JSON protocols; a processor, a client and an IDL compiler), trpc.group's tRPC (all four call kinds, interoperating with tRPC-Go and tRPC-Python), tRPC for TypeScript (JSON over HTTP with its batching envelope, interoperating with @trpc/client and @trpc/server), and Connect over HTTP/1.1 and HTTP/2 (the same four call kinds in protobuf or JSON, driven by the official conformance suite from both sides). The client and server engines are pure functions over frames and run on every backend; only the socket drivers in net are native.

    #rpc.message
    pub struct HelloRequest {
    name : String
    }

    #rpc.message
    pub struct HelloReply {
    message : String
    }

    #rpc.service(name="greet.Greeter")
    pub struct Greeter {
    greeting : String
    }

    #rpc.method
    fn Greeter::say_hello(self : Greeter, req : HelloRequest) -> HelloReply {
    { message: self.greeting + ", " + req.name }
    }

    test {
    let greeter = Greeter::{ greeting: "hello" }
    assert_eq(greeter.say_hello({ name: "Leo" }).message, "hello, Leo")
    }

    Annotations give the protobuf codec of each message, the registration of each service, and the descriptor reflection serves; see Annotations. A method's shape says which of gRPC's four call kinds it is.

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

    A server reports the port it actually bound: GrpcServer::bind opens the socket and hands back its address, and bind_thrift does the same for the Thrift driver, so port=0 asks the system for a free one. Both are what the test suite uses — moon test runs tests in parallel, and a test that names a port is a test that collides with another one.

    Documentation: the five-piece doc site — a home page, a guide, the full API reference, and a live in-browser showcase that runs the real engine.

    #Annotations

    #rpc annotations write the protobuf codec of each message, the registration of each service, and the file descriptor reflection serves:

    #rpc.enum
    pub enum Mood { Calm = 0; Glad = 1; Grumpy = 5 } // MOOD_CALM, MOOD_GLAD, MOOD_GRUMPY

    #rpc.message
    pub struct Profile {
    name : String // 1
    #rpc.field(number="4")
    tags : Array[String] // 4
    nickname : String? // 5, proto3 optional
    mood : Mood // 6
    extras : Map[String, Int64] // 7, map<string, int64>
    }

    #rpc.message(name="HelloReply.Badge") // nested: .greet.HelloReply.Badge
    pub struct Badge { label : String }

    #rpc.message
    pub struct Upto { n : Int }

    #rpc.message
    pub struct Number { value : Int }

    #rpc.method
    fn Greeter::count_to(_self : Greeter, req : Upto) -> Array[Number] {
    let out = []
    for i = 1; i <= req.n; i = i + 1 {
    out.push({ value: i })
    }
    out
    }

    #rpc.method
    fn Greeter::echo(_self : Greeter, req : Array[Number]) -> Array[Number] {
    req
    }

    // From the declarations above the generator writes a `Message` impl per message, and
    // `Greeter::register(server)` plus `Greeter::descriptor()` per service. Written out by
    // hand, registration is one line per method:
    fn wire_greeter(server : @net.GrpcServer) -> Unit {
    server.register_unary("/greet.Greeter/SayHello", (_ctx, req) => req)
    }

    // The descriptor is what reflection serves, and it can be printed back as a .proto.
    fn print_descriptor() -> Unit raise {
    let file = @moonrpc.FileDescriptor::new("greet.proto", "greet")
    println(file.source())
    }

    // A generated client takes a channel and returns typed replies:
    // let client = GreeterClient::new(@net.Channel::connect("127.0.0.1", @net.port))
    // let reply = client.say_hello({ name: "Leo" })

    Fields are numbered in declaration order from 1; number= sets one, written as a string because the attribute grammar has no integers, and the count carries on from it. #rpc.skip leaves a field out. A method's name is the function's in PascalCase, name= overriding it; returning an array streams the replies, taking one receives a client stream, both is a bidirectional stream answered once the client half-closes, and a parameter of type RpcContext receives the call's context. A StatusError raised by a method answers with its status, any other error with UNKNOWN, and a request that does not decode with INTERNAL, as grpc-go does. A dotted name= nests a message or an enum inside another message of the same file.

    An enum without payloads takes #rpc.enum. Its values are numbered by MoonBit's own tags (Glad = 1), or by #rpc.value(number="N"), or one past the previous, the first being 0 as proto3 requires; each is named after the enum and itself in upper snake case, #rpc.value(name="...") overriding it, as Google's style guide and buf's ENUM_VALUE_PREFIX ask. A number the enum does not know reads as its first value, or into a constructor holding one Int when there is one, as Java's and Swift's UNRECOGNIZED keep it.

    Each service also gets a client, GreeterClient[C] over any Caller: one async method per RPC, taking the request and metadata?, timeout_millis? and retry?, returning the decoded reply and raising a non-OK status as a StatusError. cmd/greetd calls itself through it in CI.

    MoonBitprotobuf
    String, Bytes, Boolstring, bytes, bool
    Int, Int64, UInt, UInt64int32, int64, uint32, uint64
    Double, Floatdouble, float
    a #rpc.message, a #rpc.enumthat message, that enum
    Array[T]repeated T, packed for the integers, bool and enums
    T?optional T
    Map[K, V]map<K, V>, K a string, an integer or a bool

    21-annotations checks the generated codec, service and descriptor against the same ones written by hand, byte for byte, and against protoc and Python's protobuf runtime (scripts/gen_protoc_vectors.py).

    Wire the generator into the build once. In moon.mod:

    rule(name: "rpc", command: "moonx moonbitstack/moonrpc/cmd/rpc@0.22.0 -C \"$mod_dir\" \"$input\" -o \"$output\"")

    and in the moon.pkg of the package holding the declarations:

    dev_build(rule: "rpc", input: "service.mbt", output: "service.g.mbt") formatter(ignore: [ "service.g.mbt" ])

    #The four call kinds by hand

    Annotations generate these calls, and a handler registered at run time is written the same way.

    gRPC defines four shapes of a call. moonrpc implements all of them as the same frame machinery — only how many messages each side sends differs. Register the one that matches your handler, or let #rpc infer it from the method's type.

    Call kindHow to registerHandler shapeTyped wrapper
    Unaryserver.register(path, (Bytes) -> Bytes)one request, one reply@moonrpc.unary
    Server streamingserver.register_server_streaming(path, (ctx, req) -> Array[Bytes])one request, many replies@moonrpc.server_streaming
    Client streamingserver.register_client_streaming(path, (ctx, msgs) -> Bytes)many requests, one reply@moonrpc.client_streaming
    Bidi streamingserver.register_bidi(path, ctx => BidiHandler::{ on_message: m => [...], on_end: () => [...] })many both waysBidiHandler

    fn register_four_kinds(server : @moonrpc.H2Server) -> Unit {
    // unary: one request, one reply.
    server.register("/greet.Greeter/SayHello", req => b"hello:" + req)

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

    // client-streaming: every request is collected, then one reply.
    server.register_client_streaming("/sum.S/Add", (_ctx, msgs) => msgs[0])

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

    #Packages

    Each layer is its own subpackage, so a program links the layers it uses and no others. The root re-exports all of them, so @moonrpc.H2Server and @moonrpc.encode_message work as they always did.

    PackageWhat it holdsDepends on
    moonrpc/wiremessage framing, Status, Method, metadata rules, keepalive, HTTP/2 defaultsmoonhttp, moonvar, moonbase
    moonrpc/protoPbWriter/PbReader, Message, the descriptor model and its codecmoonvar
    moonrpc/streamthe stream state machine, RpcContext, the four handler kinds, Registrarmoonhttp
    moonrpc/balanceretry, hedging and throttling after gRPC A6, the service config, connectivity states, balancers, DNS, the channel poolwire, moonhttp, moonward, moondate, moonvar, json
    moonrpc/clientH2Client, CallReplywire, moonhttp, moonzip
    moonrpc/serverH2Server, interceptorswire, proto, stream, moonhttp, moonzip, moonbase
    moonrpc/servicehealth checking, server reflection, a channelz registry, and unary/server_streaming/client_streaming over message typesserver, stream, proto, wire, moonvar
    moonrpc/channelzthe canonical grpc.channelz.v1.Channelz descriptor and its seven RPCsservice, stream, proto, wire
    moonrpc/thriftthe Protocol contract, TBinaryProtocol and TCompactProtocol, TFramedTransport, the processor and the client, independent of gRPCcore
    moonrpc/trpcthe tRPC frame and its two unary headers, the four streaming frames, a server over Registrar and a client over a byte linkstream, client, proto, wire
    moonrpc/connectthe Connect protocol: content types, the streaming envelope, the error body, and the dispatch that turns a request into a responsewire, proto, stream, moonhttp, moonjson, moonzip
    moonrpc/trpciotRPC for TypeScript: the JSON body, the batching envelope, a procedure table and a client, independent of gRPCmoonjson
    moonrpc/cmd/rpcthe generator behind the #rpc annotationsmoonast
    moonrpc/cmd/thriftcthe .thrift IDL compiler: types, codecs, service registration and clientcore
    moonrpc/netGrpcServer, Channel, MuxChannel, ManagedChannel over TCP and TLS (native)the root, moonnet, moontls, moonbitlang/async

    A client links nothing of the server and a server nothing of the client; protobuf alone is proto.

    Constructors are written without the package. pub using re-exports types but not their constructors, so @moonrpc.Serving does not resolve. Write the bare constructor and let the expected type pick it: health.set_status("greet.Greeter", Serving). Where nothing gives the type, name the subpackage: @wire.Status::NotFound.

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

    async fn channel_calls(chan : @net.Channel) -> Unit raise {
    // unary: the reply messages, in order, and the status that ended the call.
    let reply = chan.unary("/greet.Greeter/SayHello", b"Leo")
    assert_eq(reply.grpc_status, 0)
    assert_eq(reply.messages.length(), 1)

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

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

    // 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", b"x", timeout_millis=200)
    assert_eq(bounded.grpc_status is 4 || bounded.grpc_status is 0, true)
    }

    MuxChannel carries many concurrent calls and interactive bidirectional streams over one connection; ManagedChannel dials several backends, tracks each through the gRPC connectivity states and picks one per call (pick_first or round_robin). A failed dial puts the channel on a backoff clock — one second, times 1.6, capped at two minutes, jittered ±20% — so a backend that is down costs one dial per wait rather than one per call; re-dialling resolves the name again when the channel was given one.

    Both ends cap message size per engine (set_max_message_size(recv~, send~)); an incoming message over the cap ends its call with RESOURCE_EXHAUSTED rather than pinning the memory its length prefix declares, and a reply over the send cap ends it the same way. The client sends user-agent, moonrpc/<version> unless a caller sets its own.

    All three implement Caller, the trait the clients #rpc generates call through; a test double or an interceptor chain implements it to stand in their place. reply.ok() gives the messages of a call that ended OK and raises any other status as a StatusError; one() also wants exactly one message, and reply()/replies() decode them into a Message type, INTERNAL when they do not decode, as grpc-go reports.

    #Health

    The standard grpc.health.v1.Health service registers in one call. Check answers the status of a name — SERVING, NOT_SERVING, or NOT_FOUND for a service the server does not know — and Watch keeps one stream open, sending the status at subscribe time and again on every change:

    fn health_service() -> @moonrpc.HealthService {
    let health = @moonrpc.HealthService::new()
    health.set_status("greet.Greeter", @service.ServingStatus::Serving)
    // Check and Watch answer on /grpc.health.v1.Health/*; a status change is pushed to
    // every live Watch.
    health.set_status("greet.Greeter", @service.ServingStatus::NotServing)
    health
    }

    Watch is a pushed handler: it owns its response stream and answers over time rather than once. Any method can be written that way — Handler::Pushed is handed the request messages and a Responder with send and finish — which is what a subscription of any kind needs.

    #Interceptors

    One shape covers all four call kinds, on both ends. A call is the request messages it carries and the replies they produce, so a unary method is simply one message each way:

    fn logging_interceptor() -> @server.Interceptor {
    (info, _ctx, requests, next) => {
    // Answer here without calling `next` to serve the call yourself.
    let replies = next(requests)
    println("{info.path} answered {replies.length()}")
    replies
    }
    }

    fn authorize() -> @client.Interceptor {
    // A client interceptor sees the invocation and may change what goes out.
    async fn(invocation, next) {
    let metadata = invocation.metadata
    metadata.push(@header.Header::of("authorization", "Bearer ..."))
    next({ ..invocation, metadata })
    }
    }

    CallInfo carries the path and which of the two sides streams, so an interceptor that only wants unary calls reads it and passes the rest through — which is what add_unary_interceptor and add_stream_interceptor do. The client chain wraps a call once, outside the retry loop: a retry is one invocation to an interceptor, and Invocation holds the path, every request message, the metadata, the deadline and the retry policy.

    #TLS

    HTTP/2 over TLS is chosen by ALPN, so both ends offer h2 and check what the handshake settled on before a frame is exchanged. client_tls and server_tls build the two configurations with h2 alone:

    async fn serve_over_tls(cert_text : String, key_text : String) -> Unit raise {
    let server = @net.GrpcServer::new()
    server.register("/echo.Echo/Say", req => req)
    // leaf certificate first, then the chain; ALPN settles on `h2`.
    server.serve(
    port=12000,
    tls=@net.server_tls(@x509.pems(cert_text), @x509.PrivateKey::pem(key_text)),
    )
    }

    async fn call_over_tls(roots : @x509.Roots) -> Unit raise {
    let chan = @net.Channel::connect(
    "api.example.com",
    12000,
    tls=@net.client_tls(roots),
    )
    chan.close()
    }

    MuxChannel::connect, ManagedChannel::connect and ManagedChannel::dns_connect take the same tls. A peer that offers no h2 is closed rather than spoken to in a version it did not agree to (RFC 9113 §3.2), and a failed handshake on serve is a refused client, not a broken server.

    #Compression

    gRPC names three encodings — identity, gzip and deflate, the last of which is the raw DEFLATE stream rather than the zlib format — and negotiates them with grpc-encoding and grpc-accept-encoding:

    async fn compressed_traffic(server : @net.GrpcServer, host : String) -> Unit raise {
    // The server compresses a reply when the caller says it accepts the encoding.
    server.set_encoding(@moonrpc.Encoding::Gzip)
    let chan = @net.Channel::connect(
    host,
    12000,
    encoding=@moonrpc.Encoding::Gzip,
    )
    chan.close()
    }

    A server only compresses for a caller whose grpc-accept-encoding lists the encoding; a caller that accepts nothing but identity gets identity, because sending a body the far end cannot read is not compression. A message whose flag is set under identity — compressed and not compressed at once — is refused on both ends, and a body that will not inflate fails the call rather than reaching the handler as bytes. wire.Encoding carries the codecs and the list this library advertises.

    #Interoperability

    A gRPC implementation is only gRPC if a foreign one agrees on the bytes, and an implementation is only self-consistent if its own two halves do. CI runs all three pairings — grpc-go against this stack, Apache Thrift against this stack, and this stack against itself — in plaintext and over TLS:

    PartnerDirectionCallsCompressionInterceptorsTLSResult
    grpcurl (grpc-go)clientreflection list/describe, all fourgzip—yespass
    grpc-goclientall four, plus x-test-* metadata, a deadline, a rich error detail and a 160 KB messagegzipclient chainyespass
    grpc-goserverthe same calls, through the generated GreeterClientgzipserver chainyespass
    Apache Thrift (Go)clientping, add, double, fail, note (ONEWAY) — framed, buffered, zlib, HTTP and header transports; binary, compact and JSON protocols; the header zlib transformidentity—nopass
    Apache Thrift (Go)serverthe same calls, through thriftd checkidentity—nopass
    Apache Thrift cross testclientThriftTest: all 23 methods, framed/buffered/zlib/HTTP × binary/compact/jsonidentity—nopass
    Apache Thrift cross testserverthe same 23 methods, driven from this sideidentity—nopass
    moonrpcclient and servergreetd check against greetd: all four, metadata, deadline, detail, 160 KB batchgzipboth halvesyespass
    moonrpcclient and serverthriftd check against thriftd: ping, add, double, fail, note; six transports × three protocolszlib on header—nopass
    tRPC-GoclientSayHello (unary), Count (server streaming), Sum (client streaming), Chat (bidirectional), with metadata and a timeoutidentity—nopass
    tRPC-Goserverthe same four calls, through trpcd checkidentity—nopass
    tRPC-Pythonclientthe same four callsidentity—nopass
    tRPC-Pythonserverthe same four calls, through trpcd checkidentity—nopass
    tRPC for TypeScript (@trpc/client)clientgreet.hello, math.add, greet.remember (a mutation), greet.fail, and a three-call batch through httpBatchLinkidentity—nopass
    tRPC for TypeScript (@trpc/server)serverthe same calls, through trpciod checkidentity—nopass
    moonrpcclient and servertrpcd check against trpcdidentity—nopass
    moonrpcclient and servertrpciod check against trpciodidentity—nopass

    cmd/greetd is the server the first two call; the third runs the same generated client against a grpc-go server and requires the two answers to match byte for byte. The grpc-go side lives in scripts/interop, generated from the same greet.proto shape the annotations declare.

    What each of those calls carries is part of the matrix, not beside it: a message alone would exercise the framing and nothing else. SayHello answers with the x-test-* metadata the caller attached and the deadline it was given, so a header the far end dropped, or a grpc-timeout that never arrived, fails the job. An empty name comes back INVALID_ARGUMENT with a google.protobuf.StringValue in grpc-status-details-bin — the detail is what grpc-go's status.Details() decodes, and one direction checks it with grpc-go's runtime and the other with this library's reader. BatchEcho carries 40000 int32s, which is 160 KB: both directions need more than one HTTP/2 frame and a window update in the middle of the message, which a call that fits in a frame never asks for.

    The Thrift rows are the protocol stack below: framed, buffered, zlib, HTTP and header transports, with binary, compact or JSON inside each, and the header transport's zlib transform. note is a ONEWAY method — it must leave no reply on the wire, which the call that follows would otherwise read as its own. The tRPC rows run one method per cardinality, the bidirectional one message at a time: a server that waited for the send side to close before answering would deadlock against it. The tRPC-for-TypeScript rows are not the same protocol as those: they are JSON over HTTP, and what they check is the batching envelope — three calls in one request, answered in the order they were asked, each either a result or an error — through the peer's real httpLink and httpBatchLink.

    The two moonrpc rows are the third pairing, this stack against itself, and they cover what no foreign job can: one transport on both sides at once. A peer picks its own settings, so grpc-go over TLS only ever says this server speaks TLS to someone else; greetd check says this client and this server agree in each of the four configurations, and thriftd check says the same for every Thrift combination. scripts/interop/self_test.sh is that run, one case per configuration. Connect's two halves meet in connect_wbtest.mbt over a real socket; the official suite drives the server half with a foreign client (3556 cases) and this client against the runner's reference server (4292 cases).

    #Conformance

    The suites below are other people's programs, not ours, and each runs in CI on ubuntu-26.04: a failing one fails the build. The Thrift rows are Apache's own cross test, built from a pinned tag with bindings generated by that release's compiler.

    SuiteWhat it checksResult
    grpcurl (grpc-go)reflection list and describe, all four call kinds, gzip, a non-OK statuspass
    grpc-go clientthe same server over plaintext and TLS, with an interceptor chainpass
    grpc-go serverthis client against it, all four call kinds; both answers byte for bytepass
    grpc.channelz.v1.Channelz through grpcurlthe seven RPCs, the oneofs, the well-known importspass
    Apache Thrift cross test, clientThriftTest as served here: 23 methods × framed/buffered/zlib/HTTP × binary/compact/jsonpass
    Apache Thrift cross test, serverthe same 23 methods as this client calls them, in the same twelve pairingspass
    tRPC-Go, both directionsscripts/interop/trpc_interop.sh: their codec's frames to this server and this client's frames to their server, one method per cardinalitypass
    tRPC-Python, both directionsthe same pairings, with TrpcClientCodec/TrpcServerCodecpass
    tRPC differentialfive requests and three response headers encoded by tRPC-Python 1.0.0, reproduced octet for octetpass
    tRPC for TypeScript, both directionsscripts/interop/trpcio_interop.sh: their client's links to this server and this client to their server, four procedures plus the batch envelopepass
    connectrpc/conformance, server3556 cases: this reference service against the runner's reference client, both HTTP versions, both codecs, TLS and plaintext, five compressionspass; the 70 TLS deadline cases are also re-run one RPC at a time, where they pass outright
    connectrpc/conformance, client4292 cases: this client against the runner's reference server, the same matrix including all 76 cancellation casespass

    scripts/interop/build_thrift_peers.sh builds the reference client and server; scripts/interop/thrift_cross_test.sh runs both directions; src/cmd/thriftcrosstest is this side of each. The Thrift rows cover the strict message header, every primitive, structs nested two deep, maps of maps, sets, a uuid, a oneway method, and the two ways a call can fail: an exception in the field its throws clause numbers, and the application exception a processor builds when a handler raises something else.

    What the cross test cannot reach is the header transport — its binaries spell that one -protocol header, and the pair in scripts/interop/thriftclient / thriftserver is what covers it, alongside the self-interop matrix. The IDL's SecondService is still outside the cross test, and testOneway is answered without the reference servers' sleep.

    The two Connect rows are the same runner in opposite modes, and both are gated directly — no declared failures. The cancellation cases are driven over an incremental exchange: the request head goes out first, each message is its own write, and the caller closes or aborts the send side at the point the case names. HTTP/1.1 drops the socket mid-chunk; HTTP/2 sends RST_STREAM with CANCEL.

    #Stats and channelz

    Both engines report each RPC's life to a StatsHandler — begin, every header and payload that moves, and the status it ended with — and the same event order on each side:

    fn stats_handler() -> @moonrpc.StatsHandler {
    @moonrpc.StatsHandler::new(rpc=event => {
    match event {
    @wire.RpcStats::Begin(path~, name~) => println("begin {path} {name}")
    @wire.RpcStats::End(status~, message~) => println("end {status} {message}")
    _ => ()
    }
    })
    }

    A native Channel, ManagedChannel and GrpcServer forward the handler to every connection. A Registry turns those events into channelz-style counters: calls started, succeeded and failed, messages each way, and a connectivity state that follows the connection events.

    fn channelz_registry() -> @moonrpc.Registry {
    let registry = @moonrpc.Registry::new()
    // The handler a channel reports its calls through.
    let id = registry.add_channel("127.0.0.1:12000")
    registry.channel_handler(id) |> ignore
    registry
    }

    The same registry backs the canonical grpc.channelz.v1.Channelz service. ChannelzService registers its seven RPCs on any server and adds channelz.proto and the four well-known files it imports to reflection, so grpcurl can list, describe and call it — oneofs, nested messages and all:

    fn install_channelz(server : @net.GrpcServer) -> Unit {
    let registry = @moonrpc.Registry::new()
    let server_id = registry.add_server()
    ignore(registry.add_server_socket(server_id, "127.0.0.1:12000"))
    let channelz = @moonrpc.ChannelzService::new(registry)
    channelz.install(server)
    let reflection = @moonrpc.ReflectionService::new()
    channelz.add_to_reflection(reflection)
    server.register_reflection(reflection)
    }

    cmd/channelzd is the runnable form; CI calls GetServers, GetServer, GetServerSockets, GetSocket, GetTopChannels, GetChannel and GetSubchannel with grpcurl and checks the answers.

    #Service configuration and WaitForReady

    A channel parses gRPC's service-config JSON and applies each method's retryPolicy, timeout and waitForReady, leaving a setting the caller gave explicitly alone:

    async fn configured_call(host : String) -> Unit raise {
    let text =
    #|{
    #| "methodConfig": [{
    #| "name": [{"service": "greet.Greeter", "method": "SayHello"}],
    #| "timeout": "1.5s",
    #| "waitForReady": true,
    #| "retryPolicy": {
    #| "maxAttempts": 4, "initialBackoff": "0.1s", "maxBackoff": "1s",
    #| "backoffMultiplier": 2, "retryableStatusCodes": ["UNAVAILABLE"]
    #| }
    #| }]
    #|}
    let config = @moonrpc.ServiceConfig::parse(text) catch {
    _ => @moonrpc.ServiceConfig::empty()
    }
    let chan = @net.ManagedChannel::connect(
    [("127.0.0.1", 12000)],
    config=config,
    )
    chan.close()
    }

    With wait-for-ready, a ManagedChannel whose backends are all down holds the call and redials until one comes up or the call's deadline passes, instead of failing fast with UNAVAILABLE. It is on by default for a method whose config says waitForReady, and a call can turn it on with wait_for_ready~ whether or not a config names it.

    #Thrift

    A second protocol stack sits beside the gRPC one and shares only the socket layer: a processor that dispatches by method name, a client that sends a CALL and reads a REPLY, and six ways to put the bytes on the wire. Nothing in thrift/ imports HTTP/2, HPACK or protobuf.

    The wire format is a choice rather than a copy. Protocol is the contract; Binary, Compact and Json are three implementations of it — fixed-width big-endian numbers with fixed field ids, zigzag varints with field-id deltas and a double written little-endian, or TJSONProtocol's arrays and objects keyed by field id. A text format ends a message with a bracket rather than with its last field, so the contract also carries read_message_end, write_message_end and consumed: the octets a reader has taken. message_len uses them to find where a message ends when no length was written in front of it.

    The processor, the client request and a service's handlers are written once, against the trait, so a service answers any of the three without knowing which one it got. The JSON implementation is checked against Apache Thrift's own Go encoder: scripts/interop/thriftjson writes the vectors, and the test reads each with this package and writes it back byte for byte.

    ThriftWire in net picks the transport: Framed puts a four-octet length in front of each message; Buffered writes messages back to back and the reader grows a buffer until message_len finds a whole one; Zlib is one DEFLATE stream in each direction, split at the 00 00 ff ff a synchronising flush leaves — the only marker in DEFLATE that says a message ends there, and one decoder must live for the whole connection because the compressor may refer back into an earlier message; Http is one POST per call. The header transport frames its own messages and can carry its zlib transform on top.

    async fn serve_and_call_thrift() -> Unit raise {
    let processor = @thrift.Processor::new()
    processor.add("ping", proto => {
    ignore(proto.read_field_begin()) // field 1: the argument
    let text = proto.read_string()
    ignore(proto.read_field_begin()) // STOP
    proto.write_field_begin(@thrift.type_string, 0) // field 0: the result
    proto.write_string("pong:" + text)
    })
    let listener = @net.bind_thrift(host="127.0.0.1", port=0)
    let port = listener.addr().port()
    // Serve on a socket that is already bound, so the port is known before the loop starts.
    let serving = @async.with_task_group(group => {
    group.spawn_bg(() => {
    @net.serve_thrift_on(listener, processor, () => @thrift.Binary::new(b""))
    })
    let proto : &@thrift.Protocol = @thrift.Compact::new(b"")
    let client = @net.ThriftClient::connect("127.0.0.1", port, proto)
    let pong = @thrift.call(client, proto, "ping", 1, proto => {
    proto.write_field_begin(@thrift.type_string, 1)
    proto.write_string("moonrpc")
    }, proto => proto.read_string())
    client.close()
    assert_eq(pong, "pong:moonrpc")
    })
    ignore(serving)
    }

    cmd/thriftd serves ping, add, double, fail and a ONEWAY note; thriftd check calls them, and words after the port pick the bytes on either side — framed|buffered|zlib|http|header|header-zlib for the transport and binary|compact|json for the protocol. CI runs an Apache Thrift Go client against it and the moonrpc client against an Apache Thrift Go server in sixteen combinations, and Apache's own cross test drives all 23 methods of ThriftTest in both directions over twelve transport/protocol pairings. Between them they cover the strict message header, bool, byte, i16, i32, i64, double, string, structs, lists, sets and maps, with a depth guard on skip. fail answers a value in field 0 or an exception in field 1, so the shape a throws clause gives a reply is checked against Apache's client and server as well.

    The header transport is the row that only a foreign peer can settle. Its frame names the inner protocol, so a server that wrote compact bytes under a binary label would be unreadable to every Apache client, and a peer that trusted the label is the only thing that notices — its read of our reply fails, where our own two halves would agree with each other. note is the other: a ONEWAY call must leave nothing behind, and the call after it is what would read a stray reply frame if the server wrote one.

    The three pieces of Thrift that a hand-written service trips over are settled here. A uuid is sixteen raw bytes with no length in front — a type of its own, not a binary. A set carries its own field type, and in the compact protocol its own collection header, so a reader can tell it from a list; write_set_begin exists for that reason. And a handler that raises answers a TApplicationException carrying the message instead of dropping the connection, which is what an Apache Thrift processor does with an exception raised out of a handler:

    fn thrift_failures() -> @thrift.Processor {
    let processor = @thrift.Processor::new()
    processor.add("find", proto => {
    ignore(proto.read_field_begin())
    ignore(proto.read_string())
    raise @thrift.Application("no such key") // the caller reads this as a Thrift error
    })
    // A method the IDL declares `oneway`: it runs and no REPLY goes back. The flag belongs to
    // the method — Apache's Go client sends a oneway call as a plain CALL and never reads an
    // answer, so a server that replies to one desynchronises the connection.
    processor.add_oneway("notify", proto => {
    ignore(proto.read_field_begin())
    ignore(proto.read_string())
    })
    processor
    }

    The other transport shape is the header frame: the body sits inside a frame whose meta block names the inner protocol and the transforms, and whose info section carries key/value headers beside the call. One endpoint reads binary and compact frames on the same socket, which is what the reference implementations negotiate.

    fn header_endpoint() -> @thrift.Header {
    let header = @thrift.Header::new()
    header.set_protocol(@thrift.header_protocol_compact) // or header_protocol_binary
    header.add_header("tenant", "acme") // beside the call, not in it
    header.add_transform(@thrift.header_transform_zlib) // the payload travels compressed
    header
    }

    thrift.encode_header_frame and thrift.decode_header_frame are the framing on its own, for a caller that moves frames itself. The test that covers them compares bytes with Apache Thrift's Go bindings, frame for frame.

    A oneway method is sent through Transport::send rather than round_trip, so the client does not wait for a reply that will never come:

    async fn fire_and_forget(client : @net.ThriftClient) -> Unit raise {
    let proto : &@thrift.Protocol = @thrift.Binary::new(b"")
    @thrift.oneway_call(client, proto, "notify", 1, proto => {
    proto.write_field_begin(@thrift.type_string, 1)
    proto.write_string("done")
    })
    }

    cmd/thriftcrosstest is Apache's ThriftTest service and its client, so the whole reference suite — all 23 methods, four transports and three protocols — runs against this stack in both directions; see Conformance.

    cmd/thriftc compiles a .thrift file to MoonBit: enums with their numbers, structs with a codec and a default(), and each service's register_* and client functions. optional becomes Option, binary becomes Bytes, and a struct that contains itself by value is refused rather than generating a codec that cannot end. 29-thrift-idl is its output, and CI reruns the compiler with --check so the two cannot drift:

    moon run src/cmd/thriftc -- greet.thrift -o greet.g.mbt

    The compiler reads struct, enum, exception, union, typedef, const and service over the primitive types, list, set and map. A typedef is substituted where it is used — typedef i64 Timestamp leaves Int64 behind, since the wire carries no name for it either — and an alias that loops back to itself is refused with the chain.

    A union carries exactly one of its fields, so it becomes an enum with a variant each; on the wire it is a struct with that one field set. Reading a union that carries none is refused rather than defaulted, and default() — what a struct field of a union type starts from — is its first field's default:

    union Contact { 1: string email 2: i64 phone }

    pub(all) enum Contact { Email(String); Phone(Int64) }

    A const is a pub let of its type. Scalar, list, map, struct and enum values are read, the type checks the value, and the name takes the shape MoonBit wants for a value, so MAX_AGE is written maxAge:

    const i32 MAX_AGE = 130 const Person ADA = { "name": "Ada", "age": 37 }

    // `const i32 MAX_AGE = 130` in the IDL, with the name MoonBit wants for a value.
    pub let max_age : Int32 = 130

    test {
    assert_eq(max_age, 130)
    }

    A value that does not fit its type, a field or enum value that does not exist, and a constant that names another constant are refused with the line they came from.

    An exception is a struct on the wire; a method whose IDL declares throws answers a Result, one variant per declared failure, instead of raising it:

    exception Missing { 1: string message 2: i32 code } service Greeter { string find(1: string key) throws (1: Missing missing) }

    // `find(1: string key) throws (1: Missing m)` becomes a `Result`, not an exception:
    // the error enum names the declared exception, the handler returns
    // `Result[String, GreeterFindError]`, and the client gets the same type back.
    pub(all) enum GreeterFindError {
    Missing(Missing)
    }

    #derive(Debug)
    pub struct Missing {
    message : String
    }

    fn found_or_missing(found : Result[String, GreeterFindError]) -> String {
    match found {
    Ok(value) => value
    Err(Missing(missing)) => missing.message
    }
    }

    test {
    assert_eq(found_or_missing(Ok("found ada")), "found ada")
    assert_eq(found_or_missing(Err(Missing({ message: "no ada" }))), "no ada")
    }

    On the wire this is what Apache Thrift does: the success value in field 0, the exception in the field its clause numbers. A failure field the IDL does not declare is skipped and raised as @thrift.Application, so nothing is swallowed as if it were a value.

    include "shared.thrift" folds that file's types into the output, under the name the include gives them: shared.Person becomes SharedPerson, so one package holds both without a clash. Nested includes are read once each, and two included files with the same basename are refused rather than merged silently. service Child extends Base carries the base's methods into register_child and the child's client — the base's own registration stays with the base's file.

    #tRPC

    tRPC is the protocol the trpc.group stack speaks: a 16-octet fixed head, then either a unary call — a protobuf request header, the message, and an optional attachment — or one of four streaming frames. It carries more of a call than gRPC does in-band: the caller and callee names, the interface path, a timeout in milliseconds, the serialization and compression codes, and a map of transparent key-value pairs that every hop forwards.

    A service is written once. @trpc.Server implements the same Registrar the gRPC engines do, so the generated Greeter::register fills either, and the handlers are the same Unary/ServerStreaming/ClientStreaming/Bidi shapes.

    async fn trpc_call(port : Int32) -> Unit raise {
    let channel = @net.TrpcChannel::connect("127.0.0.1", port)
    let client = @trpc.Client::new(channel, caller="trpc.app.client.service")
    let request = @proto.PbWriter::new()
    request.string_(1, "world")
    // A unary call: one request frame out, one reply frame back.
    let reply = client.unary(
    "/trpc.test.Greeter/SayHello",
    request.to_bytes(),
    metadata=[{ name: b"x-test", value: b"moonrpc" }],
    timeout_millis=5000,
    )
    println(reply.length().to_string())
    channel.close()
    }

    A server registers the same way a gRPC one does, and answers unary and streaming calls from the same table:

    fn trpc_server() -> @trpc.Server {
    let server = @trpc.Server::new()
    server.register("/greet.Greeter/SayHello", @stream.Handler::Unary((ctx, req) => {
    // The caller's `x-test` metadata arrives as a header, because `trans_info` is where the
    // protocol puts it.
    match ctx.metadata_get(b"x-test") {
    Some(_) => req
    None => req
    }
    }))
    server.register("/greet.Greeter/Count", @stream.Handler::ServerStreaming((_ctx, req) => [req]))
    server
    }

    Streaming is the four frames the protocol names: INIT opens a stream and hands the far end the window it may send, DATA carries a message, FEEDBACK returns window as messages are consumed, and CLOSE ends a direction — or resets the stream. Session holds one connection's streams, so a test drives the server half without a socket.

    The client has all four call kinds. Client::open returns an Exchange the caller drives message by message — send, next, close — because a bidirectional call whose server answers as it reads deadlocks against a caller that sends everything before reading anything. client_stream and bidi are the two shapes that do not need to interleave, and stream is one line over bidi. cmd/trpcd serves one method per cardinality, and its two peers do the same against tRPC-Go's and tRPC-Python's own codecs.

    tRPC-Python featureHow this library answers it
    Filters (client, server, per-service)@client.Interceptor and the server's interceptor chain, shared with gRPC
    Serialization kinds — proto, JSON, JCE, flatbuffers, XML, thriftproto (0) and JSON (2) are built in; the code is carried in the header and a caller may send any of them
    Compression kinds — none, gzip, snappy, zlib, lz4, snappy-streamidentity, gzip (1) and zlib (3) both ways; snappy and lz4 are refused with a decode error rather than misread
    Per-call timeout, and the full-link deadlinetimeout in the header, in milliseconds, surfaced as deadline_millis on the context; a full-link deadline is the caller's own deadline less what has been spent, written into each hop
    Transparent information (trans_info), dyeing key, environmentmetadata on a call maps to trans_info both ways; dyeing_key and env_key are the framework's names, and message_type carries the dyeing flag
    ONE-WAY callscall_oneway in the header; a server that answers one would leave an unread frame, so the flag comes from the method
    Transports — TCP, UDP, HTTPTCP; the protocol is transport-agnostic and a link is any write/read pair
    Multiplexed streams on one connectionstream ids in the head; Session keeps one table per connection
    Streaming back-pressureINIT's window and FEEDBACK increments, granted per frame consumed

    #tRPC for TypeScript

    The trpc.io protocol is not the trpc.group one above. It is JSON over HTTP, one call per request or several in a batch, with no frame and no session: the call names its own path in the URL, and the body carries only its input. It is what a @trpc/server router speaks, and its envelope borrows JSON-RPC's shape without its method dispatch.

    @trpcio.Server is a table of procedures, and handle is one request in, one answer out — a pure function, so a test drives the server half without a socket, and net::serve_trpcio is the HTTP/1.1 driver over it:

    fn trpcio_service() -> @trpcio.Server {
    let server = @trpcio.Server::new()
    // A query answers a value and may be asked for with GET; a mutation may not.
    server.query("greet.hello", input => Ok({ "greeting": Json::string("hello") }))
    server.mutation("greet.remember", input => {
    let name = match input {
    Some(value) => value
    None => Json::null()
    }
    Ok({ "name": name })
    })
    // A failure names its tRPC code, and the code carries the HTTP status.
    server.query("greet.fail", _input => {
    Failed(@trpcio.failure_of("FORBIDDEN", "not for you", path="greet.fail"))
    })
    server
    }

    The verb follows the kind, and @trpc/server enforces it in both directions: a query is a GET with its input in ?input=, a mutation is a POST with its input as the body, and a POST to a query or a GET to a mutation is a 405. A batch carries one verb for all of its calls, so one batch is one kind — {"0":<input>,"1":<input>}, keyed by position, with a call that has no input left out rather than written as null — and a batch with both kinds in it is two requests whose answers are merged back into the order the calls were given, which is what tRPC's own link does. The reply to one request is an array in the same order, each element either {"result":{"data":...}} or {"error":{...}}; a failure in one call does not disturb its neighbours:

    async fn trpcio_calls(port : Int32) -> Unit raise {
    let client = @net.trpcio_client("127.0.0.1", port, base="/trpc")
    let greeting = client.query("greet.hello", input=Json::string("moon"))
    // One request, three answers, in the order they were asked for.
    let outcomes = client.batch([
    @trpcio.Call::new("greet.hello", input=Json::string("batch")),
    @trpcio.Call::new("math.add", input={ "a": Json::number(1), "b": Json::number(2) }),
    @trpcio.Call::new("greet.fail"),
    ])
    println(greeting.to_string() + outcomes.length().to_string())
    }

    scripts/interop/trpcio_peers is the other implementation: @trpc/server behind createHTTPServer with the same four procedures, and a client that goes through the real httpLink and httpBatchLink. cmd/trpciod is this side of each — it serves, and trpciod check calls a server and verifies every answer, including a batch that mixes a query with a mutation — so CI runs their client against this server, this client against their server, and this stack against itself.

    #Connect

    Beside gRPC sits the Connect protocol: the same four call kinds and the same handlers, over either HTTP version, with the body in protobuf or in JSON. A unary call is one message in the body and the outcome in the HTTP status; a streaming call is a run of five-octet envelopes closed by an end-stream envelope that carries the outcome and the trailing metadata.

    async fn serve_echo(server : @net.ConnectServer) -> Unit raise {
    server.add("/connect.Echo/Say", {
    input: ".connect.EchoRequest", // the message a JSON body is read as
    output: ".connect.EchoReply",
    handler: @stream.Handler::Unary((_ctx, req) => req),
    })
    // One listener for both versions: a connection that opens with the HTTP/2 preface is
    // served over HTTP/2, any other over HTTP/1.1.
    server.serve(port=12000)
    }

    A JSON caller needs no second service: the request is read into wire bytes through the schema a server was built with, and the reply is written back the same way, so the handler below is the same code either codec reaches. command: moon run src/cmd/connectd -- 12000 runs the example — curl -d '{"text":"hi"}' -H 'content-type: application/json'http://127.0.0.1:12000/connect.Echo/Say answers {"text":"hi"}.

    The dispatch is a pure function: connect.Server::serve takes a path, headers and a body and returns a status, headers and a body, which is what makes it testable without a socket and what lets the two drivers — net over HTTP/1.1, and its HTTP/2 half — share it.

    A call is dispatched as it arrives, not after it has finished. Server::start opens a call, Session::feed takes the next piece of the body and hands back the envelopes that may be written now, and finish closes it — so a bidirectional caller that sends a message and waits for its answer before sending the next one gets one. Server::serve is those three steps over a request that already arrived whole, which is what a driver does when it has the body in hand.

    src/conformance is the reference service the official suite drives, and scripts/conformance holds both halves' configurations. CI starts cmd/conformance, which reports the port it bound on standard output the way the suite expects, and runs connectconformance --mode server against it over both HTTP versions. The same binary with a client argument is the other half: it reads one length-delimited ClientCompatRequest per case on stdin, drives the call with connect.Client, and writes the result back in the runner's terms.

    That job gates the build, and the scope is deliberate: 3556 cases over both HTTP versions, both codecs, TLS and plaintext, and five compressions — the widest matrix the runner can express for Connect, so that a regression in any one of those dimensions shows up rather than hiding in a configuration that was never asked for. At least 3486 of them pass on every run — the two runs of this configuration so far saw 3486 and 3504 — and the count moves only inside one class of seventy, every Deadline Propagation permutation over TLS, declared flaky in scripts/conformance/connect_server_known_flaky.txt rather than dropped from the configuration. Each needs a TLS handshake inside a two-second deadline while the suite drives forty-eight RPCs at once, and a handshake here costs one RSA-2048 private-key operation in pure MoonBit, measured at 0.49 s: forty-eight of those interleaved on one event loop cannot meet the deadline. The same cases pass with the suite run one at a time (-p 1: 140 of 140 TLS cases), and every other TLS case passes at full parallelism, so what falls short is the crypto stack's throughput, not this server's TLS. --known-flaky is the runner's word for a race: all seventy still run and a failing one is still named in the output, but the outcome of a timing race does not decide the build — --known-failing would fail it in the other direction, on a machine fast enough to pass them. And the class is not left at that: CI runs it a second time with --parallel 1 and nothing tolerated, where the handshakes no longer compete and the whole class passes — 70 of 70 in 33 seconds. In run 43238907338 the parallel run passed 3505 of the 3556 and left 51 to the race; the serial run passed all 70 of the class, those 51 among them. Between the two, every case the suite defines for this service passes. The tolerated run is what keeps the full matrix gating every other dimension; the serial run is what keeps known-flaky from becoming a place to hide.

    Every failure it drove out along the way was a disagreement between this encoder and the protocol rather than a missing feature: metadata as a map of lists, the connect-timeout-ms header, error details as Any in unpadded base64, a gzip'd unary body, a reply head sent before the request was read, 415 for a content type the server does not read, and an enum field compiled as a message — that last one alone accounted for 260 cases, because a dropped enum value turned every error into a success.

    The rest were about the parts of the protocol a call shape has rather than a body does. A unary reply carries trailing metadata as Trailer--prefixed headers, since a one-message body has no other trailer channel. A path the server does not serve is a 404 that reads back as unimplemented, and a verb that is not POST is a 405. A full-duplex reply echoes the request it answers, and only its first reply carries the call's headers and deadline. A streaming reply whose handler fails is closed at that moment rather than when the caller stops sending, which is what a full-duplex caller waiting for the error needs. And HTTP/2 field names are lowercased on the way out: a capital in one is a connection error the peer answers with RST_STREAM.

    The client suite is gated too, at 4292 of 4292. connect.Exchange is the incremental shape a cancellation needs: send, close_send, next, cancel and finish over a StreamTransport that keeps the request body open. The suite's 76 cancellation cases all pass, including cancel.before_close_send — which a one-shot POST cannot express — and the full-duplex cases that report the answers that arrived before the caller gave up.

    The one-shot shape is still there for a caller that has everything up front: net::Http1Transport is the HTTP/1.1 one and net::Http2Transport speaks the preface, SETTINGS, HPACK headers and flow-controlled DATA over HTTP/2; the latter advertises a large window on the connection and the stream up front, or a reply past 65535 octets waits for a WINDOW_UPDATE that a read-to-the-end client never sends:

    async fn call_echo(transport : @net.Http1Transport) -> Unit raise {
    let client : @connect.Client[@net.Http1Transport] = @connect.Client::new(transport)
    let reply = client.unary("/connect.Echo/Say", b"hi")
    assert_eq(reply.messages[0], b"hi")
    }

    #Concurrency on a connection

    A finished call is not run inside the frame loop. The server hands it to the driver, which runs it on a task of its own, so a handler that is waiting on something holds up nothing else on the connection:

    fn slow_method(engine : @moonrpc.H2Server) -> Unit {
    engine.register_handler("/slow.S/Wait", @stream.Handler::Pushed((_ctx, requests, out) => {
    @async.sleep(400) // a slow handler, on its own task
    (out.send)(requests)
    (out.finish)()
    }))
    }

    Deadlines are preemptive: when a call's grpc-timeout passes, the engine ends the stream with DEADLINE_EXCEEDED and the driver stops the task behind it, so a handler that would have taken half a second stops at the deadline. One lock per connection serializes writes, since calls, the reader and a shutdown GOAWAY all write the same socket.

    The server also holds peers to a keepalive policy when one is set — PINGs closer together than the policy allows are strikes, and a peer that runs out of them is answered GOAWAY(ENHANCE_YOUR_CALM) — and refuses the frames RFC 9113 calls a protocol violation: PUSH_PROMISE, DATA or HEADERS on stream 0, a zero-increment WINDOW_UPDATE, a frame over the advertised SETTINGS_MAX_FRAME_SIZE, and a SETTINGS nobody acknowledges.

    #Protobuf and Server Reflection

    A message is bytes on the wire; JSON is a second way to say the same thing, and the mapping between them is driven by the descriptor rather than by generated code. proto.Schema is the set of files a value may use, and it maps a message to JSON and back:

    pub fn message_as_json(schema : @moonrpc.Schema) -> Json raise {
    // `Profile` is a message whose descriptor the schema holds; `bytes` is its wire form.
    let bytes = b""
    let value = schema.to_json(".test.Profile", bytes)
    // The same value back on the wire, so the two directions are checked against each other.
    ignore(schema.to_bytes(".test.Profile", value))
    value
    }

    A schema also compiles from the .proto files themselves, so a service definition written by hand is a schema without a code generator in between:

    fn load_schema() -> @moonrpc.Schema raise {
    @moonrpc.Schema::compile([
    (
    "greet.proto",
    #|syntax = "proto3";
    #|package greet.v1;
    #|message Hello { string name = 1; }
    #|service Greeter { rpc Say(Hello) returns (Hello); }
    ),
    ])
    }

    The parser is moonbitstack/moonproto's, which covers proto2, proto3 and the editions; what this module does with the result is resolve every type name to its absolute spelling, add the synthetic oneof a proto3 optional field needs, and wrap the file in the descriptor model the rest of the library passes around. Imports resolve across the files given, in any order.

    The shapes are proto3's: lowerCamelCase names, int64 as a string, bytes as base64, enums by name, a map as an object, and the well-known types as the specification spells them — a Timestamp is an RFC 3339 string, a Duration ends in s, a wrapper is the value it wraps.

    PbWriter and PbReader carry the proto3 binary format: varint, zigzag, fixed32/fixed64 and length-delimited fields. The base-128 varint and the zigzag fold are moonvar's, since DWARF writes the same integer; what is here is the field structure built over it.

    fn protobuf_bytes() -> Unit raise {
    let w = @moonrpc.PbWriter::new()
    w.string_(1, "testing") // field 1, a length-delimited string
    w.int32(2, -1) // field 2, a varint (negative -> ten octets)
    let r = @moonrpc.PbReader::new(w.to_bytes())
    let (field, _wire) = r.read_tag() // (1, LengthDelim)
    assert_eq(field, 1)
    assert_eq(r.read_string(), "testing")
    }

    On top of it, a descriptor model builds and parses FileDescriptorProto and FileDescriptorSet: imports, messages with their nested messages, enums, oneofs and map entries, and services. grpc.reflection.v1.ServerReflection (with its v1alpha alias) answers ListServices, FileContainingSymbol (a service, a method, a message, a nested type or an enum) and FileByFilename, so grpcurl can list and describe a service. CI runs exactly that against cmd/greetd. FileDescriptor::source() prints the .proto a descriptor describes; protoc compiles it back to the same descriptor, which 21-annotations checks.

    fn describe_service() -> @moonrpc.FileDescriptor {
    let greeter : @moonrpc.ServiceDescriptor = {
    name: "Greeter",
    methods: [
    {
    name: "SayHello",
    input_type: ".greet.HelloRequest",
    output_type: ".greet.HelloReply",
    client_streaming: false,
    server_streaming: false,
    },
    ],
    }
    @moonrpc.FileDescriptor::new("greet.proto", "greet", services=[greeter])
    }

    #The engines

    Everything below serve is a pure engine: feed turns a decoded frame into the frames to write back, so the whole server path (HPACK, flow control, dispatch, HEADERS + DATA + grpc-status trailers) runs in memory on every backend. H2Client is the same on the other side, and the root package's tests drive the two against each other with no socket.

    fn engine_reply(engine : @moonrpc.H2Server, frame : @http2.Frame) -> Unit raise {
    engine.register("/echo.Echo/Say", req => req)
    // [HEADERS(:status 200), DATA(reply), HEADERS(grpc-status:0)]
    let out = engine.feed(frame)
    assert_eq(out.length() > 0, true)
    }

    What the server refuses: a request line that is not gRPC's (:method other than POST, a content-type not starting application/grpc, no te: trailers) gets an HTTP status instead of 200; SETTINGS_MAX_CONCURRENT_STREAMS is advertised and enforced with REFUSED_STREAM; overrunning a flow-control window is FLOW_CONTROL_ERROR; a field block must arrive contiguously (RFC 9113 §6.10); a call already past its grpc-timeout never reaches its handler.

    #Framing and status

    test {
    let frame = @moonrpc.encode_message(b"hello grpc")
    // [0x00][0x00 0x00 0x00 0x0A]["hello grpc"]
    assert_eq(frame.length(), 5 + 10)

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

    assert_eq(@wire.Status::code(@wire.Status::NotFound), 5)
    assert_eq(@wire.Status::name(@wire.Status::Unauthenticated), "UNAUTHENTICATED")

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

    #Retries and hedging

    Retries follow gRPC proposal A6, checked against grpc-go and grpc-java. Every call kind takes retry; a channel takes throttling once, for the server name it reaches.

    async fn throttled_call(host : String) -> Unit raise {
    let chan = @net.Channel::connect(
    host,
    12000,
    throttling=@moonrpc.Throttling::new(tokens=10, ratio=0.1),
    )
    let reply = chan.unary(
    "/greet.Greeter/SayHello",
    b"Ada",
    timeout_millis=2000,
    retry=@moonrpc.retry,
    )
    ignore(reply)
    chan.close()
    }

    RuleWhat happens
    Backoffmin(base × factor^(n−1), cap) times random(0.8, 1.2); the preset is A6's example, four attempts from 100 ms doubling to 1 s
    Statusonly a status in codes is retried; OK and anything else go back to the caller
    Committedonce response headers arrive, the call is never retried
    Pushbackgrpc-retry-pushback-ms sets the next wait exactly and restarts the backoff; a negative or unreadable value stops
    Throttlea retryable failure takes a token, a success gives back ratio; at half the tokens or fewer nothing is retried or hedged
    Transparenta stream the server refused, or one a GOAWAY shows it never handled, goes again at once, without a policy and without counting
    Limitat most five attempts whatever a policy asks, the client-side limit A6 sets
    Metadataeach retry carries grpc-previous-rpc-attempts
    Deadlineone deadline for the whole call; each attempt's grpc-timeout is what is left of it

    ManagedChannel picks a backend for every attempt, so a retry can land elsewhere, and unary_hedged takes hedging: the next attempt every delay or at once after a non-fatal status, the pushback and the throttle applying as for retries.

    #Not yet

    • Descriptor options other than map_entry; oneof and imports from #rpc; extensions anywhere in the descriptor model — the reflection queries are answered, but there is nothing to report.
    • HPACK never-indexed literals: the encoder indexes what it is given, and sends the dynamic table size update the peer asks for.
    • -bin metadata goes out base64-padded. Receivers accept both forms (and metadata_value_from_wire reads unpadded), and the suite pins the padded form.
    • Thrift: the IDL's SecondService is outside Apache's cross test. TJSONProtocol writes a million as 1000000 where Apache's Go encoder writes 1e+06; both decode to the same double, so the generated vectors avoid that spelling and the corner values have a test of their own.
    • Connect: zstd is the one compression neither configuration declares — the decoder here reads raw and RLE blocks where the reference encoder emits entropy-coded ones, so a zstd request this library writes would come back unreadable. GET, TLS, the other four compressions and the message receive limit are declared; the client half sets the limit and the server half does not, and scripts/conformance/connect.yaml and connect_client.yaml are the two statements of scope the runner reads.
    • tRPC: both directions of back-pressure are tracked now — the client waits for credit before writing past the peer's window and hands credit back as it consumes, and the server holds frames that do not fit in pending until a FEEDBACK releases them in order. What is not there: a bidirectional call answers each DATA on the connection's own task, so one slow handler holds the frames behind it.
    • tRPC for TypeScript: no subscriptions — the protocol's WebSocket link — and the transformer is identity, so a superjson-transformed body is the caller's to serialise and the reply's to read back; Server::new bounds a batch at 32 calls, where tRPC's own server leaves it open.

    #Licence

    Apache-2.0. See LICENSE.

    Attempts

    One call's retry state across its attempts.

    Backoff

    A backoff schedule: the first wait, the factor each failure multiplies it by, the cap, and how much of the wait is jitter.

    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.

    Call

    A call a concurrent server hands to its driver: the stream it belongs to, the context the handler will see, the request messages it carries, and the way to run it. The driver runs run on a task of its own and answers through the Responder, which comes back into the engine as push and end.

    CallInfo

    What a call is, apart from the messages in it: the path, and which of gRPC's two streaming sides it has. An interceptor that only wants unary calls reads this and passes anything else straight on.

    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. A Trailers-Only response has no initial metadata: what it carries is trailers.

    committed says Response-Headers arrived before the trailers, so the server had started answering and A6 never retries the call. unprocessed says the server refused the stream or a GOAWAY showed it was never handled, so it is safe to send again.

    Caller

    What a generated client stub calls through: something that sends requests on path with metadata, half-closes, and returns how the call ended, under an optional deadline in milliseconds and retry policy. The native Channel, ManagedChannel and MuxChannel implement it; a test double or an interceptor chain implements it to stand in their place, as tonic's GrpcService does.

    ChannelNode

    One channel as channelz sees it: its target, connectivity state, call counts, how many messages each way it has carried, and the sockets under it.

    ChannelzService

    The channelz service over one registry.

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

    ConnStats

    A connection lifecycle event, the coarser grain grpc-go reports alongside RPCs.

    ConnectionPool

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

    ConnectivityState

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

    Counters

    The call counters channelz keeps for one scope: how many began, and how they ended.

    DnsRecord

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

    DnsResponse

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

    Encoding

    A message compression the gRPC protocol names.

    Enum

    A protobuf enum: its number on the wire and back. The #rpc generator writes it for each #rpc.enum; of gives the zero value, or the constructor that keeps it, for a number the enum does not know.

    EnumDescriptor

    An enum type (EnumDescriptorProto): its simple name and its values.

    EnumValueDescriptor

    One value of an enum (EnumValueDescriptorProto).

    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. oneof_index places the field in one of its message's oneofs; a proto3 optional field sits alone in a synthetic one and sets proto3_optional.

    FieldLabel

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

    FieldType

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

    FileDescriptor

    A single .proto file (FileDescriptorProto): its filename, package, the files it imports, the messages, enums and services it defines, and the syntax level. This is the unit Server Reflection returns.

    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.

    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.

    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.

    Pushed is the shape for a method that owns its response stream: it is handed the call's request messages and a [Responder], and answers over time — a subscription such as health Watch. Running one needs a driver that can await the handler, so a pure engine left eager answers it UNIMPLEMENTED rather than hanging the connection.

    The metadata a call carries, the type moonhttp's HTTP/2 headers are.

    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.

    Hedging

    A method's hedging policy (A6 hedgingPolicy): attempts is maxAttempts, delay is hedgingDelay, codes is nonFatalStatusCodes.

    Invocation

    One call as an interceptor sees it: the method, the request messages it carries, the metadata it goes out with, its deadline in milliseconds and its retry policy.

    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.

    LbPolicy

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

    LoadBalancer

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

    Message

    A protobuf message: prost's Message. The #rpc generator writes it for each #rpc.message struct; a type of the program's own implements the two methods to travel as a request or a reply.

    MessageDescriptor

    A message type (DescriptorProto): its simple name, its fields, the messages and enums declared inside it, the names of its oneofs, and whether it is the entry type protoc makes for a map field.

    Method

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

    MethodConfig

    One method's configuration. service and name are the two halves of a method name; either may be left out to match every method of that service or of every service.

    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.

    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.

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

    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.

    Pushback

    What the server said about trying again, in grpc-retry-pushback-ms.

    Reconnect

    One backend's reconnect clock: how many dials have failed in a row, and when the next one may happen.

    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.

    ReflectionResponse

    A decoded ServerReflectionResponse, reduced to the one message_response arm.

    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.

    Registrar

    Somewhere handlers are registered by path: grpc-go's ServiceRegistrar. The in-memory engine and the native server both are, so a generated service registers on either.

    Registry

    A registry of the channels, subchannels, sockets and servers a program wants to watch. handler builds a stats handler bound to one node, so a caller attaches it to the channel or server that node describes.

    Responder

    The reply side of a call that answers as it goes: hand it messages as they happen with send, and close the call with finish (its status being whatever the context says, OK unless ctx.fail was called).

    Retry

    A method's retry policy (A6 retryPolicy). backoff carries maxAttempts as attempts, initialBackoff as base, maxBackoff as cap and backoffMultiplier as factor; codes is retryableStatusCodes.

    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.

    RpcStats

    One RPC lifecycle event. The same shape serves the client and the server, the way grpc-go's stats.RPCStats does: In* is what arrived, Out* what was sent.

    Schema

    The types one JSON mapping resolves a field's type_name against: every message and enum of every file that can appear in the value, keyed by its fully-qualified name with the leading dot.

    ServerNode

    One server as channelz sees it: its call counts and the sockets it listens on.

    ServiceConfig

    A parsed service config: the per-method entries in the order they were written (first match wins), the channel-wide load-balancing policy and retry throttling.

    ServiceDescriptor

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

    ServingStatus

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

    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.

    StatsHandler

    A sink for the events a channel or a server observes. Register one with H2Client::set_stats_handler or H2Server::set_stats_handler; every call on that connection reports to it. A [StatsHandler] is a pair of functions, as the other callbacks in this library are, so a caller can build one from closures.

    Status

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

    StatusDetails

    A google.rpc.Status as it arrives in the grpc-status-details-bin trailer: the status code, the message, and the serialized google.protobuf.Any details in order.

    StatusError

    The error a handler raises to answer with a status of its choosing: grpc-go's status.Error. A typed handler turns it into the call's grpc-status and grpc-message.

    Stream

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

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

    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.

    StreamInterceptor

    A server-streaming interceptor: (ctx, request, next) -> replies. It applies to server-streaming methods only.

    StreamState

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

    SubConn

    One address the Channel dials, tracking its connectivity state.

    Throttle

    The token count one server name shares across every call to it. A failure takes a token, a success gives back ratio, and at or below half the maximum no call is retried or hedged. Kept in thousandths of a token as gRPC core keeps it, so a ratio is read to three decimal places and nothing drifts.

    Throttling

    Retry throttling (A6 retryThrottling): tokens is maxTokens, ratio is tokenRatio. There is no preset, because a channel without one does not throttle.

    UnaryInterceptor

    A unary server interceptor: (ctx, request, next) -> reply, the shape gRPC-go calls a unary interceptor. It applies to unary methods only; [Interceptor] covers every kind.

    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 of rejects them.

    accept_encodings

    let accept_encodings : String

    The grpc-accept-encoding value this library advertises, best first: the encodings a peer may send us.

    backoff

    The schedule the gRPC document specifies: one second, times 1.6 each time, capped at two minutes, with the wait drawn from ±20% of it.

    bidi

    A bidirectional-streaming handler over message types that answers once the client half-closes: every request decoded, the replies encoded in order.

    bidi_each

    A bidirectional-streaming handler over message types that answers as each request arrives: f gives the replies to one request, sent at once, and end the replies to send when the client half-closes.

    channel_path

    let channel_path : String

    client_streaming

    A client-streaming handler over message types: every request decoded, one reply encoded.

    decode_file_descriptor_set

    Decode a FileDescriptorSet into its files.

    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.

    decode_reflection_request

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

    decode_reflection_response

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

    decode_status_details

    fn decode_status_details(body : BytesView) ->
    StatusDetails
    ?

    Decode a google.rpc.Status (code = 1, message = 2, repeated details = 3). None when the bytes are not one: a trailer that will not decode is worse than no trailer, but it is not something to fail a call over, since the status itself already came in the grpc-status header.

    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_max_streams

    let default_max_streams : Int

    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.

    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.

    detail_body

    fn detail_body(detail : BytesView) -> Bytes?

    The body of a google.protobuf.Any detail (field 2): the message the type URL names, ready for that message's own decode.

    detail_type_url

    fn detail_type_url(detail : BytesView) -> String?

    The type URL of a google.protobuf.Any detail: field 1 of the Any, the message name such as type.googleapis.com/greet.Badge, or None when the bytes are not an Any.

    dns_decode_response

    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.

    dns_encode_query

    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.

    dns_type_a

    let dns_type_a : Int

    The DNS record type for an IPv4 address.

    dns_type_aaaa

    let dns_type_aaaa : Int

    The DNS record type for an IPv6 address.

    encode_file_descriptor_set

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

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

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

    encode_reflection_request

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

    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.

    hedging

    A6's example policy: four attempts half a second apart, UNAVAILABLE, INTERNAL and ABORTED letting the others carry on.

    is_binary_metadata

    fn is_binary_metadata(name : Bytes) -> Bool

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

    limit

    let limit : Int

    The most attempts a client makes whatever a policy asks for. A6 puts it on the client because a service config can arrive over DNS.

    max_header_list_size

    let max_header_list_size : Int

    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.

    max_message_size

    let max_message_size : Int

    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.

    message_len

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

    The payload length of the length-prefixed message at at, or None when fewer than the five octets of a prefix are there.

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

    metadata_value_from_wire

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

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

    Padding is optional on the wire, and a peer may pad or not; anything outside the alphabet is skipped rather than rejected, which is what the gRPC spec asks a receiver to tolerate.

    metadata_value_to_wire

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

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

    The base64 form carries no = padding, as gRPC's own clients send it; the receiver accepts padded or unpadded alike (§6.1 of the gRPC HTTP/2 spec).

    negotiated

    What to compress a reply with, given what the server wants and what the caller said it accepts: the server's encoding when the caller listed it, identity otherwise — never a body the far end would have to guess at.

    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.

    percent_decode

    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.

    reflection_v1_path

    let reflection_v1_path : String

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

    reflection_v1alpha_path

    let reflection_v1alpha_path : String

    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.

    retry

    A6's example policy: four attempts, a hundred milliseconds doubling to a second with a fifth either way, retrying UNAVAILABLE.

    server_path

    let server_path : String

    server_sockets_path

    let server_sockets_path : String

    server_streaming

    A server-streaming handler over message types: each reply encoded in turn.

    servers_path

    let servers_path : String

    socket_path

    let socket_path : String

    split_messages

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

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

    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.

    subchannel_path

    let subchannel_path : String

    top_channels_path

    let top_channels_path : String

    unary

    A unary handler over message types: the request decoded, the reply encoded, an error raised by f answered with its status.

    Source Files