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}