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