///|
/// ASCII/latin-1 `String` to `Bytes`, one octet per code unit. HTTP/2 pseudo-header
/// values (`:path`, `:authority`) and gRPC paths are ASCII, so this is exact.
fn ascii_bytes(s : String) -> Bytes {
let buf = Buffer()
for i = 0; i < s.length(); i = i + 1 {
buf.write_byte((s[i].to_int() & 0xFF).to_byte())
}
buf.to_bytes()
}
///|
/// Parse an ASCII decimal byte string (a `grpc-status` header value) as an `Int`.
/// A non-digit octet or an empty value yields `-1`, which maps to `Unknown`.
fn parse_ascii_int(b : Bytes) -> Int {
if b.length() == 0 {
return -1
}
let mut n = 0
for i = 0; i < b.length(); i = i + 1 {
let d = b[i].to_int() - 0x30
if d < 0 || d > 9 {
return -1
}
n = n * 10 + d
}
n
}
///|
/// The value of the first `@moonrpc.Header` whose name equals the ASCII `name`,
/// or `None`.
fn h2_header_value(headers : Array[@moonrpc.Header], name : String) -> Bytes? {
let want = ascii_bytes(name)
for h in headers {
if h.name == want {
return Some(h.value)
}
}
None
}
///|
/// Map a numeric `grpc-status` code back to a `@moonrpc.Status`. Anything outside
/// the canonical 0–16 range is reported as `Unknown`, matching how a gRPC client
/// treats an unrecognised code.
pub fn status_of_code(code : Int) -> @moonrpc.Status {
match code {
0 => Ok
1 => Cancelled
2 => Unknown
3 => InvalidArgument
4 => DeadlineExceeded
5 => NotFound
6 => AlreadyExists
7 => PermissionDenied
8 => ResourceExhausted
9 => FailedPrecondition
10 => Aborted
11 => OutOfRange
12 => Unimplemented
13 => Internal
14 => Unavailable
15 => DataLoss
16 => Unauthenticated
_ => Unknown
}
}
///|
/// Build a `@moonrpc.H2Server` protocol engine from this zRPC server's registered
/// handlers — the transport-facing view of the same registry `dispatch` reads.
/// Each handler is bound to its gRPC path, so a request arriving over the h2c
/// transport is dispatched to exactly the handler the group registered.
pub fn RpcServer::to_h2(self : RpcServer) -> @moonrpc.H2Server {
let engine = @moonrpc.H2Server::new()
for path, handler in self.handlers {
engine.register(path, req => handler(req))
}
for path, handler in self.server_streaming {
engine.register_server_streaming(path, (_ctx, req) => handler(req))
}
for path, handler in self.client_streaming {
engine.register_client_streaming(path, (_ctx, msgs) => handler(msgs))
}
for path, factory in self.bidi_streaming {
engine.register_bidi(path, _ctx => {
let h = factory()
{ on_message: h.on_message, on_end: h.on_end }
})
}
engine
}
///|
/// An in-process gRPC channel bound to a server engine — the client half of the
/// h2c transport. A call is carried as the real HTTP/2 frames a socket-backed
/// client would send: an HPACK-coded HEADERS block with the gRPC pseudo-headers,
/// a length-prefixed DATA frame closing the stream, and the `grpc-status` trailer
/// read back off the engine's reply. The channel's HPACK encoder pairs with the
/// engine's decoder and vice versa, so the dynamic-table state stays in lockstep
/// across every call on the channel.
pub struct RpcChannel {
engine : @moonrpc.H2Server
encoder : @moonrpc.HpackEncoder
decoder : @moonrpc.HpackDecoder
authority : String
mut next_stream_id : Int
}
///|
/// Open a channel to `server` over an in-process h2c transport, exchanging the
/// opening SETTINGS the way a real connection does. Client-initiated streams use
/// odd identifiers (RFC 7540 §5.1.1), starting at 1.
pub fn RpcChannel::connect(
server : RpcServer,
authority? : String = "localhost",
) -> RpcChannel raise {
let engine = server.to_h2()
let ch = {
engine,
encoder: @moonrpc.HpackEncoder::new(),
decoder: @moonrpc.HpackDecoder::new(),
authority,
next_stream_id: 1,
}
// Client connection preface SETTINGS; the ack is consumed and discarded.
let _ = engine.feed(Settings(params=[], ack=false))
ch
}
///|
/// Invoke a unary method at `path` with `request` as its message payload, driving
/// the call through the h2c engine and returning the reply payload on
/// `grpc-status: 0`, or the mapped `@moonrpc.Status` otherwise. `request` and the
/// returned reply are bare message bytes; the length-prefix framing is applied
/// and stripped by the transport.
pub fn RpcChannel::call(
self : RpcChannel,
path : String,
request : Bytes,
) -> Result[Bytes, @moonrpc.Status] raise {
let (sid, frames) = self.open(path)
for f in self.feed_data(sid, @moonrpc.encode_message(request), true) {
frames.push(f)
}
match self.collect_reply(sid, frames) {
(code, msgs) =>
match status_of_code(code) {
Ok => Ok(if msgs.length() > 0 { msgs[0] } else { b"" })
other => Err(other)
}
}
}
///|
/// Invoke a server-streaming method at `path`: send the single request message and
/// read back the ordered sequence of reply messages the server produced, or the
/// mapped error `@moonrpc.Status` if the stream closed with a non-zero
/// `grpc-status`. On `Ok` the array holds every message in emission order (possibly
/// empty).
pub fn RpcChannel::call_server_streaming(
self : RpcChannel,
path : String,
request : Bytes,
) -> Result[Array[Bytes], @moonrpc.Status] raise {
let (sid, frames) = self.open(path)
for f in self.feed_data(sid, @moonrpc.encode_message(request), true) {
frames.push(f)
}
match self.collect_reply(sid, frames) {
(code, msgs) =>
match status_of_code(code) {
Ok => Ok(msgs)
other => Err(other)
}
}
}
///|
/// Invoke a client-streaming method at `path`: send every message in `requests`
/// as its own DATA frame, half-close the stream, and read back the single reply.
/// An empty `requests` still opens and half-closes the stream, so the handler runs
/// with no messages.
pub fn RpcChannel::call_client_streaming(
self : RpcChannel,
path : String,
requests : Array[Bytes],
) -> Result[Bytes, @moonrpc.Status] raise {
let (sid, frames) = self.open(path)
if requests.length() == 0 {
for f in self.feed_data(sid, b"", true) {
frames.push(f)
}
} else {
for i = 0; i < requests.length(); i = i + 1 {
let last = i == requests.length() - 1
for f in self.feed_data(sid, @moonrpc.encode_message(requests[i]), last) {
frames.push(f)
}
}
}
match self.collect_reply(sid, frames) {
(code, msgs) =>
match status_of_code(code) {
Ok => Ok(if msgs.length() > 0 { msgs[0] } else { b"" })
other => Err(other)
}
}
}
///|
/// A live client-side bidirectional call over the h2c channel (← gRPC's
/// `ClientStream`): the request stream stays open while messages flow both ways.
/// `send` writes one request message and returns whatever replies the server
/// produced right then (bidi interleaving — an echo handler answers each message
/// as it arrives); `close_send` half-closes the request stream, runs the server's
/// `on_end`, and reports the final `grpc-status`. The channel's HPACK decoder is
/// advanced across every reply block, so its dynamic table stays in lockstep with
/// the engine's encoder for the life of the call. `pending` holds DATA octets not
/// yet split into a whole length-prefixed message (a message may straddle two DATA
/// frames under flow control).
pub struct BidiCall {
channel : RpcChannel
sid : Int
mut pending : Bytes
mut status_code : Int
mut ended : Bool
}
///|
/// Open a bidirectional stream to `path`, sending the request HEADERS without
/// half-closing so the stream stays open for interleaved `send`s. An unregistered
/// path answers trailers-only UNIMPLEMENTED during this HEADERS feed, which the
/// returned call captures as its status.
pub fn RpcChannel::open_bidi(
self : RpcChannel,
path : String,
) -> BidiCall raise {
let (sid, frames) = self.open(path)
let call = { channel: self, sid, pending: b"", status_code: -1, ended: false }
let _ = call.absorb(frames)
call
}
///|
/// Fold a batch of reply frames into the call: HPACK-decode every HEADERS block
/// for this stream (keeping the decoder's dynamic table in sync and picking up
/// `grpc-status` when it appears), append this stream's DATA to `pending`, and
/// return the reply messages that are now complete, leaving any partial tail
/// buffered.
fn BidiCall::absorb(
self : BidiCall,
frames : Array[@moonrpc.Frame],
) -> Array[Bytes] raise {
let acc = Buffer()
acc.write_bytes(self.pending)
for f in frames {
match f {
Headers(stream_id~, fragment~, ..) =>
if stream_id == self.sid {
let headers = self.channel.decoder.decode(fragment)
match h2_header_value(headers, "grpc-status") {
Some(v) => self.status_code = parse_ascii_int(v)
None => ()
}
}
Data(stream_id~, data~, ..) =>
if stream_id == self.sid {
acc.write_bytes(data)
}
_ => ()
}
}
let (msgs, rest) = drain_messages(acc.to_bytes())
self.pending = rest
msgs
}
///|
/// Send one request message on the open stream and return the replies the server
/// emitted in response to it (possibly empty). A no-op once the stream is
/// half-closed.
pub fn BidiCall::send(self : BidiCall, msg : Bytes) -> Array[Bytes] raise {
guard !self.ended else { return [] }
let frames = self.channel.feed_data(
self.sid,
@moonrpc.encode_message(msg),
false,
)
self.absorb(frames)
}
///|
/// Half-close the request stream: run the server's `on_end`, return its final
/// reply messages, and map the `grpc-status` trailer to `Ok`/`Err`. Calling it a
/// second time is an error (`Cancelled`).
pub fn BidiCall::close_send(
self : BidiCall,
) -> Result[Array[Bytes], @moonrpc.Status] raise {
guard !self.ended else { return Err(@moonrpc.Status::Cancelled) }
self.ended = true
let frames = self.channel.feed_data(self.sid, b"", true)
let msgs = self.absorb(frames)
match status_of_code(self.status_code) {
Ok => Ok(msgs)
other => Err(other)
}
}
///|
/// Drive a whole bidirectional call at `path` in one shot: send every message in
/// `requests` (collecting the interleaved replies in order), then half-close and
/// append the `on_end` replies. The result is every reply message the server
/// produced, in emission order, or the non-zero `grpc-status` the stream closed
/// with.
pub fn RpcChannel::call_bidi_streaming(
self : RpcChannel,
path : String,
requests : Array[Bytes],
) -> Result[Array[Bytes], @moonrpc.Status] raise {
let call = self.open_bidi(path)
let out : Array[Bytes] = []
for r in requests {
for m in call.send(r) {
out.push(m)
}
}
match call.close_send() {
Ok(final_msgs) => {
for m in final_msgs {
out.push(m)
}
Ok(out)
}
Err(status) => Err(status)
}
}
///|
/// Split a run of concatenated gRPC length-prefixed messages into their payloads,
/// returning the complete messages and any trailing partial-message octets that
/// have not yet arrived in full. Like `decode_all_messages` but surfaces the
/// remainder so a streaming caller can carry it across DATA frames.
fn drain_messages(body : Bytes) -> (Array[Bytes], Bytes) {
let out : Array[Bytes] = []
let n = body.length()
let mut off = 0
while n - off >= 5 {
let len = (body[off + 1].to_int() << 24) |
(body[off + 2].to_int() << 16) |
(body[off + 3].to_int() << 8) |
body[off + 4].to_int()
if n - off < 5 + len {
break
}
out.push(body[off + 5:off + 5 + len].to_owned())
off = off + 5 + len
}
(out, body[off:n].to_owned())
}
///|
/// Allocate the next client stream id and send the request HEADERS (the gRPC
/// pseudo-headers + `content-type`/`te`), returning the id and any frames the engine
/// emitted right away — an unregistered path answers with its trailers-only
/// UNIMPLEMENTED during this HEADERS feed, before any DATA. Client-initiated streams
/// use odd identifiers advancing by two (RFC 7540 §5.1.1).
fn RpcChannel::open(
self : RpcChannel,
path : String,
) -> (Int, Array[@moonrpc.Frame]) raise {
let sid = self.next_stream_id
self.next_stream_id = self.next_stream_id + 2
let block = self.encoder.encode([
{ name: b":method", value: b"POST" },
{ name: b":scheme", value: b"http" },
{ name: b":path", value: ascii_bytes(path) },
{ name: b":authority", value: ascii_bytes(self.authority) },
{ name: b"content-type", value: b"application/grpc" },
{ name: b"te", value: b"trailers" },
])
let frames = self.engine.feed(
Headers(
stream_id=sid,
fragment=block,
end_stream=false,
end_headers=true,
priority=None,
padding=0,
),
)
(sid, frames)
}
///|
/// Feed one DATA frame carrying `payload` on stream `sid`, half-closing the stream
/// when `end` is set, and return the reply frames the engine emitted.
fn RpcChannel::feed_data(
self : RpcChannel,
sid : Int,
payload : Bytes,
end : Bool,
) -> Array[@moonrpc.Frame] raise {
self.engine.feed(Data(stream_id=sid, data=payload, end_stream=end, padding=0))
}
///|
/// Decode the engine's reply frames for stream `sid`: concatenate DATA payloads,
/// HPACK-decode every HEADERS block in arrival order (keeping the decoder's
/// dynamic table in sync) to find `grpc-status`, then split the body into its
/// length-prefixed messages. Returns the status code (`-1`, i.e. `Unknown`, when no
/// `grpc-status` was seen) and the decoded messages in order.
fn RpcChannel::collect_reply(
self : RpcChannel,
sid : Int,
frames : Array[@moonrpc.Frame],
) -> (Int, Array[Bytes]) raise {
let body = Buffer()
let mut status_code : Int = -1
for f in frames {
match f {
Headers(stream_id~, fragment~, ..) =>
if stream_id == sid {
let headers = self.decoder.decode(fragment)
match h2_header_value(headers, "grpc-status") {
Some(v) => status_code = parse_ascii_int(v)
None => ()
}
}
Data(stream_id~, data~, ..) =>
if stream_id == sid {
body.write_bytes(data)
}
_ => ()
}
}
(status_code, decode_all_messages(body.to_bytes()))
}
///|
/// Split a run of concatenated gRPC length-prefixed messages into their payloads,
/// stopping at the first truncated frame. Each message is a 1-byte compression flag
/// plus a 4-byte big-endian length plus that many payload octets.
fn decode_all_messages(body : Bytes) -> Array[Bytes] {
let out : Array[Bytes] = []
let n = body.length()
let mut off = 0
while n - off >= 5 {
let len = (body[off + 1].to_int() << 24) |
(body[off + 2].to_int() << 16) |
(body[off + 3].to_int() << 8) |
body[off + 4].to_int()
if n - off < 5 + len {
break
}
out.push(body[off + 5:off + 5 + len].to_owned())
off = off + 5 + len
}
out
}