Skip to main content

Module rpc

Module rpc 

Source
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 an RpcErrorBody on error with HTTP status mapped via [CratestackError::status_code].
  • POST /rpc/batch — sequence of RpcRequest frames in, sequence of RpcResponseFrame frames 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). One event: message per ModelEvent<T>, terminated by one event: error on 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§

FeedEvent
A model event as it comes off the backplane: the event a subscriber may be sent, and, for an update on an @@subscribe model, the row before it. The pre-image stays server-side: it is only ever the data of a filter-exit event, after the subscriber’s own read check (route_event).
RpcErrorBody
Wire shape of a single error returned by an RPC call. Maps from CratestackError via rpc_code + CratestackError::public_message.
RpcGetInput
RPC input for model.<X>.get. Deliberately its own struct rather than reusing RpcPkInput for both get and delete: delete also decodes RpcPkInput, 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 REST GET /<plural>/{id} — fields, include, and includeFields (line-for-line parity; on transport rpc the RPC dispatcher synthesizes a URL query string from these fields and runs the macro-generated parse_model_fetch_query parser to enforce selection validation and projection in one path).
RpcListInput
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.
RpcListPredicate
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).
RpcPkInput
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.
RpcResponseFrame
Wire shape of a single batch response frame.
RpcUpdateInput
RPC input for model.<X>.update. Parameterized on both the PK type and the model’s concrete Update<X>Input so the patch decodes straight to its real type — round-tripping through serde_json::Value would corrupt CBOR Option::None values (which minicbor-serde encodes as 0xf6 simple-null but serde_json::Value encodes as the CBOR empty-array marker; see comments in cratestack-codec-cbor). The dispatcher re-encodes patch through the same codec before handing it to the existing update handler.
SubscribeQuery
The operations a feed wants (None: all the model emits) and the rest of its query, for the list parser.
SubscriptionPush
Handed to one or more CratestackEventBus::subscribe callbacks 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 RpcRequest frames.
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 incremental application/cbor-seq sequence response (a @stream procedure, see cratestack#282/#283) fails partway through, the last item of the sequence is Tag(RPC_STREAM_ERROR_TAG, RpcErrorBody-as-CBOR-map) — CBOR major type 6, this tag number, wrapping RpcErrorBody encoded as a plain CBOR map — in place of what would otherwise be the next unwrapped out item. No further items follow it; end of body comes immediately after.
RPC_SUBSCRIBE_PATH
Mount path for @@subscribe SSE 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. Unlike RPC_UNARY_PATH this is GET-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 to RpcErrorBody with the gRPC-style code, and re-encoded with the same HTTP status.
cratestack_error_code_to_rpc_code
Map a CratestackErrorResponse.code string (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’s Content-Type header. Missing header → CBOR (the default for the REST binding too).
encode_model_event_sse_response
Encode items as a text/event-stream response. items ending (None from the underlying Stream) always means backpressure overflow closed the channel (see module docs) — the last byte chunk written is always the Error{unavailable} sentinel event.
encode_rpc_error
Build an axum::Response carrying an RpcErrorBody for a CratestackError raised inside the dispatcher (e.g. body decode failure, unknown op id). The HTTP status comes from CratestackError::status_code; the body is codec-encoded via the request’s codec, content-type negotiated against RPC_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 update dispatch arm to re-encode the typed patch before handing it to the existing update handler as Bytes.
encode_subscribe_error
The subscribe endpoint’s error response.
encode_subscription_sse_response
Encode a subscription’s items as a text/event-stream response.
guarded_receiver_stream
Wraps a raw mpsc::Receiver into a Stream, keeping guard alive for exactly as long as the stream is — dropped together whether the stream ends normally (overflow, see SubscriptionPush) or is cancelled mid-poll (an ordinary client disconnect just drops this whole future). This is what lets a cratestack_core::SubscriptionGuard passed as guard unsubscribe cleanly in either case without the caller needing to distinguish which one happened.
model_events
The model events published on a backplane channel, decoded. A payload that does not decode (a replica on another schema version mid-deploy) ends the feed: the client refetches rather than miss an event.
model_feed
items routed by route and encoded to JSON values, ending with the error that stopped it (module docs). route returns what to send for an item, or nothing to drop it.
procedure_feed
A handler’s items encoded to JSON values, ending after the first error.
procedure_input_query
A subscription procedure’s JSON input as the query string open_query takes: null or {} is none.
procedure_query_args
The procedure’s Args from the query string: each parameter is one argument, its value that argument’s JSON.
project_event_fields
items with each event’s record narrowed to fields (None: all).
response_to_frame
Convert an [axum::Response] returned by an inner dispatch arm into a single batch response frame.
route_event
What one subscriber is sent for feed, if anything: the event itself when its row passes the subscriber’s filter and read policy (matches, readable, both on a row); otherwise, for an update, a filter-exit event (matches: false, §5.3) if the row before the write had passed both, so the subscriber’s copy is not left stale.
rpc_code
Map a CratestackError to its stable RPC code (gRPC-style snake_case).
split_subscribe_query
subscribe_input_query
A request frame’s input as the REST query: null or {} is none.
subscription_channel
Builds a fresh bounded channel plus the SubscriptionPush handle callers clone into every CratestackEventBus::subscribe closure that should feed it.
synthesize_get_query
Synthesize a URL query string from an RpcGetInput in exactly the shape the macro-generated parse_model_fetch_query parses — the direct get counterpart to synthesize_list_query, so RPC get and REST GET /<plural>/{id} run one and the same validation and projection path. Returns None when no field is set.
synthesize_list_query
Synthesize a URL query string from an RpcListInput in exactly the shape the macro-generated parse_model_list_query parses. Returns None when the input has no fields set (the existing handler treats a missing query the same as an empty one — no point allocating).
until_credential_expires
items, ended with unauthenticated at expires_at (seconds since the epoch, the exp claim). Already expired: the first item is that error.
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 plain Accept header, same as every other HTTP RPC binding. Reject anything that doesn’t ask for SSE up front, before any CratestackEventBus subscription gets registered.