Skip to main content

heddle_api/v2/
client.rs

1//! Typed, statically dispatched clients over caller-owned transport and signers.
2use std::{collections::BTreeSet, future::Future, marker::PhantomData};
3
4use prost::Message;
5
6use super::MethodDescriptor;
7
8pub trait Rpc {
9    type Request: Message;
10    type Response: Message + Default;
11    const METHOD: &'static MethodDescriptor;
12}
13pub trait UnaryRpc: Rpc {}
14pub trait ServerStreamingRpc: Rpc {}
15pub trait ClientStreamingRpc: Rpc {}
16pub trait BidirectionalRpc: Rpc {}
17
18/// Frames are already transport-decoded protobuf messages. Implementations
19/// enforce negotiated byte limits before allocating/decoding a frame body.
20pub trait MessageReader: Send {
21    type Error: std::error::Error + Send + Sync + 'static;
22    fn next(&mut self) -> impl Future<Output = Result<Option<Vec<u8>>, Self::Error>> + Send;
23    /// Stops observation; it does not cancel a durable server operation.
24    fn cancel(&mut self);
25}
26
27pub trait MessageWriter: Send {
28    type Error: std::error::Error + Send + Sync + 'static;
29    fn send(&mut self, message: Vec<u8>) -> impl Future<Output = Result<(), Self::Error>> + Send;
30    fn finish(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send;
31    fn abort(&mut self);
32}
33
34/// Owns endpoint selection, credentials, exact-request signing, deadlines and
35/// Iroh framing. No automatic write retry or cross-endpoint failover is implied.
36/// A streaming implementation pulls input and output under transport flow control.
37pub trait RpcTransport: Send + Sync {
38    type Error: std::error::Error + Send + Sync + 'static;
39    type Reader: MessageReader<Error = Self::Error>;
40    type Writer: MessageWriter<Error = Self::Error>;
41    fn unary(
42        &self,
43        method: &'static MethodDescriptor,
44        request: Vec<u8>,
45    ) -> impl Future<Output = Result<Vec<u8>, Self::Error>> + Send;
46    fn observe(
47        &self,
48        method: &'static MethodDescriptor,
49        request: Vec<u8>,
50    ) -> impl Future<Output = Result<Self::Reader, Self::Error>> + Send;
51    fn exchange(
52        &self,
53        method: &'static MethodDescriptor,
54        opening: Vec<u8>,
55    ) -> impl Future<Output = Result<(Self::Writer, Self::Reader), Self::Error>> + Send;
56}
57
58#[derive(Debug, thiserror::Error)]
59pub enum ClientError<E: std::error::Error> {
60    #[error("endpoint does not implement {0}")]
61    NotImplemented(&'static str),
62    #[error("incompatible peer for {0}")]
63    Protocol(&'static str),
64    #[error("{0} requires a stable client operation ID")]
65    MissingOperationId(&'static str),
66    #[error("invalid request metadata: {0}")]
67    Metadata(#[from] crate::RequestMetadataError),
68    #[error("transport failure: {0}")]
69    Transport(E),
70    #[error("invalid request variant: {0}")]
71    RequestFeature(#[from] crate::hybrid_codec::Reject),
72    #[error("invalid protobuf response: {0}")]
73    Decode(#[from] prost::DecodeError),
74}
75
76pub struct Client<T> {
77    transport: T,
78    implemented: BTreeSet<String>,
79    protocol: Option<crate::heddle::api::common::ProtocolCompatibility>,
80}
81
82impl<T: RpcTransport> Client<T> {
83    /// implemented is the authenticated endpoint's advertised handler set, not
84    /// ALL_METHODS or the proto's maturity declarations.
85    pub fn new(transport: T, implemented: impl IntoIterator<Item = String>) -> Self {
86        Self {
87            transport,
88            implemented: implemented.into_iter().collect(),
89            protocol: None,
90        }
91    }
92
93    /// Set support obtained from DescribeEndpoint on this authenticated endpoint.
94    /// Discard the client on endpoint/connection replacement; never reuse support
95    /// across peers. Missing support keeps all gated routes closed.
96    pub fn with_protocol(
97        mut self,
98        protocol: crate::heddle::api::common::ProtocolCompatibility,
99    ) -> Self {
100        self.protocol = Some(protocol);
101        self
102    }
103
104    fn encode<M: Rpc>(&self, request: &M::Request) -> Result<Vec<u8>, ClientError<T::Error>> {
105        let method = M::METHOD;
106        if !self.implemented.contains(method.path) {
107            return Err(ClientError::NotImplemented(method.path));
108        }
109        if !method.mandatory_features.is_empty() {
110            crate::import_authority::require_hybrid_peer(self.protocol.as_ref())
111                .map_err(|_| ClientError::Protocol(method.path))?;
112        }
113        let bytes = request.encode_to_vec();
114        super::protocol::validate_request_features(method.path, &bytes, self.protocol.as_ref())?;
115        validate_stream_protocol(method, &bytes, true, true)
116            .map_err(|_| ClientError::Protocol(method.path))?;
117        if method.client_operation_id_required {
118            let Some(field) = method.client_operation_id_field_number else {
119                return Err(ClientError::MissingOperationId(method.path));
120            };
121            let id = crate::transport::protobuf_string_field(&bytes, field)?;
122            if id.is_none_or(|value| value.trim().is_empty()) {
123                return Err(ClientError::MissingOperationId(method.path));
124            }
125        }
126        Ok(bytes)
127    }
128
129    /// Transports the caller's exact request and request PoP. For HYBRID imports,
130    /// Prepare returns frozen fields/bounds; the browser completes and signs the
131    /// delegation explicitly. Commit hosts use verify_prepared_delegation with
132    /// their durable reservation; this generic client grants no import authority.
133    pub async fn call<M: UnaryRpc>(
134        &self,
135        request: &M::Request,
136    ) -> Result<M::Response, ClientError<T::Error>> {
137        let bytes = self
138            .transport
139            .unary(M::METHOD, self.encode::<M>(request)?)
140            .await
141            .map_err(ClientError::Transport)?;
142        Ok(M::Response::decode(bytes.as_slice())?)
143    }
144
145    pub async fn observe<M: ServerStreamingRpc>(
146        &self,
147        request: &M::Request,
148    ) -> Result<Messages<T::Reader, M::Response>, ClientError<T::Error>> {
149        let reader = self
150            .transport
151            .observe(M::METHOD, self.encode::<M>(request)?)
152            .await
153            .map_err(ClientError::Transport)?;
154        Ok(Messages {
155            reader,
156            method: M::METHOD,
157            first: true,
158            done: false,
159            message: PhantomData,
160        })
161    }
162
163    /// Returns independently borrowable halves so receives cannot block sends.
164    pub async fn exchange<M: BidirectionalRpc>(
165        &self,
166        opening: &M::Request,
167    ) -> Result<
168        (
169            Sender<T::Writer, M::Request>,
170            Messages<T::Reader, M::Response>,
171        ),
172        ClientError<T::Error>,
173    > {
174        let (writer, reader) = self
175            .transport
176            .exchange(M::METHOD, self.encode::<M>(opening)?)
177            .await
178            .map_err(ClientError::Transport)?;
179        Ok((
180            Sender {
181                writer,
182                method: M::METHOD,
183                finished: false,
184                message: PhantomData,
185            },
186            Messages {
187                reader,
188                method: M::METHOD,
189                first: true,
190                done: false,
191                message: PhantomData,
192            },
193        ))
194    }
195}
196
197pub struct Messages<R: MessageReader, O> {
198    reader: R,
199    method: &'static MethodDescriptor,
200    first: bool,
201    done: bool,
202    message: PhantomData<O>,
203}
204
205impl<R: MessageReader, O> Messages<R, O> {
206    /// Stop receiving without cancelling any durable server operation. This is
207    /// also safe to call after a typed terminal control such as StreamComplete.
208    pub fn cancel(&mut self) {
209        if !self.done {
210            self.reader.cancel();
211            self.done = true;
212        }
213    }
214}
215
216impl<R: MessageReader, O: Message + Default> Messages<R, O> {
217    pub async fn next(&mut self) -> Result<Option<O>, ClientError<R::Error>> {
218        if self.done {
219            return Ok(None);
220        }
221        let decoded = match self.reader.next().await {
222            Ok(Some(bytes)) => {
223                if validate_stream_protocol(self.method, &bytes, false, self.first).is_err() {
224                    Err(ClientError::Protocol(self.method.path))
225                } else {
226                    self.first = false;
227                    O::decode(bytes.as_slice())
228                        .map(Some)
229                        .map_err(ClientError::Decode)
230                }
231            }
232            Ok(None) => {
233                if self.first && is_hybrid_stream(self.method) {
234                    Err(ClientError::Protocol(self.method.path))
235                } else {
236                    self.done = true;
237                    Ok(None)
238                }
239            }
240            Err(error) => Err(ClientError::Transport(error)),
241        };
242        if decoded.is_err() {
243            self.reader.cancel();
244            self.done = true;
245        }
246        decoded
247    }
248}
249
250impl<R: MessageReader, O> Drop for Messages<R, O> {
251    fn drop(&mut self) {
252        self.cancel();
253    }
254}
255
256pub struct Sender<W: MessageWriter, I> {
257    writer: W,
258    method: &'static MethodDescriptor,
259    finished: bool,
260    message: PhantomData<I>,
261}
262
263impl<W: MessageWriter, I: Message> Sender<W, I> {
264    pub async fn send(&mut self, message: &I) -> Result<(), ClientError<W::Error>> {
265        let bytes = message.encode_to_vec();
266        validate_stream_protocol(self.method, &bytes, true, false)
267            .map_err(|_| ClientError::Protocol(self.method.path))?;
268        self.writer
269            .send(bytes)
270            .await
271            .map_err(ClientError::Transport)
272    }
273    pub async fn finish(mut self) -> Result<(), W::Error> {
274        self.writer.finish().await?;
275        self.finished = true;
276        Ok(())
277    }
278}
279
280impl<W: MessageWriter, I> Drop for Sender<W, I> {
281    fn drop(&mut self) {
282        if !self.finished {
283            self.writer.abort();
284        }
285    }
286}
287
288// Native opening/ready checks follow the RpcContract gate. Ordinary Sync
289// remains ungated until heddle and weft ship HYBRID support (api#307).
290fn is_hybrid_stream(method: &MethodDescriptor) -> bool {
291    method.path.starts_with("/heddle.api.v1alpha2.SyncService/")
292        && !method.mandatory_features.is_empty()
293}
294/// Validate openings and ready frames before exposing any bytes to consumers.
295fn validate_stream_protocol(
296    method: &MethodDescriptor,
297    bytes: &[u8],
298    request: bool,
299    first: bool,
300) -> Result<(), crate::hybrid_codec::Reject> {
301    use crate::heddle::api::v1alpha2::*;
302    use crate::hybrid_codec::Reject;
303    use crate::import_authority::require_hybrid_peer;
304    if !is_hybrid_stream(method) {
305        return Ok(());
306    }
307    macro_rules! frame {
308        ($ty:ty, $variant:path) => {{
309            let value = <$ty>::decode(bytes).map_err(|_| Reject::Protocol)?;
310            match value.body {
311                Some($variant(open)) => require_hybrid_peer(open.protocol.as_ref()),
312                _ if first => Err(Reject::Protocol),
313                _ => Ok(()),
314            }
315        }};
316    }
317    match (method.path.rsplit('/').next(), request) {
318        (Some("Fetch"), true) => {
319            frame!(FetchClientFrame, fetch_client_frame::Body::Open)
320        }
321        (Some("Fetch"), false) => frame!(FetchServerFrame, fetch_server_frame::Body::Ready),
322        (Some("PublishContent"), true) => frame!(
323            PublishContentClientFrame,
324            publish_content_client_frame::Body::Open
325        ),
326        (Some("PublishContent"), false) => frame!(
327            PublishContentServerFrame,
328            publish_content_server_frame::Body::Ready
329        ),
330        (Some("ReplicateThread"), true) => {
331            frame!(ReplicateThreadRequest, replicate_thread_request::Body::Open)
332        }
333        (Some("ReplicateThread"), false) => frame!(
334            ReplicateThreadResponse,
335            replicate_thread_response::Body::Ready
336        ),
337        _ => Err(Reject::Protocol),
338    }
339}