Skip to main content

cratestack_client_rust/runtime/
handle.rs

1use std::sync::Arc;
2
3use cratestack_codec_cbor::CborCodec;
4#[cfg(feature = "codec-json")]
5use cratestack_codec_json::JsonCodec;
6use reqwest::Url;
7
8use crate::client::CratestackClient;
9use crate::config::ClientConfig;
10use crate::error::ClientError;
11use crate::runtime::sealing::{Sealing, bad_input};
12use crate::runtime::transport::RuntimeTransportClient;
13use crate::runtime::wire::{
14    RuntimeCodecConfig, RuntimeConfigWire, RuntimeErrorCode, RuntimeErrorWire, RuntimeRequestWire,
15    RuntimeResponseWire, RuntimeStateStoreConfig,
16};
17use crate::state::{
18    ClientStateStore, InMemoryStateStore, JsonFileStateStore, PersistedClientState,
19};
20use crate::streaming_callback::RuntimeChunkWire;
21
22pub struct RuntimeHandle {
23    runtime: tokio::runtime::Runtime,
24    pub(crate) client: RuntimeTransportClient,
25}
26
27impl RuntimeHandle {
28    /// A handle with no envelope. A config that names one
29    /// (`CoseSign1`, `CoseMac0`) is refused: the keys cannot travel in the
30    /// config, so build the handle with `RuntimeHandle::with_envelope` (feature `cose`).
31    pub fn new(config: RuntimeConfigWire) -> Result<Self, RuntimeErrorWire> {
32        Self::build(config, Sealing::None)
33    }
34
35    /// A handle that seals every request and opens every response
36    /// (cratestack#1007). `config.transport.envelope` must name `envelope`'s
37    /// mode (`CoseSign1` for Sign1, `CoseMac0` for Mac0) and its codec must
38    /// be CBOR. `contracts` is the generated client schema's
39    /// `OP_CONTRACTS`: the raw bridge has no generated `Client` to stamp
40    /// them, and every binding needs its op's digest.
41    ///
42    /// Raw requests must be RPC ones (`/rpc/{op_id}`, `/rpc/batch`); a raw
43    /// REST path has no route template to bind. Streams are refused
44    /// (`envelope_streams_unsupported`).
45    #[cfg(feature = "cose")]
46    pub fn with_envelope(
47        config: RuntimeConfigWire,
48        envelope: crate::envelope::ClientEnvelope,
49        contracts: cratestack_core::OpContracts,
50    ) -> Result<Self, RuntimeErrorWire> {
51        Self::build(config, Sealing::Cose(envelope, contracts))
52    }
53
54    fn build(config: RuntimeConfigWire, sealing: Sealing) -> Result<Self, RuntimeErrorWire> {
55        let base_url = Url::parse(&config.base_url).map_err(|error| {
56            bad_input(format!("invalid base URL '{}': {error}", config.base_url))
57        })?;
58        let state_store: Arc<dyn ClientStateStore> = match config.state_store {
59            RuntimeStateStoreConfig::InMemory => Arc::new(InMemoryStateStore::default()),
60            RuntimeStateStoreConfig::JsonFile { path } => Arc::new(JsonFileStateStore::new(path)),
61        };
62        sealing.agrees_with(&config.transport.envelope)?;
63        let client = match config.transport.codec {
64            RuntimeCodecConfig::Cbor => RuntimeTransportClient::Cbor(
65                sealing.apply(
66                    CratestackClient::new(ClientConfig::new(base_url.clone()), CborCodec)
67                        .with_state_store(state_store.clone()),
68                )?,
69            ),
70            #[cfg(feature = "codec-json")]
71            RuntimeCodecConfig::Json => RuntimeTransportClient::Json(
72                sealing.apply(
73                    CratestackClient::new(ClientConfig::new(base_url), JsonCodec)
74                        .with_state_store(state_store),
75                )?,
76            ),
77            #[cfg(not(feature = "codec-json"))]
78            RuntimeCodecConfig::Json => {
79                let _ = state_store;
80                return Err(bad_input(
81                    "JSON codec is not compiled in (cratestack-client-rust built with \
82                     `--no-default-features` or without the `codec-json` feature)"
83                        .to_owned(),
84                ));
85            }
86        };
87        let runtime = tokio::runtime::Builder::new_current_thread()
88            .enable_all()
89            .build()
90            .map_err(|error| RuntimeErrorWire {
91                code: RuntimeErrorCode::State,
92                http_status: None,
93                message: format!("failed to build runtime: {error}"),
94                remote_code: None,
95                remote_body: None,
96            })?;
97
98        Ok(Self { runtime, client })
99    }
100
101    pub fn execute(
102        &self,
103        request: RuntimeRequestWire,
104    ) -> Result<RuntimeResponseWire, RuntimeErrorWire> {
105        self.runtime
106            .block_on(self.client.execute_raw(request))
107            .map_err(RuntimeErrorWire::from)
108    }
109
110    /// Streaming companion to [`Self::execute`]. The callback receives
111    /// one [`RuntimeChunkWire`] per complete cbor-seq item as bytes
112    /// arrive on the wire; returning `false` cancels the stream.
113    /// Returns when the stream terminates (clean end, error, or
114    /// cancellation).
115    ///
116    /// Designed for the FFI surface — the callback gets raw CBOR bytes
117    /// per item, so the host language decodes with its native CBOR
118    /// library (Dart, Swift, Kotlin) rather than carrying a typed
119    /// generic across the bridge.
120    pub fn execute_streamed<F>(
121        &self,
122        request: RuntimeRequestWire,
123        on_chunk: F,
124    ) -> Result<(), RuntimeErrorWire>
125    where
126        F: FnMut(RuntimeChunkWire) -> bool + Send,
127    {
128        self.runtime
129            .block_on(self.client.execute_streamed(request, on_chunk))
130    }
131
132    pub fn state(&self) -> Result<PersistedClientState, ClientError> {
133        self.client.state()
134    }
135}