Skip to main content

darkbio_wire/protocol/
responder.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Sending one reply to a received request, or `UNANSWERED` when dropped.
8
9use super::session::SessionInner;
10use super::{Error, Message, Promise, schema};
11use darkbio_clock::Clock;
12use std::sync::Weak;
13use std::time::Instant;
14
15/// Handle for answering one incoming request through the session that received it.
16///
17/// The handler selects the success content, without a static request/response map.
18/// This handle cannot keep its session open or address a replacement session.
19///
20/// Dropping an unanswered responder queues an `UNANSWERED` error without blocking
21/// on I/O, using the session's current autoreply timeout.
22/// Configure it with [`super::Session::set_autoreply_timeout`] or
23/// [`super::Server::set_autoreply_timeout`]. If the session has closed, no reply
24/// is queued.
25///
26/// A held responder counts toward the session's inbound request limit. Queuing a
27/// reply keeps that slot until the writer takes it or the reply is discarded.
28/// See [`super::Session::set_inbound_limits`].
29///
30/// Both [`Self::reply`] and [`Self::fail`] consume the responder, so it cannot
31/// be reused:
32///
33/// ```compile_fail,E0382
34/// use darkbio_wire::protocol::{Responder, schema};
35/// use std::time::Instant;
36///
37/// fn answer_twice(responder: Responder, deadline: Instant) {
38///     let _ = responder.reply(schema::DeviceInfoResponse::default(), deadline);
39///     let _ = responder.fail(schema::Error::new(0x100, "refused"), deadline);
40/// }
41/// ```
42///
43/// Responders cannot be cloned either:
44///
45/// ```compile_fail,E0599
46/// use darkbio_wire::protocol::Responder;
47/// fn duplicate(responder: Responder) { let _ = responder.clone(); }
48/// ```
49#[derive(Debug)]
50pub struct Responder {
51    /// Clock of the session, kept here so it outlives the session.
52    clock: Clock,
53    /// Session that received the request; holding a responder cannot keep it open.
54    session: Weak<SessionInner>,
55    /// Request ID to answer, cleared after queueing a reply so dropping does
56    /// nothing.
57    id: Option<u64>,
58}
59
60impl Responder {
61    /// Returns the clock of this responder's session, which reply deadlines are
62    /// measured on.
63    ///
64    /// It stays available after the session is gone.
65    pub fn clock(&self) -> Clock {
66        self.clock.clone()
67    }
68
69    /// Queues a successful response and consumes the responder.
70    ///
71    /// Returns a promise for writing and flushing it. A closed session returns
72    /// an error immediately. The deadline includes time in the queue and I/O.
73    /// Waiting on the promise does not restart it.
74    ///
75    /// A message invalid for this session's direction fails the promise with
76    /// [`Error::WrongDirection`].
77    ///
78    /// A reply needs no further acknowledgment. Dropping its promise leaves it queued.
79    /// Accepts a protobuf response or [`Message`] directly. Use [`Self::fail`] to
80    /// return an error instead.
81    pub fn reply(
82        self,
83        response: impl Into<Message>,
84        deadline: Instant,
85    ) -> Result<Promise<()>, Error> {
86        self.enqueue(Ok(response.into()), deadline)
87    }
88
89    /// Queues an error response and consumes the responder.
90    ///
91    /// The deadline and write promise work as in [`Self::reply`]. The error
92    /// itself does not close the session. It accepts an application error
93    /// implementing [`super::CodedError`] directly. Use
94    /// [`schema::Error::reserved`] for a named protocol error or
95    /// [`schema::Error::new`] for a bare numeric code.
96    pub fn fail(
97        self,
98        error: impl Into<schema::Error>,
99        deadline: Instant,
100    ) -> Result<Promise<()>, Error> {
101        self.enqueue(Err(error.into()), deadline)
102    }
103
104    /// Queues either kind of response and marks this responder as answered.
105    fn enqueue(
106        mut self,
107        result: Result<Message, schema::Error>,
108        deadline: Instant,
109    ) -> Result<Promise<()>, Error> {
110        let promise = self.session.upgrade().ok_or(Error::Closed)?.reply(
111            self.id.expect("reply obligation present"),
112            result,
113            deadline,
114        )?;
115        self.id = None; // keeps drop from also queueing UNANSWERED
116        Ok(promise)
117    }
118
119    /// Creates a responder for the request taken by
120    /// [`Session::recv`](super::Session::recv).
121    pub(super) fn new(session: Weak<SessionInner>, clock: &Clock, id: u64) -> Self {
122        Self {
123            clock: clock.clone(),
124            session,
125            id: Some(id),
126        }
127    }
128}
129
130impl Drop for Responder {
131    /// Queues `UNANSWERED` if this responder still has an ID and its session is
132    /// open.
133    ///
134    /// The writer sends the error later.
135    fn drop(&mut self) {
136        if let Some(id) = self.id.take()
137            && let Some(session) = self.session.upgrade()
138        {
139            session.reply_unanswered(id);
140        }
141    }
142}
143
144/// Checks responder ownership and compiles success, error and deferred reply paths.
145#[cfg(test)]
146#[cfg_attr(coverage_nightly, coverage(off))]
147mod tests {
148    use crate::protocol::schema::DeviceInfoResponse;
149    use crate::protocol::{Error, Message, Promise, Responder, Session, schema};
150    use std::fmt::Debug;
151    use std::time::Instant;
152
153    /// Compiles receiving a host-side request and returning an application error.
154    #[allow(dead_code)]
155    fn receive_on_host(session: &mut Session, deadline: Instant) -> Result<(), Error> {
156        let (request, responder): (Message, Responder) = session.recv()?;
157        let _ = request;
158        responder
159            .fail(
160                schema::Error::reserved(schema::ReservedErrors::Unspecified, "refused"),
161                deadline,
162            )?
163            .wait()
164    }
165
166    /// Compiles immediate replies, a background reverse request and responder
167    /// abandonment.
168    #[allow(dead_code)]
169    fn receive_on_server(session: &mut Session, deadline: Instant) -> Result<(), Error> {
170        let (request, responder): (Message, Responder) = session.recv()?;
171        match request {
172            Message::DeviceInfoRequest(_) => {
173                let written: Promise<()> =
174                    responder.reply(DeviceInfoResponse::default(), deadline)?;
175                written.wait()?;
176            }
177            Message::Develop(bytes) => {
178                // Opaque development traffic is supported in both directions
179                let requester = session.requester();
180                std::thread::spawn(move || -> Result<(), Error> {
181                    let answer: Vec<u8> = requester.request(bytes, deadline)?.wait()?;
182                    drop(responder.reply(answer, deadline)?);
183                    Ok(())
184                });
185            }
186            _ => drop(responder), // schedules the standard unanswered error
187        }
188        Ok(())
189    }
190
191    /// Checks the bounds required to move the responder to an application thread
192    /// and to print it.
193    #[test]
194    fn test_thread_capabilities() {
195        /// Requires an owned value to be printable and transferable to a
196        /// background thread.
197        fn movable<T: Debug + Send + 'static>() {}
198        movable::<Responder>();
199    }
200}