Expand description
Runtime primitives for the transport rpc generation style.
See docs/design/rpc-transport.md for the full design. This module
provides the binding-side surface that schemas with transport rpc
generate against:
POST /rpc/{op_id}— unary calls. Body is the codec-encoded input (no frame wrapper); response body is the codec-encoded output on success, or anRpcErrorBodyon error with HTTP status mapped via [CratestackError::status_code].POST /rpc/batch— sequence ofRpcRequestframes in, sequence ofRpcResponseFrameframes out in the same order. Per-frame errors don’t poison the batch.GET /rpc/subscribe/{op_id}— SSE subscription dispatch for@@subscribed models (design doc §3.4a, cratestack#390). Oneevent: messageperModelEvent<T>, terminated by oneevent: erroron backpressure overflow. See [sse] and [subscription_bridge].
The full WebSocket frame loop (§3.4) remains speced but unbuilt,
gated on a real bidirectional/high-multiplexing need per issue
#183’s spike decision — see docs/design/rpc-transport.md §6.5.
The macro emits the dispatch table and the rpc_router constructor.
This crate provides the shared frame shapes, error mapping, and the
RPC_*_PATH constants both sides agree on.
Structs§
- RpcError
Body - Wire shape of a single error returned by an RPC call. Maps from
CratestackErrorviarpc_code+CratestackError::public_message. - RpcGet
Input - RPC input for
model.<X>.get. Deliberately its own struct rather than reusingRpcPkInputfor both get and delete:deletealso decodesRpcPkInput, and adding selection fields there would be a silently- ignored surface on a verb that has no response body to carry projections into. Carries the same selection surface as RESTGET /<plural>/{id}—fields,include, andincludeFields(line-for-line parity; ontransport rpcthe RPC dispatcher synthesizes a URL query string from these fields and runs the macro-generatedparse_model_fetch_queryparser to enforce selection validation and projection in one path). - RpcList
Input - RPC input for
model.<X>.list. Mirrors the REST URL query 1:1 — every optional field maps to a query param of the same name, predicates carry arbitrary(key, value)pairs that aren’t reserved keywords. - RpcList
Predicate - Single arbitrary key/value predicate inside
RpcListInput::filters. Models the REST URL form’s “anything that isn’t a reserved keyword is a predicate” rule (e.g.?published=true&authorId=42). - RpcPk
Input - RPC input for
model.<X>.delete. The PK type is instantiated per-model at the macro emission site. - RpcRequest
- Wire shape of a single batch request frame.
- RpcResponse
Frame - Wire shape of a single batch response frame.
- RpcUpdate
Input - RPC input for
model.<X>.update. Parameterized on both the PK type and the model’s concreteUpdate<X>Inputso the patch decodes straight to its real type — round-tripping throughserde_json::Valuewould corrupt CBOROption::Nonevalues (whichminicbor-serdeencodes as0xf6simple-null butserde_json::Valueencodes as the CBOR empty-array marker; see comments incratestack-codec-cbor). The dispatcher re-encodespatchthrough the same codec before handing it to the existing update handler. - Subscription
Push - Handed to one or more
CratestackEventBus::subscribecallbacks registered for the same logical subscription (e.g. one per@@emitted operation on a model). Every clone shares the same underlying sender slot, so the first overflow observed by any of them permanently closes the channel — subsequent pushes from any clone become silent no-ops.
Constants§
- RPC_
BATCH_ PATH - Mount path for batched RPC calls. Body is a codec-encoded sequence
of
RpcRequestframes. - RPC_
BINDING_ CAPABILITIES - Codec/transport capabilities for every RPC binding route. Both unary and batch accept and emit CBOR or JSON, default CBOR; sequence responses (streaming) are not yet supported by this binding.
- RPC_
STREAM_ ERROR_ TAG - CBOR tag number reserved for the mid-stream error sentinel described
in
docs/design/rpc-transport.md§3.3: when a genuinely incrementalapplication/cbor-seqsequence response (a@streamprocedure, see cratestack#282/#283) fails partway through, the last item of the sequence isTag(RPC_STREAM_ERROR_TAG, RpcErrorBody-as-CBOR-map)— CBOR major type 6, this tag number, wrappingRpcErrorBodyencoded as a plain CBOR map — in place of what would otherwise be the next unwrappedoutitem. No further items follow it; end of body comes immediately after. - RPC_
SUBSCRIBE_ PATH - Mount path for
@@subscribeSSE subscriptions (docs/design/rpc-transport.md§3.4a, cratestack#390). The trailing segment is the percent-decoded op id, e.g.GET /rpc/subscribe/model.User.subscribe. UnlikeRPC_UNARY_PATHthis isGET-only and carries no request body — auth is header-based (same as every other HTTP RPC binding), not an upgrade-time HMAC like the WS path (§3.4). - RPC_
UNARY_ PATH - Mount path for unary RPC calls. The trailing segment is the
percent-decoded op id, e.g.
POST /rpc/model.User.list.
Functions§
- convert_
handler_ error_ response - Post-process a handler-emitted response. Success responses pass
through unchanged. Non-2xx responses are buffered, their bodies
decoded as
cratestack_core::CratestackErrorResponse(the REST shape the existing axum handlers emit), translated toRpcErrorBodywith the gRPC-style code, and re-encoded with the same HTTP status. - cratestack_
error_ code_ to_ rpc_ code - Map a
CratestackErrorResponse.codestring (screaming-snake, REST- binding vocabulary) to the stable gRPC-style code the RPC binding emits. - decode_
rpc_ body - Decode an RPC unary request body into
T, picking the codec based on the request’sContent-Typeheader. Missing header → CBOR (the default for the REST binding too). - encode_
model_ event_ sse_ response - Encode
itemsas atext/event-streamresponse.itemsending (Nonefrom the underlyingStream) always means backpressure overflow closed the channel (see module docs) — the last byte chunk written is always theError{unavailable}sentinel event. - encode_
rpc_ error - Build an
axum::Responsecarrying anRpcErrorBodyfor aCratestackErrorraised inside the dispatcher (e.g. body decode failure, unknown op id). The HTTP status comes fromCratestackError::status_code; the body is codec-encoded via the request’s codec, content-type negotiated againstRPC_BINDING_CAPABILITIES. - encode_
rpc_ value - Encode an arbitrary serializable value back to bytes using the same
codec as the request. Used by the macro-generated
updatedispatch arm to re-encode the typed patch before handing it to the existing update handler asBytes. - guarded_
receiver_ stream - Wraps a raw
mpsc::Receiverinto aStream, keepingguardalive for exactly as long as the stream is — dropped together whether the stream ends normally (overflow, seeSubscriptionPush) or is cancelled mid-poll (an ordinary client disconnect just drops this whole future). This is what lets acratestack_core::SubscriptionGuardpassed asguardunsubscribe cleanly in either case without the caller needing to distinguish which one happened. - response_
to_ frame - Convert an [
axum::Response] returned by an inner dispatch arm into a single batch response frame. - rpc_
code - Map a
CratestackErrorto its stable RPC code (gRPC-style snake_case). - subscription_
channel - Builds a fresh bounded channel plus the
SubscriptionPushhandle callers clone into everyCratestackEventBus::subscribeclosure that should feed it. - synthesize_
get_ query - Synthesize a URL query string from an
RpcGetInputin exactly the shape the macro-generatedparse_model_fetch_queryparses — the directgetcounterpart tosynthesize_list_query, so RPCgetand RESTGET /<plural>/{id}run one and the same validation and projection path. ReturnsNonewhen no field is set. - synthesize_
list_ query - Synthesize a URL query string from an
RpcListInputin exactly the shape the macro-generatedparse_model_list_queryparses. ReturnsNonewhen the input has no fields set (the existing handler treats a missing query the same as an empty one — no point allocating). - validate_
subscribe_ accept_ header GET /rpc/subscribe/{op_id}has no upgrade handshake to negotiate a binary subprotocol like WS does — the client states its intent via a plainAcceptheader, same as every other HTTP RPC binding. Reject anything that doesn’t ask for SSE up front, before anyCratestackEventBussubscription gets registered.