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("{0} requires a stable client operation ID")]
63    MissingOperationId(&'static str),
64    #[error("invalid request metadata: {0}")]
65    Metadata(#[from] crate::RequestMetadataError),
66    #[error("transport failure: {0}")]
67    Transport(E),
68    #[error("invalid protobuf response: {0}")]
69    Decode(#[from] prost::DecodeError),
70}
71
72pub struct Client<T> {
73    transport: T,
74    implemented: BTreeSet<String>,
75}
76
77impl<T: RpcTransport> Client<T> {
78    /// implemented is the authenticated endpoint's advertised handler set, not
79    /// ALL_METHODS or the proto's maturity declarations.
80    pub fn new(transport: T, implemented: impl IntoIterator<Item = String>) -> Self {
81        Self {
82            transport,
83            implemented: implemented.into_iter().collect(),
84        }
85    }
86
87    fn encode<M: Rpc>(&self, request: &M::Request) -> Result<Vec<u8>, ClientError<T::Error>> {
88        let method = M::METHOD;
89        if !self.implemented.contains(method.path) {
90            return Err(ClientError::NotImplemented(method.path));
91        }
92        let bytes = request.encode_to_vec();
93        if method.client_operation_id_required {
94            let Some(field) = method.client_operation_id_field_number else {
95                return Err(ClientError::MissingOperationId(method.path));
96            };
97            let id = crate::transport::protobuf_string_field(&bytes, field)?;
98            if id.is_none_or(|value| value.trim().is_empty()) {
99                return Err(ClientError::MissingOperationId(method.path));
100            }
101        }
102        Ok(bytes)
103    }
104
105    pub async fn call<M: UnaryRpc>(
106        &self,
107        request: &M::Request,
108    ) -> Result<M::Response, ClientError<T::Error>> {
109        let bytes = self
110            .transport
111            .unary(M::METHOD, self.encode::<M>(request)?)
112            .await
113            .map_err(ClientError::Transport)?;
114        Ok(M::Response::decode(bytes.as_slice())?)
115    }
116
117    pub async fn observe<M: ServerStreamingRpc>(
118        &self,
119        request: &M::Request,
120    ) -> Result<Messages<T::Reader, M::Response>, ClientError<T::Error>> {
121        let reader = self
122            .transport
123            .observe(M::METHOD, self.encode::<M>(request)?)
124            .await
125            .map_err(ClientError::Transport)?;
126        Ok(Messages {
127            reader,
128            done: false,
129            message: PhantomData,
130        })
131    }
132
133    /// Returns independently borrowable halves so receives cannot block sends.
134    pub async fn exchange<M: BidirectionalRpc>(
135        &self,
136        opening: &M::Request,
137    ) -> Result<
138        (
139            Sender<T::Writer, M::Request>,
140            Messages<T::Reader, M::Response>,
141        ),
142        ClientError<T::Error>,
143    > {
144        let (writer, reader) = self
145            .transport
146            .exchange(M::METHOD, self.encode::<M>(opening)?)
147            .await
148            .map_err(ClientError::Transport)?;
149        Ok((
150            Sender {
151                writer,
152                finished: false,
153                message: PhantomData,
154            },
155            Messages {
156                reader,
157                done: false,
158                message: PhantomData,
159            },
160        ))
161    }
162}
163
164pub struct Messages<R: MessageReader, O> {
165    reader: R,
166    done: bool,
167    message: PhantomData<O>,
168}
169
170impl<R: MessageReader, O> Messages<R, O> {
171    /// Stop receiving without cancelling any durable server operation. This is
172    /// also safe to call after a typed terminal control such as StreamComplete.
173    pub fn cancel(&mut self) {
174        if !self.done {
175            self.reader.cancel();
176            self.done = true;
177        }
178    }
179}
180
181impl<R: MessageReader, O: Message + Default> Messages<R, O> {
182    pub async fn next(&mut self) -> Result<Option<O>, ClientError<R::Error>> {
183        if self.done {
184            return Ok(None);
185        }
186        let decoded = match self.reader.next().await {
187            Ok(Some(bytes)) => O::decode(bytes.as_slice())
188                .map(Some)
189                .map_err(ClientError::Decode),
190            Ok(None) => {
191                self.done = true;
192                Ok(None)
193            }
194            Err(error) => Err(ClientError::Transport(error)),
195        };
196        if decoded.is_err() {
197            self.reader.cancel();
198            self.done = true;
199        }
200        decoded
201    }
202}
203
204impl<R: MessageReader, O> Drop for Messages<R, O> {
205    fn drop(&mut self) {
206        self.cancel();
207    }
208}
209
210pub struct Sender<W: MessageWriter, I> {
211    writer: W,
212    finished: bool,
213    message: PhantomData<I>,
214}
215
216impl<W: MessageWriter, I: Message> Sender<W, I> {
217    pub async fn send(&mut self, message: &I) -> Result<(), W::Error> {
218        self.writer.send(message.encode_to_vec()).await
219    }
220    pub async fn finish(mut self) -> Result<(), W::Error> {
221        self.writer.finish().await?;
222        self.finished = true;
223        Ok(())
224    }
225}
226
227impl<W: MessageWriter, I> Drop for Sender<W, I> {
228    fn drop(&mut self) {
229        if !self.finished {
230            self.writer.abort();
231        }
232    }
233}