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