moonzero

moonzero — a service framework for MoonBit (← go-zero): config-driven assembly of a moonapi application with middleware, producing a runnable moonasgi AsgiApp. v0.6.2 runs real unary, server/client-streaming, and bidirectional zRPC calls over moonrpc's h2c transport (an in-process RpcChannel drives HPACK HEADERS + length-prefixed DATA + grpc-status trailers through the H2Server engine), graceful shutdown draining in-flight calls, a persisted, watchable etcd v3-shaped registry (watch, events_since catch-up, snapshot/restore) with round-robin/pick-first balancers and a load-balanced client, real file-backed registry I/O over moonbitlang/async's fs with a live filesystem watcher (native discov driver), request metrics (counter + latency histogram), and W3C traceparent propagation.

go-zero
microservice
server
middleware
moonapi
moonbit
moon add Lfan-ke/moonzero@0.6.2
Download zip
Author
Version
0.6.2
License
Apache-2.0
Last updated
18 days ago
Downloads
32
README

#moonzero

A service framework for MoonBit — ← go-zero.

Check and Test License mooncakes

moonzero is the integration layer of the moon* suite — the role go-zero plays for Go. It assembles a moonapi application from config, wraps it in middleware, and produces a runnable moonasgi AsgiApp that a server (mooncat) runs. It depends only on moonapi + moonasgi, so it stays backend-agnostic.

flowchart LR conf["ServiceConf"] --> srv["**moonzero** Server"] api["moonapi App"] --> srv mw["middleware<br/>(logging, ...)"] --> srv srv -->|"to_asgi()"| asgi(["moonasgi AsgiApp"]) asgi --> cat["mooncat serves it"]

#Quickstart

let app = @moonapi.App::new()
let api = @moonzero.Group::new(app, "/api/v1") // prefix a set of routes
api.get("/ping", _ctx => @moonapi.text(200, "pong"))

let conf = @moonzero.ServiceConf::new(
name="greet", host="127.0.0.1", port=8888, timeout_ms=3000, log_level=Info,
)
let server = @moonzero.Server::new(conf, app)
.use_(@moonzero.cors(@moonzero.CorsConf::new())) // Access-Control-* headers
.use_(@moonzero.request_id()) // x-request-id per request
.use_(@moonzero.recovery) // 500 instead of a panic
.use_(@moonzero.logging)

server.describe() // "greet listening on 127.0.0.1:8888"
@mooncat.serve(server.to_asgi(), host=conf.host, port=conf.port) // run it (native)

Verified across all backends (wasm, wasm-gc, js, native) in CI, 0 warnings under --deny-warn.

#Resilience middleware

Beyond the base onion (logging, recovery, CORS, request-id), moonzero ships go-zero's resilience set, each modelled as a pure decision core over an injected Clock so its timing is exactly testable:

let clock = @moonzero.Clock::new(() => now_ms()) // real time at the async edge
let server = @moonzero.Server::new(conf, app)
.use_(@moonzero.maxbytes(1 << 20)) // 413 over 1 MiB
.use_(@moonzero.rate_limit(@moonzero.TokenBucket::new(100.0), clock))// 429 when empty
.use_(@moonzero.breaker(@moonzero.Breaker::new(), clock)) // 503 while open
.use_(@moonzero.timeout(3000L, clock)) // deadline
.use_(@moonzero.structured_logging(clock)) // one JSON line/req

  • rate_limit — a TokenBucket: admits a burst, refills continuously, answers 429 when empty.
  • breaker — a Breaker closed/open/half-open state machine: trips after K consecutive failures, fails fast with 503, then admits half-open probes to test recovery.
  • timeout — a Deadline enforced on the response path. Preemptively aborting a hung handler needs racing it against a timer (@async.any) under the native runtime; that race is wired at the async server edge, the deadline core is portable and tested.
  • maxbytes — rejects a request whose declared Content-Length exceeds the limit with 413.
  • structured_logging — a RequestLog rendered as one JSON line per request (method, path, status, duration, request-id, client-ip, user-agent).

#Auth, YAML config, and zRPC over the h2c transport

// JWT HS256 — self-built SHA-256/HMAC (verified against NIST/RFC vectors)
let token = @moonzero.jwt_sign(
Map([("sub", Json::string("alice")), ("exp", Json::number(1893456000.0))]),
"topsecret",
)
let server = @moonzero.Server::new(conf, app)
.use_(@moonzero.auth("topsecret", clock)) // 401 unless a valid Bearer JWT
.use_(@moonzero.tracing()) // W3C traceparent in/out
.use_(@moonzero.metrics(m, clock)) // request counter + latency histogram

// YAML config — the etc/*.yaml format go-zero ships, same lenient defaults as JSON
let conf = @moonzero.ServiceConf::from_yaml("name: greet\nport: 9000\nlog_level: error\n")

// A real zRPC call over moonrpc's h2c transport — unary and streaming
let rpc = @moonzero.RpcServer::new(@moonzero.RpcServerConf::new(name="greeter", port=9090))
let g = rpc.group("hello.Greeter")
g.register("SayHello", req => handle(req)) // unary
g.register_server_streaming("Tail", req => chunks(req)) // one in, many out
g.register_client_streaming("Upload", msgs => summarize(msgs)) // many in, one out
g.register_bidi_streaming("Chat", () => @moonzero.BidiStreamHandler::{
on_message: m => [echo(m)], // a reply the instant each message arrives
on_end: () => [b"bye"], // a farewell after the client half-closes
})
let ch = @moonzero.RpcChannel::connect(rpc)
ch.call("/hello.Greeter/SayHello", request) // Ok(reply) | Err(status)
ch.call_server_streaming("/hello.Greeter/Tail", request) // Ok([msg, ...]) | Err(status)
ch.call_client_streaming("/hello.Greeter/Upload", [a, b]) // Ok(reply) | Err(status)
let call = ch.open_bidi("/hello.Greeter/Chat") // stream both ways
call.send(a) // -> the replies produced right then (interleaved)
call.close_send() // -> Ok([on_end replies...]) | Err(status)

  • jwt_sign / jwt_verify — compact HS256 tokens on a self-built SHA-256 + HMAC-SHA256, signatures compared in constant time, exp/nbf enforced, and the alg:none downgrade refused. Interop-verified against the canonical jwt.io token.
  • auth — the middleware that requires Authorization: Bearer <jwt> and answers 401 for an absent, malformed, tampered, or expired token.
  • ServiceConf::from_yaml — a minimal-subset YAML parser (block maps, nesting, sequences, typed scalars, comments) feeding the same field reader as the JSON loader, so both formats agree field-for-field.
  • RpcServer / RpcGroupconfig-driven zRPC groups that register moonrpc Method handlers by gRPC path.
  • RpcChannel — a client over the h2c transport: to_h2 turns the registered handlers into a moonrpc H2Server, and the call family runs real exchanges through it — HPACK-coded HEADERS, length-prefixed DATA frames, and the grpc-status trailer read back off the reply. Unary (call), server-streaming (call_server_streaming, one request then every framed reply in order), client-streaming (call_client_streaming, each request as its own DATA frame then one reply after half-close), and bidirectional streaming all round-trip through the same engine. A call to an unregistered path comes back UNIMPLEMENTED, the trailers-only response a gRPC server sends for an unknown method.
  • Bidi streamingopen_bidi opens a stream that stays open both ways: each BidiCall::send writes one request message and returns the replies the server produced right then (an echo handler answers each message as it arrives), and close_send half-closes, runs the server's on_end, and reports the final grpc-status. call_bidi_streaming drives a whole exchange in one shot, returning the interleaved replies followed by the on_end messages. 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.
  • ShutdownCoordinatorgraceful shutdown for a serving zRPC server: dispatch_graceful counts each call as in-flight for its duration, initiate_shutdown makes new calls come back Unavailable (a stopped listener) while in-flight ones run to completion, and is_drained reports when the last one has finished so the process may exit.

#Discovery, metrics, tracing

// Persisted, watchable registry (etcd v3-shaped): register, watch, snapshot
let reg = @moonzero.PersistentRegistry::new()
reg.watch(e => log(e)) // Put/Delete events in revision order
reg.register("greeter", @moonzero.Endpoint::new("10.0.0.1", 9090))
reg.register("greeter", @moonzero.Endpoint::new("10.0.0.2", 9090))
let saved = reg.snapshot() // persist to a file/etcd; restore reloads it

// Real registry I/O over a file (native): a publisher persists, a reader watches
@discov.persist_registry(path, reg) // write the snapshot to a real file
let reader = @discov.FileRegistry::load(path) // load it back
reader.reload() // re-read -> the Put/Delete diff since last load
reader.watch(dir, e => log(e)) // reload on every filesystem change

// Load-balanced client: resolve an instance, then call it over h2c
let ch = @moonzero.LoadBalancedChannel::new(reg.resolver(), cluster, "greeter")
ch.call("/hello.Greeter/SayHello", request) // dials 10.0.0.1, then .2, cycling

// Metrics read out for a /metrics scrape after serving
let m = @moonzero.ServerMetrics::new()
m.requests().value("GET /ping 200") // request count for that label
m.latency().mean() // mean request latency, ms

  • InMemoryRegistry — a service registry shaped like go-zero's etcd discov store: a service -> instance -> endpoint map with a monotonic store revision a watcher can compare against. RoundRobin and pick_first balancers select an endpoint from a resolved set.
  • PersistentRegistry — the persisted, watchable registry: adds a live watch (Put/Delete events in revision order), an events_since catch-up from any revision, and snapshot/restore that round-trip the whole keyspace through an etcd v3 RangeResponse-shaped JSON document without losing a revision — the bytes a file- or etcd-backed deployment persists and reloads.
  • LoadBalancedChannel — a load-balanced zRPC client over a Resolve interface: it resolves a service, picks a live instance with the balancer, dials it through an RpcCluster, and makes the call, so instances registering or leaving between calls take effect on the next one. Any store exposing resolver() backs it; an etcd- or consul-backed store drops in unchanged. Unary, server-streaming, and bidi calls all go through the resolve-then-balance path.
  • FileRegistry — real registry I/O in the native discov sub-package (← go-zero's discov publisher/subscriber, over the filesystem instead of etcd's network): persist_registry writes the snapshot to a real file through moonbitlang/async's fs, FileRegistry::load reads it back and exposes a resolver(), reload re-reads and returns the Put/Delete diff since the last load, and watch/watch_once block on a real filesystem watcher and reload on every change. Reader and publisher share only the file, exactly as an etcd subscriber and publisher share only the keyspace, so the balancer and load-balanced channel above drive it unchanged.
  • ServerMetrics — a CounterVec of per-method/route/status request tallies and a cumulative latency Histogram (Prometheus le buckets, sum, count), driven by the metrics middleware that times each request on the clock.
  • tracingtrace-id propagation: continue an inbound W3C traceparent or start a new trace, mint a child span, and stamp traceparent + x-trace-id onto the response.

#Roadmap (transliterating go-zero)

Typed config (JSON + YAML with defaults, timeout + log level) + service assembly + the base middleware onion (logging, recovery, CORS, request-id) + the resilience set (timeout, rate-limit, breaker, maxbytes, structured logging) + route groups + JWT auth + zRPC groups with real unary, server/client-streaming, and bidirectional h2c round-trips + graceful shutdown draining in-flight calls + a persisted, watchable registry with round-robin/pick-first balancers and a load-balanced client + real file-backed registry I/O with a live filesystem watcher + request metrics + trace-id propagation are here. Next: an etcd/consul network client behind the same Resolve interface, and moonctl-driven scaffolding of a full moonzero service from a spec.

#License

Apache-2.0.

#
BidiStreamFactory

type BidiStreamFactory = () -> BidiStreamHandler

A factory that mints one BidiStreamHandler per call, so each stream gets its own handler state (← the fresh ServerStream gRPC hands every bidi invocation).

#
ClientStreamHandler

type ClientStreamHandler = (Array[Bytes]) -> Bytes

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

#
Middleware

type Middleware = (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit)

An AsgiApp transformer — one layer of the middleware onion.

#
Resolve

type Resolve = (String) -> Array[Endpoint]

The resolve half of go-zero's discovery (← discov.Discovery): a function from a service name to its live endpoints. Any store — the in-memory InMemoryRegistry, the persisted PersistentRegistry, or a future etcd/consul client — exposes one via resolver(), so a balancer and the load-balanced channel are written once against the interface and the backing store swaps by swapping the closure.

#
RpcHandler

type RpcHandler = (Bytes) -> Bytes

A unary RPC handler: it maps a request message's wire bytes to a response message's wire bytes (the application/grpc+proto payload, sans the length-prefix framing that @moonrpc.encode_message adds). Streaming handlers arrive with the h2 transport; this is the unary shape zRPC registers today.

#
ServerStreamHandler

type ServerStreamHandler = (Bytes) -> Array[Bytes]

A server-streaming handler: one request message in, an ordered sequence of response messages out (each framed as its own length-prefixed gRPC message). Mirrors go-zero's pb.XxxServer server-streaming method, which writes to a grpc.ServerStream instead of returning one reply.

#
ConfigError

pub suberror ConfigError {
ConfigError(String)
}

A config-loading failure (← go-zero's conf.Load errors): malformed JSON, a non-object root, or a field of the wrong type, with a human-readable reason.

#
JwtError

pub suberror JwtError {
MalformedToken(String)
UnsupportedAlg(String)
BadSignature
Expired
NotYetValid
}

A JWT verification failure (← go-zero's handler.Authorize rejection cases). Every path a bad token can fail on is reported distinctly so callers (and the auth middleware) can log or branch precisely.

#
Balancer

pub(all) enum Balancer {
RoundRobinBalancer(RoundRobin)
PickFirst
}

The choice of balancer for a load-balanced channel: round-robin cycles through the resolved instances (spreading load), PickFirst pins the head instance (← gRPC's pick_first).

#
Balancer::pick

fn Balancer::pick(self : Balancer, endpoints : Array[Endpoint]) -> Endpoint?

Pick one endpoint from a resolved set, or None if it is empty.

#
Balancer::round_robin

fn Balancer::round_robin() -> Balancer

A round-robin balancer, cursor at the first instance.

#
BidiCall

pub struct BidiCall {
channel : RpcChannel
sid : Int
pending : Bytes
status_code : Int
ended : Bool
}

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

#
BidiCall::close_send

fn BidiCall::close_send(self : BidiCall) -> Result[Array[Bytes],
Status
] raise

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

#
BidiCall::send

fn BidiCall::send(self : BidiCall, msg : Bytes) -> Array[Bytes] raise

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.

#
BidiStreamHandler

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

A live bidirectional call (← go-zero's pb.XxxServer bidi method, which reads from and writes to the same grpc.ServerStream): on_message fires once per request message and returns the replies to send right then, so responses interleave with requests; on_end runs after the client half-closes and returns the final replies before the grpc-status trailer. The moonzero-local mirror of @moonrpc.BidiHandler, so callers register bidi methods without naming the transport package.

#
Breaker

pub struct Breaker {
max_failures : Int
open_ms : Int64
half_open_max : Int
state : BreakerState
failures : Int
opened_at : Int64
probes : Int
}

A circuit breaker as an explicit open/half-open/closed state machine over a clock (← go-zero's breaker.Breaker; go-zero's default is Google's SRE adaptive algorithm, but the canonical state machine is the faithful, testable core and is exposed here). Trips after max_failures consecutive failures, stays Open for open_ms, then admits up to half_open_max probes; one probe success closes it, one probe failure re-opens it.

#
Breaker::allow

fn Breaker::allow(self : Breaker, now : Int64) -> Bool

Decide whether a request may proceed at time now, advancing the state machine as a side effect:

  • Closed — always admitted.
  • Open — rejected until open_ms has elapsed since it tripped, at which point it moves to HalfOpen and admits this request as the first probe.
  • HalfOpen — admitted while fewer than half_open_max probes are outstanding, otherwise rejected.

Returns true to admit, false to fail fast.

#
Breaker::new

fn Breaker::new(max_failures? : Int, open_ms? : Int64, half_open_max? : Int) -> Breaker

Build a closed breaker that trips after max_failures consecutive failures (default 5), stays open for open_ms milliseconds (default 5000), and admits half_open_max probes while half-open (default 1).

#
Breaker::record_failure

fn Breaker::record_failure(self : Breaker, now : Int64) -> Unit

Record that an admitted request failed at time now. In Closed it extends the failure streak and trips Open once it reaches max_failures; a HalfOpen probe failure re-opens the breaker immediately.

#
Breaker::record_success

fn Breaker::record_success(self : Breaker) -> Unit

Record that an admitted request succeeded. In Closed it resets the failure streak; in HalfOpen a probe success closes the breaker.

#
Breaker::state

fn Breaker::state(self : Breaker) -> BreakerState

The breaker's current state (after any pending OpenHalfOpen transition is applied by allow).

#
BreakerState

pub(all) enum BreakerState {
Closed
Open
HalfOpen
} derive(Eq)

The three states of a circuit breaker (← go-zero's breaker package). A Closed breaker lets traffic through; after too many failures it trips Open and fails fast; once its cool-down elapses it goes HalfOpen and lets a few probe requests through to test recovery.

#
BreakerState::to_string

fn BreakerState::to_string(self : BreakerState) -> String

The state's lowercase name, for logs and metrics.

#
Clock

pub struct Clock {
now_ms : () -> Int64
}

A monotonic time source in milliseconds, injected into the resilience middlewares (rate-limit, breaker, timeout) so their timing logic is a pure function of an explicit clock rather than a hidden wall-clock read. go-zero reads timex.Now() directly; because that is neither portable across MoonBit's backends nor testable, moonzero threads the clock as a value — the same pattern Go's clockwork/x/time/rate accept for a Clock.

#
Clock::new

fn Clock::new(now_ms : () -> Int64) -> Clock

Wrap a now-in-milliseconds thunk as a Clock.

#
Clock::now

fn Clock::now(self : Clock) -> Int64

The current time in milliseconds, as reported by the wrapped source.

#
CorsConf

pub(all) struct CorsConf {
allow_origin : String
allow_methods : String
allow_headers : String
allow_credentials : Bool
max_age : Int
}

CORS configuration (← go-zero's cors.Middleware options): the values echoed back in the Access-Control-* preflight/response headers.

#
CorsConf::new

fn CorsConf::new(allow_origin? : String, allow_methods? : String, allow_headers? : String, allow_credentials? : Bool, max_age? : Int) -> CorsConf

Build a permissive CORS config: any origin, the full method set, and a one-day preflight cache. Credentials are off by default, matching go-zero.

#
CounterVec

pub struct CounterVec {
counts : Map[String, Int64]
}

A monotonic counter (← go-zero's metric.CounterVec) partitioned by a label string. Each inc/add accrues against one label (e.g. "GET /ping 200"), so a single vector holds the per-method/route/status request tallies Prometheus scrapes. Counters only ever go up.

#
CounterVec::add

fn CounterVec::add(self : CounterVec, label : String, delta : Int64) -> Unit

Add delta to label's count (creating the series on first sight).

#
CounterVec::inc

fn CounterVec::inc(self : CounterVec, label : String) -> Unit

Increment label's count by one.

#
CounterVec::labels

fn CounterVec::labels(self : CounterVec) -> Array[String]

The set of labels that have been observed.

#
CounterVec::new

fn CounterVec::new() -> CounterVec

A fresh counter vector with no labels seen yet.

#
CounterVec::total

fn CounterVec::total(self : CounterVec) -> Int64

The sum of every label's count — the total number of observations.

#
CounterVec::value

fn CounterVec::value(self : CounterVec, label : String) -> Int64

The current count for label, 0 if never touched.

#
Deadline

pub struct Deadline {
budget_ms : Int64
started_ms : Int64
}

A request deadline (← go-zero's timeout middleware's context.WithTimeout): a budget in milliseconds measured from a start instant on the shared clock. A budget_ms <= 0 means "no deadline" and never expires — go-zero's convention for a disabled timeout.

#
Deadline::expired

fn Deadline::expired(self : Deadline, now : Int64) -> Bool

Whether the deadline has passed at time now. A non-positive budget never expires.

#
Deadline::remaining

fn Deadline::remaining(self : Deadline, now : Int64) -> Int64

Milliseconds left before the deadline at time now (never negative); -1 for a disabled (non-positive-budget) deadline, which has no finite remaining.

#
Deadline::start

fn Deadline::start(budget_ms : Int64, now : Int64) -> Deadline

Start a deadline of budget_ms milliseconds at time now.

#
Endpoint

pub(all) struct Endpoint {
host : String
port : Int
weight : Int
} derive(Eq,
Debug
)

A service endpoint (← go-zero's discov target): the host and port an instance listens on, plus a routing weight the balancer honours (default 1).

#
Endpoint::address

fn Endpoint::address(self : Endpoint) -> String

The host:port dial string.

#
Endpoint::new

fn Endpoint::new(host : String, port : Int, weight? : Int) -> Endpoint

Build an endpoint; weight defaults to 1, matching an unweighted instance.

#
Group

pub struct Group {
app :
App

prefix : String
}

A route group (← go-zero's RouteGroup): registers a set of routes on an underlying moonapi.App under a shared path prefix, so related endpoints (e.g. everything under /api/v1) are declared without repeating the prefix.

#
Group::delete

fn Group::delete(self : Group, path : String, handler : (
Context
) ->
Response
raise, summary? : String) -> Unit

Register a DELETE route under the group's prefix.

#
Group::get

fn Group::get(self : Group, path : String, handler : (
Context
) ->
Response
raise, summary? : String) -> Unit

Register a GET route under the group's prefix.

#
Group::new

fn Group::new(app :
App
, prefix : String) -> Group

Open a group that prefixes every route it registers with prefix on app.

#
Group::patch

fn Group::patch(self : Group, path : String, handler : (
Context
) ->
Response
raise, summary? : String) -> Unit

Register a PATCH route under the group's prefix.

#
Group::post

fn Group::post(self : Group, path : String, handler : (
Context
) ->
Response
raise, summary? : String) -> Unit

Register a POST route under the group's prefix.

#
Group::prefix

fn Group::prefix(self : Group) -> String

The prefix this group joins onto each registered route.

#
Group::put

fn Group::put(self : Group, path : String, handler : (
Context
) ->
Response
raise, summary? : String) -> Unit

Register a PUT route under the group's prefix.

#
Group::route

fn Group::route(self : Group, verb :
Method
, path : String, handler : (
Context
) ->
Response
raise, summary? : String) -> Unit

Register a route for an explicit method under the group's prefix.

#
Histogram

pub struct Histogram {
bounds : Array[Double]
bucket : Array[Int64]
sum : Double
count : Int64
}

A cumulative histogram (← go-zero's metric.HistogramVec, Prometheus semantics): a sorted list of le (less-than-or-equal) upper bounds and, for each, the count of observations that fell at or below it, plus the running sum and total count. An observation above every bound still lands in the implicit +Inf bucket that count represents.

#
Histogram::bounds

fn Histogram::bounds(self : Histogram) -> Array[Double]

The upper bounds this histogram partitions on.

#
Histogram::bucket_count

fn Histogram::bucket_count(self : Histogram, i : Int) -> Int64

The cumulative count in the bucket bounded by bounds[i] — how many observations were <= that bound.

#
Histogram::mean

fn Histogram::mean(self : Histogram) -> Double

The mean of the observations, or 0 when none have been recorded.

#
Histogram::new

fn Histogram::new(bounds? : Array[Double]) -> Histogram

A histogram over bounds (defaulting to default_latency_buckets). The bounds are taken as given; supply them in ascending order, as Prometheus requires.

#
Histogram::observe

fn Histogram::observe(self : Histogram, value : Double) -> Unit

Record one observation: it lands in every bucket whose le bound it does not exceed (cumulative), and updates the sum and count.

#
Histogram::sum_value

fn Histogram::sum_value(self : Histogram) -> Double

The sum of all observed values (Prometheus _sum).

#
Histogram::total

fn Histogram::total(self : Histogram) -> Int64

The total number of observations (the +Inf bucket count).

#
InMemoryRegistry

pub struct InMemoryRegistry {
instances : Map[String, Map[String, Endpoint]]
seq : Int
revision : Int64
}

An in-memory service registry (← go-zero's etcd discov store, minus the network): a two-level map of service -> instance-id -> endpoint and a monotonic revision bumped on every mutation, mirroring etcd's store revision so a watcher could detect change. Instance ids are <service>/<n>, the leaf of the etcd key an instance would lease.

#
InMemoryRegistry::deregister

fn InMemoryRegistry::deregister(self : InMemoryRegistry, service : String, key : String) -> Bool

Remove the instance at key from service. Returns true if it existed (and bumps the revision), false if the service or key was unknown.

#
InMemoryRegistry::new

A fresh, empty registry at revision 0.

#
InMemoryRegistry::register

fn InMemoryRegistry::register(self : InMemoryRegistry, service : String, endpoint : Endpoint) -> String

Register endpoint under service and return its instance key. Each call mints a distinct key, so two instances of one service coexist, and bumps the revision.

#
InMemoryRegistry::resolve

fn InMemoryRegistry::resolve(self : InMemoryRegistry, service : String) -> Array[Endpoint]

The endpoints registered for service, in registration order.

#
InMemoryRegistry::resolver

fn InMemoryRegistry::resolver(self : InMemoryRegistry) -> ((String) -> Array[Endpoint])

This registry as a Resolve interface value.

#
InMemoryRegistry::revision

fn InMemoryRegistry::revision(self : InMemoryRegistry) -> Int64

The store revision, incremented on each register/deregister — etcd's mod-revision, the value a watcher compares against to see new state.

#
InMemoryRegistry::services

fn InMemoryRegistry::services(self : InMemoryRegistry) -> Array[String]

Every service name with at least one live instance.

#
LoadBalancedChannel

pub struct LoadBalancedChannel {
resolve : (String) -> Array[Endpoint]
cluster : RpcCluster
balancer : Balancer
service : String
}

A load-balanced zRPC client (← go-zero's zrpc.Client over a discovery target): it resolves a service through the Resolver, picks a live instance with the Balancer, dials it on the RpcCluster, and makes the call. The whole resolve→balance→dial→call path runs per call, so instances registering or deregistering between calls take effect on the next one.

#
LoadBalancedChannel::call

fn LoadBalancedChannel::call(self : LoadBalancedChannel, path : String, request : Bytes) -> Result[Bytes,
Status
] raise

Make a unary call to path, resolving and balancing to a live instance first.

#
LoadBalancedChannel::call_bidi_streaming

fn LoadBalancedChannel::call_bidi_streaming(self : LoadBalancedChannel, path : String, requests : Array[Bytes]) -> Result[Array[Bytes],
Status
] raise

Make a bidirectional-streaming call to path, resolving and balancing to a live instance first, then driving the whole requests exchange to completion.

#
LoadBalancedChannel::call_server_streaming

fn LoadBalancedChannel::call_server_streaming(self : LoadBalancedChannel, path : String, request : Bytes) -> Result[Array[Bytes],
Status
] raise

Make a server-streaming call to path, resolving and balancing to a live instance first.

#
LoadBalancedChannel::new

fn LoadBalancedChannel::new(resolve : (String) -> Array[Endpoint], cluster : RpcCluster, service : String, balancer? : Balancer) -> LoadBalancedChannel

Build a load-balanced channel for service over a Resolve interface, dialing through cluster with balancer (round-robin by default).

#
LogLevel

pub(all) enum LogLevel {
Debug
Info
Error
Severe
} derive(Compare, Eq,
Debug
)

Log verbosity (← go-zero's LogConf.Level), ordered from most to least verbose. Compare follows that order so thresholds can be tested directly.

#
LogLevel::parse

fn LogLevel::parse(s : String) -> LogLevel

Parse a level name, falling back to Info for anything unrecognised — the same lenient default go-zero applies to a missing/empty level.

#
LogLevel::to_string

fn LogLevel::to_string(self : LogLevel) -> String

The canonical lowercase name go-zero uses on the wire.

#
ManualClock

pub struct ManualClock {
ms : Int64
}

A deterministic, hand-advanced clock for tests and for driving the rate-limit / breaker cores without a real time source. Wall time is replaced by an explicit advance, so a token bucket's refill or a breaker's open window can be exercised exactly.

#
ManualClock::advance

fn ManualClock::advance(self : ManualClock, delta : Int64) -> Unit

Move the manual clock forward by delta milliseconds.

#
ManualClock::as_clock

fn ManualClock::as_clock(self : ManualClock) -> Clock

A Clock view over this manual clock: reading it reflects every advance.

#
ManualClock::new

fn ManualClock::new(start? : Int64) -> ManualClock

A manual clock starting at start milliseconds (default 0).

#
PersistentRegistry

pub struct PersistentRegistry {
instances : Map[String, Map[String, Endpoint]]
key_rev : Map[String, Int64]
seq : Int
revision : Int64
events : Array[RegistryEvent]
watchers : Array[(RegistryEvent) -> Unit]
}

A persisted, watchable service registry (← go-zero's etcd discov publisher): the same two-level service -> instance-id -> endpoint store as InMemoryRegistry, plus a per-key mod-revision, an append-only event log for catch-up watchers, live watcher callbacks fired on every mutation, and snapshot/restore that round-trip the whole keyspace through an etcd v3 RangeResponse-shaped JSON document — the bytes a file- or etcd-backed deployment persists and reloads without losing a revision.

#
PersistentRegistry::deregister

fn PersistentRegistry::deregister(self : PersistentRegistry, service : String, key : String) -> Bool

Remove the instance at key from service. On success bumps the revision and emits a Delete; an unknown service or key is a no-op returning false.

#
PersistentRegistry::events_since

fn PersistentRegistry::events_since(self : PersistentRegistry, revision : Int64) -> Array[RegistryEvent]

Every event with a revision greater than revision (← etcd's watch start_revision): the catch-up a client replays to reach the current state before switching to live watch callbacks.

#
PersistentRegistry::keyed_entries

fn PersistentRegistry::keyed_entries(self : PersistentRegistry) -> Array[(String, Endpoint, Int64)]

Every registered instance as (instance-key, endpoint, mod-revision), across all services — the flat keyspace a file- or etcd-backed reader diffs one load against the next to compute the Put/Delete events a change produced.

#
PersistentRegistry::new

A fresh, empty persisted registry at revision 0.

#
PersistentRegistry::register

fn PersistentRegistry::register(self : PersistentRegistry, service : String, endpoint : Endpoint) -> String

Register endpoint under service, mint a fresh instance key, bump the revision, and emit a Put. Returns the instance key (<service>/<n>).

#
PersistentRegistry::resolve

fn PersistentRegistry::resolve(self : PersistentRegistry, service : String) -> Array[Endpoint]

The endpoints registered for service, in registration order.

#
PersistentRegistry::resolver

fn PersistentRegistry::resolver(self : PersistentRegistry) -> ((String) -> Array[Endpoint])

This registry as a Resolve interface value.

#
PersistentRegistry::restore

fn PersistentRegistry::restore(src : String) -> PersistentRegistry raise ConfigError

Rebuild a registry from a snapshot document, preserving instance keys, their endpoints and mod-revisions, the id counter, and the store revision — so a reloaded registry mints the next key exactly where the persisted one left off and a watcher's events_since(old_revision) still lines up.

#
PersistentRegistry::revision

fn PersistentRegistry::revision(self : PersistentRegistry) -> Int64

The store revision, bumped on each register/deregister.

#
PersistentRegistry::services

fn PersistentRegistry::services(self : PersistentRegistry) -> Array[String]

Every service name with at least one live instance.

#
PersistentRegistry::snapshot

fn PersistentRegistry::snapshot(self : PersistentRegistry) -> String

Serialize the whole keyspace as an etcd v3 RangeResponse-shaped JSON document: a header carrying the store revision and the id counter, and one key/value entry per instance carrying its endpoint and mod-revision. This is the exact payload a file- or etcd-backed deployment persists; restore rebuilds an identical registry from it, revisions intact.

#
PersistentRegistry::watch

fn PersistentRegistry::watch(self : PersistentRegistry, on_event : (RegistryEvent) -> Unit) -> Unit

Register a live watcher fired on every subsequent mutation, in revision order (← etcd's Watch with no start revision). To also see changes already applied, replay events_since first.

#
RegistryEvent

pub(all) enum RegistryEvent {
Put(key~ : String, endpoint~ : Endpoint, revision~ : Int64)
Delete(key~ : String, revision~ : Int64)
} derive(Eq,
Debug
)

A change to the registry keyspace, in etcd v3's watch shape: a Put carries the instance key and its endpoint, a Delete carries the key that went away, and both carry the store revision the change produced. A watcher receives these in revision order, so a client can rebuild the live set incrementally instead of re-resolving the whole service.

#
RegistryEvent::revision

fn RegistryEvent::revision(self : RegistryEvent) -> Int64

The store revision a registry event was produced at.

#
RequestLog

pub(all) struct RequestLog {
http_method : String
path : String
status : Int
duration_ms : Int64
request_id : String
client_ip : String
user_agent : String
}

A structured access-log record (← go-zero's logx HTTP access fields). Rather than a free-form line, each request is captured as typed fields and rendered as one JSON object per line — the format go-zero emits under logx and the shape log collectors (ELK, Loki) expect.

#
RequestLog::render

fn RequestLog::render(self : RequestLog) -> String

The record rendered as a single-line JSON string, ready to write to a log sink.

#
RequestLog::to_json

fn RequestLog::to_json(self : RequestLog) -> Json

The record as a JSON value with a stable field order, duration_ms rendered as a number of milliseconds.

#
RoundRobin

pub struct RoundRobin {
cursor : Int
}

A round-robin balancer (← go-zero's roundRobinBalancer) over a resolved endpoint set: successive picks cycle through the instances, spreading load evenly. Holds only a cursor, so it is cheap to keep per client.

#
RoundRobin::new

fn RoundRobin::new() -> RoundRobin

A round-robin balancer starting at the first instance.

#
RoundRobin::pick

fn RoundRobin::pick(self : RoundRobin, endpoints : Array[Endpoint]) -> Endpoint?

Pick the next endpoint in rotation, or None if the set is empty. The cursor advances modulo the set size, so it stays valid as instances come and go.

#
RpcChannel

pub struct RpcChannel {
engine :
H2Server

encoder :
HpackEncoder

decoder :
HpackDecoder

authority : String
next_stream_id : Int
}

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.

#
RpcChannel::call

fn RpcChannel::call(self : RpcChannel, path : String, request : Bytes) -> Result[Bytes,
Status
] raise

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.

#
RpcChannel::call_bidi_streaming

fn RpcChannel::call_bidi_streaming(self : RpcChannel, path : String, requests : Array[Bytes]) -> Result[Array[Bytes],
Status
] raise

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.

#
RpcChannel::call_client_streaming

fn RpcChannel::call_client_streaming(self : RpcChannel, path : String, requests : Array[Bytes]) -> Result[Bytes,
Status
] raise

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.

#
RpcChannel::call_server_streaming

fn RpcChannel::call_server_streaming(self : RpcChannel, path : String, request : Bytes) -> Result[Array[Bytes],
Status
] raise

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

#
RpcChannel::connect

fn RpcChannel::connect(server : RpcServer, authority? : String) -> RpcChannel raise

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.

#
RpcChannel::open_bidi

fn RpcChannel::open_bidi(self : RpcChannel, path : String) -> BidiCall raise

Open a bidirectional stream to path, sending the request HEADERS without half-closing so the stream stays open for interleaved sends. An unregistered path answers trailers-only UNIMPLEMENTED during this HEADERS feed, which the returned call captures as its status.

#
RpcCluster

pub struct RpcCluster {
servers : Map[String, RpcServer]
}

The in-process dial table: an endpoint address maps to the RpcServer listening there. It stands in for DNS resolution plus a socket dial in the in-process h2c transport — a real deployment opens a connection to the address instead of looking the server up here, but the resolve→balance→call path above it is the same.

#
RpcCluster::add

fn RpcCluster::add(self : RpcCluster, endpoint : Endpoint, server : RpcServer) -> Unit

Bind the server reachable at endpoint's address.

#
RpcCluster::dial

fn RpcCluster::dial(self : RpcCluster, endpoint : Endpoint) -> RpcChannel? raise

Open a channel to the server bound at endpoint, or None if nothing is reachable there (a stale registry entry pointing at a gone instance).

#
RpcCluster::new

fn RpcCluster::new() -> RpcCluster

An empty cluster.

#
RpcGroup

pub struct RpcGroup {
server : RpcServer
service : String
}

A per-service registration handle (← go-zero's service registrar closure): binds a set of methods to one package.Service on a shared RpcServer.

#
RpcGroup::register

fn RpcGroup::register(self : RpcGroup, name : String, handler : (Bytes) -> Bytes) -> Unit

Register a method name on this group's service, building the @moonrpc.Method descriptor and installing handler under its gRPC path. (register, not method — the latter is a reserved word.)

#
RpcGroup::register_bidi_streaming

fn RpcGroup::register_bidi_streaming(self : RpcGroup, name : String, factory : () -> BidiStreamHandler) -> Unit

Register a bidirectional-streaming method name on this group's service.

#
RpcGroup::register_client_streaming

fn RpcGroup::register_client_streaming(self : RpcGroup, name : String, handler : (Array[Bytes]) -> Bytes) -> Unit

Register a client-streaming method name on this group's service.

#
RpcGroup::register_server_streaming

fn RpcGroup::register_server_streaming(self : RpcGroup, name : String, handler : (Bytes) -> Array[Bytes]) -> Unit

Register a server-streaming method name on this group's service.

#
RpcGroup::service

fn RpcGroup::service(self : RpcGroup) -> String

The fully-qualified package.Service this group registers under.

#
RpcServer

pub struct RpcServer {
conf : RpcServerConf
handlers : Map[String, (Bytes) -> Bytes]
server_streaming : Map[String, (Bytes) -> Array[Bytes]]
client_streaming : Map[String, (Array[Bytes]) -> Bytes]
bidi_streaming : Map[String, () -> BidiStreamHandler]
}

A zRPC server (← go-zero's zrpc.Server): config plus a registry mapping each method's gRPC :path (/package.Service/Method) to its handler. Handlers are registered via @moonrpc.Method descriptors — directly or through a RpcGroup — and dispatched by path, mirroring how go-zero registers service implementations on the underlying gRPC server. Unary, server-streaming, and client-streaming methods live in separate registries so one path resolves to exactly one cardinality.

#
RpcServer::conf

fn RpcServer::conf(self : RpcServer) -> RpcServerConf

The server's configuration.

#
RpcServer::dispatch

fn RpcServer::dispatch(self : RpcServer, path : String, request : Bytes) -> Result[Bytes,
Status
]

Dispatch a unary call to the handler registered for path, returning the response bytes. An unregistered path yields Err(Unimplemented) — exactly the grpc-status a real gRPC server returns for an unknown method — so a transport can translate the result straight onto the wire.

#
RpcServer::dispatch_graceful

fn RpcServer::dispatch_graceful(self : RpcServer, coord : ShutdownCoordinator, path : String, request : Bytes) -> Result[Bytes,
Status
]

Dispatch a unary call through the shutdown gate: refuse with Unavailable when the server is shutting down, otherwise run the handler and count it as in-flight for the duration so a concurrent shutdown drains behind it. The gate wraps RpcServer::dispatch, so an unregistered path still yields Unimplemented.

#
RpcServer::group

fn RpcServer::group(self : RpcServer, service : String) -> RpcGroup

Open a RpcGroup that registers methods under the fully-qualified package.Service name — go-zero's per-service registration, without repeating the service name on each method.

#
RpcServer::has_method

fn RpcServer::has_method(self : RpcServer, path : String) -> Bool

Whether a handler is registered for path.

#
RpcServer::lookup

fn RpcServer::lookup(self : RpcServer, path : String) -> (Bytes) -> Bytes?

Look up the handler registered for a gRPC :path, or None if unregistered.

#
RpcServer::methods

fn RpcServer::methods(self : RpcServer) -> Array[String]

The gRPC paths of every registered method.

#
RpcServer::new

fn RpcServer::new(conf : RpcServerConf) -> RpcServer

Build an empty RPC server from its config.

#
RpcServer::register

fn RpcServer::register(self : RpcServer, desc :
Method
, handler : (Bytes) -> Bytes) -> Unit

Register handler for method, keyed by its gRPC path. A later registration for the same path replaces the earlier one.

#
RpcServer::register_bidi_streaming

fn RpcServer::register_bidi_streaming(self : RpcServer, desc :
Method
, factory : () -> BidiStreamHandler) -> Unit

Register a bidirectional-streaming handler for method, keyed by its gRPC path. factory runs once per call so each stream gets fresh handler state.

#
RpcServer::register_client_streaming

fn RpcServer::register_client_streaming(self : RpcServer, desc :
Method
, handler : (Array[Bytes]) -> Bytes) -> Unit

Register a client-streaming handler for method, keyed by its gRPC path.

#
RpcServer::register_server_streaming

fn RpcServer::register_server_streaming(self : RpcServer, desc :
Method
, handler : (Bytes) -> Array[Bytes]) -> Unit

Register a server-streaming handler for method, keyed by its gRPC path.

#
RpcServer::to_h2

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.

#
RpcServerConf

pub(all) struct RpcServerConf {
name : String
host : String
port : Int
timeout_ms : Int
} derive(Eq,
Debug
,
FromJson
)

zRPC server configuration (← go-zero's zrpc.RpcServerConf): the service name, the address it listens on, and a per-call timeout in milliseconds (0 disables it). The registry/etcd fields of go-zero's conf are modelled by the separate discovery layer; this is the transport-facing core.

#
RpcServerConf::from_json

fn RpcServerConf::from_json(src : String) -> RpcServerConf raise ConfigError

Load an RpcServerConf from a JSON config string, filling omitted fields from the new() defaults — the lenient loader matching go-zero's ,optional/,default= config tags.

#
RpcServerConf::from_yaml

fn RpcServerConf::from_yaml(src : String) -> RpcServerConf raise ConfigError

Load an RpcServerConf from a YAML config string (self-built yaml_parse), with the same default-filling semantics as from_json.

#
RpcServerConf::new

fn RpcServerConf::new(name? : String, host? : String, port? : Int, timeout_ms? : Int) -> RpcServerConf

Build an RPC server config with go-zero-style defaults (0.0.0.0:8080, 2s timeout).

#
Server

pub struct Server {
conf : ServiceConf
handler : async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit
}

A moonzero service: its config plus the assembled application (a moonapi App with any middleware already wrapped around it).

#
Server::describe

fn Server::describe(self : Server) -> String

A human-readable description of what this service binds to.

#
Server::new

Assemble a service from config and a moonapi application.

#
Server::to_asgi

fn Server::to_asgi(self : Server) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit)

The assembled AsgiApp, ready for a server (mooncat) to run.

#
Server::use_

fn Server::use_(self : Server, mw : (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit)) -> Server

Wrap the current application in another middleware layer (outermost last).

#
ServerMetrics

pub struct ServerMetrics {
requests : CounterVec
latency : Histogram
}

The request metrics an HTTP service exposes (← go-zero's server metrics): a request counter partitioned by method/route/status and a latency histogram. Held by the caller so it can be read out for a /metrics scrape after serving.

#
ServerMetrics::latency

fn ServerMetrics::latency(self : ServerMetrics) -> Histogram

The request-latency histogram, in milliseconds.

#
ServerMetrics::new

Fresh server metrics: an empty counter and a default-bucket latency histogram.

#
ServerMetrics::requests

fn ServerMetrics::requests(self : ServerMetrics) -> CounterVec

The request counter, labelled "<METHOD> <path> <status>".

#
ServiceConf

pub(all) struct ServiceConf {
name : String
host : String
port : Int
timeout_ms : Int
log_level : LogLevel
} derive(Eq,
FromJson
)

Service configuration (← go-zero's ServiceConf): the service name, its bind address, a request timeout, and the log level. timeout_ms is the per-request budget in milliseconds; 0 disables the deadline.

#
ServiceConf::from_json

fn ServiceConf::from_json(src : String) -> ServiceConf raise ConfigError

Load a ServiceConf from a JSON config string, applying go-zero-style defaults for every omitted field (an empty {} yields exactly ServiceConf::new()). This is the lenient loader mirroring go-zero's conf.Load with ,optional/,default= struct tags: unlike the strict derived FromJson — reachable via @json.from_json and requiring every field present — a partial config is filled from the same defaults new() uses.

Raises ConfigError on malformed JSON, a non-object root, or a field of the wrong type.

#
ServiceConf::from_yaml

fn ServiceConf::from_yaml(src : String) -> ServiceConf raise ConfigError

Load a ServiceConf from a YAML config string — the format go-zero actually ships (etc/*.yaml) — with the same lenient, default-filling semantics as from_json: an empty document yields exactly ServiceConf::new(), and each omitted field falls back to its new() default. The YAML is parsed by the self-built yaml_parse (block mappings, nesting, sequences, scalars, comments) into a Json object, then decoded by the shared field reader — so JSON and YAML configs agree field-for-field.

Raises ConfigError on malformed YAML, a non-mapping root, or a field of the wrong type.

#
ServiceConf::new

fn ServiceConf::new(name? : String, host? : String, port? : Int, timeout_ms? : Int, log_level? : LogLevel) -> ServiceConf

Build a config with sensible defaults (0.0.0.0:8888, 3s timeout, info).

#
ShutdownCoordinator

pub struct ShutdownCoordinator {
shutting_down : Bool
in_flight : Int
}

A graceful-shutdown coordinator for a zRPC server (← go-zero's proc.AddShutdownListener + gRPC's GracefulStop): once shutdown is initiated the server stops admitting new calls, but calls already in flight are allowed to run to completion. A call brackets its work between begin_call and end_call; begin_call returns false when the server is shutting down, which the transport surfaces as Unavailable — exactly the status a client sees once a server has stopped listening. The server is fully drained once shutdown has been initiated and no calls remain in flight.

The counter is plain mutable state, which is safe under moonbitlang/async's cooperative single-threaded scheduling: begin_call/end_call never yield, so the count is only observed at await points between them.

#
ShutdownCoordinator::begin_call

fn ShutdownCoordinator::begin_call(self : ShutdownCoordinator) -> Bool

Admit a new call: register it as in-flight and return true, unless shutdown has been initiated, in which case the call is refused (false) and the count is left untouched.

#
ShutdownCoordinator::end_call

fn ShutdownCoordinator::end_call(self : ShutdownCoordinator) -> Unit

Mark an admitted call finished, dropping it from the in-flight count. Only call it for a call that begin_call admitted; the count never goes below zero.

#
ShutdownCoordinator::in_flight

fn ShutdownCoordinator::in_flight(self : ShutdownCoordinator) -> Int

The number of calls currently in flight.

#
ShutdownCoordinator::initiate_shutdown

fn ShutdownCoordinator::initiate_shutdown(self : ShutdownCoordinator) -> Unit

Begin the graceful shutdown: from now on begin_call refuses new calls while in-flight ones keep running. Idempotent.

#
ShutdownCoordinator::is_drained

fn ShutdownCoordinator::is_drained(self : ShutdownCoordinator) -> Bool

Whether the server is fully drained: shutdown initiated and no call in flight. A supervisor loops on this (yielding between checks) to know the last in-flight RPC has finished and the process may exit.

#
ShutdownCoordinator::is_shutting_down

fn ShutdownCoordinator::is_shutting_down(self : ShutdownCoordinator) -> Bool

Whether shutdown has been initiated.

#
ShutdownCoordinator::new

A coordinator that is serving normally with no calls in flight.

#
TokenBucket

pub struct TokenBucket {
capacity : Double
refill_per_ms : Double
tokens : Double
last_ms : Int64
}

A token-bucket rate limiter (← go-zero's limit.TokenLimiter, modelled as a pure in-process counter over an explicit clock instead of Redis+Lua). The bucket holds up to capacity tokens and refills continuously at refill_per_ms tokens per millisecond; each admitted request spends one token. Because every decision is a function of (state, now), the limiter is exactly testable without a real clock.

#
TokenBucket::allow

fn TokenBucket::allow(self : TokenBucket, now : Int64) -> Bool

Try to admit one request at time now: refill, then spend a token if one is available. Returns true when admitted, false when the bucket is empty.

#
TokenBucket::allow_n

fn TokenBucket::allow_n(self : TokenBucket, n : Double, now : Int64) -> Bool

Try to admit a request costing n tokens at time now. Returns false (spending nothing) when fewer than n tokens are available.

#
TokenBucket::available

fn TokenBucket::available(self : TokenBucket, now : Int64) -> Double

The (fractional) number of tokens currently available, after refilling to now. Useful for metrics and tests.

#
TokenBucket::new

fn TokenBucket::new(rate : Double, burst? : Double, now? : Int64) -> TokenBucket

Build a bucket admitting rate requests per second on average with room for a burst of that many back-to-back (default burst = rate). It starts full at time now. A non-positive rate/burst is clamped to a minimum so the bucket always has a defined capacity.

#
TraceContext

pub(all) struct TraceContext {
trace_id : String
span_id : String
flags : Int
}

A W3C Trace Context (← go-zero's OpenTelemetry propagation): the 128-bit trace id shared across a request's whole call tree, the 64-bit span id of the current hop, and the 8-bit sampling flags.

#
TraceContext::span_id

fn TraceContext::span_id(self : TraceContext) -> String

The span id of this hop.

#
TraceContext::to_traceparent

fn TraceContext::to_traceparent(self : TraceContext) -> String

Format as a W3C traceparent header value: 00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>.

#
TraceContext::trace_id

fn TraceContext::trace_id(self : TraceContext) -> String

The trace id (the value propagated unchanged down the call tree).

#
auth

fn auth(secret : String, clock : Clock) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

JWT auth middleware (← go-zero's handler.Authorize): require every HTTP request to carry a valid Authorization: Bearer <jwt> header. The token is verified against secret under HS256 at the current time read from clock (milliseconds, converted to the JWT seconds epoch); an absent, malformed, tampered, expired, or not-yet-valid token is rejected with 401 Unauthorized before the wrapped app runs. Non-HTTP scopes (lifespan, websocket) pass through untouched.

#
base64url_decode

fn base64url_decode(s : String) -> Bytes

Decode a base64url string (padding optional) back to bytes, remapping -/_ to +// before decoding. Lenient about missing padding, the way JWT segments are written.

#
base64url_encode

fn base64url_encode(data : Bytes) -> String

base64url encoding (RFC 4648 §5, no padding): standard base64 with +// remapped to -/_ and trailing = dropped — the alphabet JWT uses for its header, payload, and signature segments.

#
breaker

fn breaker(b : Breaker, clock : Clock) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

Circuit-breaker middleware (← go-zero's breaker interceptor): gate each HTTP request through a shared Breaker. When the breaker admits the request, the outbound HttpResponseStart status is observed — a 5xx (or a raised failure) is recorded as a failure, anything else as a success, driving the state machine. When the breaker is open, the request is failed fast with 503 Service Unavailable without touching the wrapped app. Non-HTTP scopes pass through untouched.

#
constant_time_eq

fn constant_time_eq(a : Bytes, b : Bytes) -> Bool

A constant-time byte-string equality: it inspects every byte of both inputs regardless of where they first differ, so an attacker cannot recover a valid signature byte-by-byte from response timing. Unequal lengths return false immediately (length is not secret). Used to compare JWT signatures.

#
cors

fn cors(conf : CorsConf) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

CORS middleware (← go-zero's cors.Middleware): wrap the outbound Send so the configured Access-Control-* headers are injected onto every HttpResponseStart, leaving the body and other events untouched.

#
default_latency_buckets

let default_latency_buckets : Array[Double]

The default latency buckets go-zero ships (milliseconds): a request spends most of its time under a second, so the bounds cluster there.

#
generate_span_id

fn generate_span_id(seed : Int64) -> String

A 16-hex-char (64-bit) span id from seed.

#
generate_trace_id

fn generate_trace_id(seed : Int64) -> String

A 32-hex-char (128-bit) trace id from seed, mixing two independent words.

#
hmac_sha256

fn hmac_sha256(key : Bytes, msg : Bytes) -> Bytes

HMAC-SHA256 (RFC 2104): a keyed message-authentication code over sha256. A key longer than the 64-byte block is hashed first; a shorter key is zero-padded. The message is authenticated as H((K ⊕ opad) ∥ H((K ⊕ ipad) ∥ msg)). Verified against RFC 4231 test case 2. This is the signature function behind JWT HS256.

#
jwt_authorized

fn jwt_authorized(token : String?, secret : String, now_secs : Int64) -> Bool

Whether a request bearing token is authorised at now_secs: the token verifies against secret under HS256 and is neither expired nor not-yet-valid. A missing token is unauthorised. Exposed as a pure decision so the middleware's accept/reject is testable without driving the transport.

#
jwt_sign

fn jwt_sign(claims : Map[String, Json], secret : String) -> String

Sign a claims set as a compact JWT using HS256 (← go-zero's jwt.NewWithClaims(SigningMethodHS256, ...)). The header is fixed to {"alg":"HS256","typ":"JWT"}; claims is serialised as the JSON payload (include exp/iat/nbf/sub/… as ordinary entries); secret is the shared HS256 key. Returns header.payload.signature, each segment base64url-encoded.

#
jwt_verify

fn jwt_verify(token : String, secret : String, now_secs : Int64) -> Map[String, Json] raise JwtError

Verify a compact HS256 JWT and return its claims (← go-zero's handler.Authorize). Checks, in order: three segments; header alg is HS256; the HMAC-SHA256 signature matches (compared in constant time); exp (if present) is strictly after now_secs; nbf (if present) is at or before now_secs. now_secs is the verification time as a Unix timestamp in seconds (JWT NumericDate). Raises the matching JwtError on any failure; a tampered payload or signature fails at BadSignature.

#
logging

fn logging(inner : async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit)

A request-logging middleware: prints METHOD path for each HTTP request, then delegates to the wrapped application.

#
maxbytes

fn maxbytes(limit : Int) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

Max-bytes middleware (← go-zero's MaxBytesHandler): reject any HTTP request whose declared Content-Length exceeds limit bytes with 413 Payload TooLarge, before the wrapped app runs. A limit <= 0 disables the check. Non- HTTP scopes pass through untouched.

#
metrics

fn metrics(m : ServerMetrics, clock : Clock) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

Metrics middleware (← go-zero's prometheus interceptor): time each HTTP request on the shared clock and, when the response starts, count it under "<METHOD> <path> <status>" and record its latency in milliseconds. The record is taken once per request even if a downstream (under a recovery race) emits a second start. Non-HTTP scopes pass through unmeasured.

#
next_trace_context

fn next_trace_context(inbound : String?, seed : Int64) -> TraceContext

Derive the outgoing trace context for a request: reuse the inbound traceparent's trace id if the client sent a valid one (continuing the distributed trace), else start a new trace, and always mint a fresh child span id from seed. This is the propagation decision, pulled out as a pure function so it is testable without the transport.

#
parse_traceparent

fn parse_traceparent(value : String) -> TraceContext?

Parse a W3C traceparent value, or None if it is malformed. Only the four canonical fields with correct lengths are accepted; the flags default to 0 if unparseable.

#
pick_first

fn pick_first(endpoints : Array[Endpoint]) -> Endpoint?

Pick the first endpoint (← gRPC's pick_first), or None if the set is empty. A stable choice that only moves when the head instance goes away.

#
rate_limit

fn rate_limit(bucket : TokenBucket, clock : Clock) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

Rate-limit middleware (← go-zero's TokenLimitMiddleware): admit each HTTP request against a shared TokenBucket read at clock.now(), answering 429 Too Many Requests when the bucket is empty and otherwise delegating to the wrapped app. The bucket is captured once per assembly, so its state is shared across every request this layer serves. Non-HTTP scopes (lifespan, websocket) pass through untouched.

#
recovery

fn recovery(inner : async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit)

Recovery middleware (← go-zero's RecoverHandler): run the wrapped application inside a try, and if it raises, emit a 500 Internal ServerError instead of letting the failure escape to the server. A downstream that has already streamed its response start before raising will produce a second start event; recovery is a last-resort guard, so it always answers rather than trying to detect that race.

#
request_id

fn request_id(header? : String) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

Request-ID middleware (← go-zero's trace/x-request-id handling): reuse an inbound x-request-id if the client sent one, otherwise mint a fresh monotonic id, and stamp it onto every response's HttpResponseStart. The counter is captured once per assembly, so ids stay unique across the requests this layer serves.

#
resolve_one

fn resolve_one(registry : InMemoryRegistry, service : String, balancer : RoundRobin) -> Endpoint?

Resolve service on the registry and pick one endpoint with balancer — the resolve-then-balance step a zRPC client runs before each call. An etcd- or consul-backed registry with the same resolve shape drops in unchanged.

#
sha256

fn sha256(msg : Bytes) -> Bytes

SHA-256 (FIPS 180-4): hash an arbitrary byte string to a 32-byte digest. A self-built primitive — MoonBit's core ships no crypto — implementing the full message schedule and 64-round compression over 512-bit blocks with the standard length-padding. Verified against the NIST vectors ("", "abc"). The building block for hmac_sha256, and through it for JWT HS256 signing.

#
status_of_code

fn status_of_code(code : Int) ->
Status

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.

#
structured_logging

fn structured_logging(clock : Clock) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

Structured-logging middleware (← go-zero's LogHandler): time each HTTP request on the shared clock, capture its method/path/client-ip/user-agent and the response status observed on HttpResponseStart, and print one RequestLog JSON line when the response starts. Non-HTTP scopes pass through without logging.

#
timeout

fn timeout(budget_ms : Int64, clock : Clock) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

Timeout middleware (← go-zero's TimeoutHandler): establish a per-request Deadline of budget_ms at clock.now() for the wrapped app.

Async boundary (faithful model). Preemptively aborting an in-flight handler the instant its deadline fires requires racing the handler against a timer and cancelling the loser — in MoonBit that is @async.any([handler,timer]) with structured cancellation, which only runs under the native async runtime and cannot be driven synchronously. What this middleware does portably: it installs the deadline and enforces it on the response path — if the handler blows its budget before emitting its first event, the client receives a 503 timeout (from timeout_events) and the late response is suppressed. The remaining gap (a handler that hangs and never emits) is closed by the race/cancel wired at the async server edge. A budget_ms <= 0 disables the timeout, passing straight through.

#
tracing

fn tracing(header? : String) -> ((async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit) -> (async (
Scope
, async () ->
Event
, async (
Event
) -> Unit) -> Unit))

Trace-id propagation middleware (← go-zero's trace handler): continue the inbound traceparent trace or start a new one, mint a child span, and stamp both traceparent and a convenience x-trace-id onto the response so the id flows to the client and downstream calls. The per-assembly seed counter keeps span ids distinct across the requests this layer serves. Non-HTTP scopes pass through untraced.

#
yaml_parse

fn yaml_parse(src : String) -> Json raise ConfigError

Parse a minimal YAML subset into a Json value: block mappings (key: value), arbitrary indentation-based nesting, block sequences (-item, including - key: value maps in a list), scalars (quoted/plain strings, integers, floats, true/false, ~/null), and # line comments. Enough of YAML 1.1 to load go-zero-style service config. Flow style ({a: 1}, [1, 2]), anchors/aliases, multi-document streams, and block scalars (|/>) are not supported — use JSON for those. Raises ConfigError on a line that is neither a mapping entry nor a sequence item.