Skip to main content

cratestack_client_rust/
subscription.rs

1//! Subscriptions (`docs/design/subscriptions-websocket.md` §8): one
2//! contract whatever the binding — the call resolves once the server says
3//! `Ready`, then the stream yields items and ends, or ends with an error.
4//!
5//! The binding is client configuration ([`SubscriptionTransport`]), never
6//! part of a generated method's name. `Auto` tries WebTransport when the
7//! `webtransport` feature is on and the config names the listener, then a
8//! WebSocket when the `ws` feature is on, then SSE (native targets for the
9//! first two). It moves on only when a binding cannot be opened; a refusal
10//! is never a reason to fall back.
11
12mod failure;
13mod frames;
14mod open;
15mod sse;
16#[cfg(all(feature = "ws", not(target_arch = "wasm32")))]
17mod ws;
18#[cfg(all(feature = "webtransport", not(target_arch = "wasm32")))]
19mod wt;
20
21pub use frames::rpc_error_to_client;
22
23/// A live subscription: items until the server ends it, or a terminal `Err`.
24/// `None` after an `Err`, or when the stream ends normally (a subscription
25/// procedure whose source ran out). Dropping it cancels the subscription.
26pub type ClientSubscription<T> = tokio::sync::mpsc::Receiver<Result<T, ClientError>>;
27
28use crate::error::ClientError;
29
30/// Which binding a subscription uses.
31#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
32pub enum SubscriptionTransport {
33    /// WebTransport, then a WebSocket, then SSE: each binding this build
34    /// has and the config allows, moving on only when one cannot be opened.
35    #[default]
36    Auto,
37    /// One WebTransport session per subscription (`webtransport` feature,
38    /// native targets, `ClientConfig::with_webtransport`).
39    WebTransport,
40    /// One WebSocket per subscription (`ws` feature, native targets).
41    WebSocket,
42    /// `text/event-stream` over HTTP.
43    Sse,
44}
45
46/// Where a subscription is served. The generated client fills it in; the
47/// binding decides which half it reads.
48#[derive(Debug, Clone)]
49pub struct SubscriptionTarget {
50    /// `model.<Model>.subscribe` or `procedure.<name>`.
51    pub op_id: String,
52    /// The input a socket's `Request` carries: a model feed's
53    /// `RpcSubscribeInput`, a subscription procedure's `Args`.
54    pub input: serde_json::Value,
55    /// The SSE route: `/rpc/subscribe/<op_id>` on `transport rpc`,
56    /// `/<plural>/$subscribe` or `/$procs/<name>` on REST.
57    pub sse_path: String,
58    /// The input as that route's query string.
59    pub sse_query: Option<String>,
60    /// The socket path: `/rpc/ws`, or `/$ws` on REST.
61    pub socket_path: &'static str,
62}
63
64impl SubscriptionTarget {
65    /// A subscription procedure's `Args` as its SSE query string: each
66    /// argument a parameter whose value is that argument's JSON.
67    pub fn procedure_query(args: &serde_json::Value) -> Option<String> {
68        let object = args.as_object().filter(|object| !object.is_empty())?;
69        let mut query = url::form_urlencoded::Serializer::new(String::new());
70        for (name, value) in object {
71            query.append_pair(name, &value.to_string());
72        }
73        Some(query.finish())
74    }
75}