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