Skip to main content

darkbio_wire/protocol/
promise.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3
4//! Waiting for request answers and reply write results.
5
6use super::envelope::IncomingEnvelope;
7use super::session::SessionInner;
8use super::{Error, Message};
9use std::marker::PhantomData;
10use std::sync::{Arc, Mutex, Weak, mpsc};
11#[cfg(any(test, feature = "fuzz"))]
12use std::time::Duration;
13use std::time::Instant;
14
15/// Result of a queued request or reply.
16///
17/// Requests return `Promise<Message>`; their wait selects the expected response
18/// type or takes the message directly. Replies return `Promise<()>`; their wait
19/// observes local writing and flushing, not peer receipt or processing.
20///
21/// Dropping a promise leaves the request or reply running with its original
22/// deadline. A timeout does not mean the peer stopped working.
23/// Completed results remain available after the session closes.
24///
25/// A buffered response counts toward the session's inbound byte limit until
26/// `wait()` or drop. It stays encoded until `wait()` decodes it. Late answers and
27/// answers with no matching request are discarded. Answers whose promises were
28/// dropped are also discarded. Their payloads are never decoded.
29///
30/// Each promise returns its result once:
31///
32/// ```compile_fail,E0382
33/// use darkbio_wire::protocol::{DeviceInfoResponse, Message, Promise};
34/// fn take_twice(promise: Promise<Message>) {
35///     let _ = promise.wait::<DeviceInfoResponse>();
36///     let _ = promise.wait::<DeviceInfoResponse>();
37/// }
38/// ```
39pub struct Promise<T> {
40    /// Receives one result from the corresponding `PendingOperation`. A buffered
41    /// result remains available even after the session is dropped.
42    result: mpsc::Receiver<Result<PromiseResult, Error>>,
43    /// Completion and its optional notification, independent of session lifetime.
44    notification: Arc<Mutex<NotificationState>>,
45    /// Registration is single-use even after the notification has been sent.
46    registered: bool,
47    /// Public result type; responses stay encoded until `wait()`.
48    value: PhantomData<fn() -> T>,
49    /// Lets the waiter call `SessionInner::expire()` when its deadline is reached.
50    session: Weak<SessionInner>,
51    /// Deadline supplied with the request or reply. `wait()` does not restart it.
52    deadline: Instant,
53    /// One-shot notification just before entering the blocking receive.
54    #[cfg(any(test, feature = "fuzz"))]
55    wait_hook: Option<mpsc::Sender<()>>,
56}
57
58impl Promise<Message> {
59    /// Blocks for completion, then decodes the response. The response must be
60    /// accepted before the request's original deadline. Decoding is outside that
61    /// deadline. An accepted response remains available after the deadline or closure.
62    ///
63    /// Taking the response removes its bytes from the inbound byte count before
64    /// decoding it. Invalid protobuf returns [`Error::Malformed`] and closes its
65    /// original session.
66    ///
67    /// Selects the expected response type at this call, either through inference
68    /// or `wait::<Response>()`. Message extraction checks the content variant and
69    /// returns [`Error::UnexpectedResponse`] on mismatch. The [`Message`] enum can
70    /// also be taken directly for application pattern matching.
71    pub fn wait<T>(self) -> Result<T, Error>
72    where
73        T: TryFrom<Message>,
74        Error: From<T::Error>,
75    {
76        T::try_from(self.wait_result()?.response()?).map_err(Error::from)
77    }
78
79    /// Observes reader/deadline worker completion without servicing deadlines itself.
80    #[cfg(any(test, feature = "fuzz"))]
81    pub(super) fn wait_worker_result(self) -> Result<Message, Error> {
82        self.worker_result()?.response()
83    }
84}
85
86impl Promise<()> {
87    /// Blocks for local write/flush completion under the reply's original deadline.
88    /// The peer does not send another acknowledgment for this reply.
89    pub fn wait(self) -> Result<(), Error> {
90        self.wait_result()?.written()
91    }
92
93    /// Observes writer/deadline worker completion without servicing deadlines itself.
94    #[cfg(any(test, feature = "fuzz"))]
95    pub(super) fn wait_worker_result(self) -> Result<(), Error> {
96        self.worker_result()?.written()
97    }
98}
99
100impl<T> Promise<T> {
101    /// Creates a promise and the sender that its `PendingOperation` will own.
102    /// The channel holds one result without waiting for the caller to receive it.
103    pub(super) fn pair(
104        session: Weak<SessionInner>,
105        deadline: Instant,
106        response: bool,
107    ) -> (ResultSender, Self) {
108        let (sender, result) = mpsc::sync_channel(1);
109        let notification = Arc::new(Mutex::new(NotificationState::default()));
110        (
111            ResultSender {
112                response,
113                result: sender,
114                notification: notification.clone(),
115            },
116            Self {
117                result,
118                notification,
119                registered: false,
120                value: PhantomData,
121                session,
122                deadline,
123                #[cfg(any(test, feature = "fuzz"))]
124                wait_hook: None,
125            },
126        )
127    }
128
129    /// Sends `event` through the unbounded channel once a terminal result is ready.
130    /// Requests notify on a response or error; replies notify on local write/flush
131    /// completion or error. Notification does not imply success or peer receipt.
132    /// The result is published before the event, so `wait()` can then extract it
133    /// without waiting for completion. Response decoding still happens in `wait()`.
134    ///
135    /// Registering or receiving a notification neither decodes the response nor
136    /// releases its retained bytes. They remain charged until `wait()` or drop.
137    /// Deadlines are unchanged, and registration does not service expiry.
138    ///
139    /// An already-completed promise sends immediately on the registering thread,
140    /// even after its session is gone. Otherwise the thread settling the operation
141    /// sends the event. A disconnected notification receiver discards the event
142    /// without affecting the result. Copy tokens cannot run application destructors
143    /// on a protocol worker; keep any associated payload on the consumer's side.
144    ///
145    /// Dropping the promise clears an unsent notification without cancelling the
146    /// operation. An event already sent can outlive its promise.
147    ///
148    /// # Panics
149    /// Panics if notification was already registered on this promise.
150    pub fn notify<E: Copy + Send + 'static>(&mut self, sender: mpsc::Sender<E>, event: E) {
151        // Check before locking so caller misuse cannot poison shared state and
152        // cause another panic when the promise is dropped during unwinding.
153        assert!(!self.registered, "promise notification already registered");
154
155        self.registered = true;
156        let mut notification = self.notification.lock().expect("notification not poisoned");
157        if notification.done {
158            let _ = sender.send(event);
159        } else {
160            notification.hook = Some(Box::new(move || {
161                let _ = sender.send(event);
162            }));
163        }
164    }
165
166    /// Waits for the result channel. On timeout, asks the session to expire pending
167    /// operations, then reads the result it sent. The session decides whether an
168    /// answer or timeout came first; results already in the channel are kept.
169    #[allow(unused_mut)] // The test-only wait hook must be taken now that we implement Drop.
170    fn wait_result(mut self) -> Result<PromiseResult, Error> {
171        if let Some(session) = self.session.upgrade() {
172            session.expire();
173        }
174        #[cfg(any(test, feature = "fuzz"))]
175        if let Some(wait_hook) = self.wait_hook.take() {
176            let _ = wait_hook.send(());
177        }
178        loop {
179            match self
180                .result
181                .recv_timeout(self.deadline.saturating_duration_since(Instant::now()))
182            {
183                Ok(result) => return result,
184                Err(mpsc::RecvTimeoutError::Timeout) => {
185                    if let Some(session) = self.session.upgrade() {
186                        session.expire();
187                    }
188                }
189                Err(mpsc::RecvTimeoutError::Disconnected) => {
190                    unreachable!("registered operation settles before its sender is released");
191                }
192            }
193        }
194    }
195
196    /// Notifies a test just before `wait_result()` starts waiting on the result channel.
197    /// A result sent before the wait stays buffered in that channel.
198    #[cfg(any(test, feature = "fuzz"))]
199    pub(super) fn watch_wait(&mut self) -> mpsc::Receiver<()> {
200        let (sender, receiver) = mpsc::channel();
201        self.wait_hook = Some(sender);
202        receiver
203    }
204
205    /// Waits for a worker result without calling `SessionInner::expire()`, so tests
206    /// can prove workers process deadlines without help from `Promise::wait()`.
207    /// Fails the test if the result does not arrive within five seconds.
208    #[cfg(any(test, feature = "fuzz"))]
209    fn worker_result(self) -> Result<PromiseResult, Error> {
210        self.result
211            .recv_timeout(Duration::from_secs(5))
212            .expect("protocol worker must settle the promise")
213    }
214}
215
216impl<T> Drop for Promise<T> {
217    /// Clears an unsent event. The protocol never drops a handed-out promise
218    /// under its session lock; this path only takes the notification lock.
219    fn drop(&mut self) {
220        let hook = self
221            .notification
222            .lock()
223            .expect("notification not poisoned")
224            .hook
225            .take();
226        drop(hook);
227    }
228}
229
230/// Shared notification state survives the session without retaining it.
231#[derive(Default)]
232struct NotificationState {
233    /// The result has been published, including when no hook was registered yet.
234    done: bool,
235    /// Internally constructed channel send; never an application callback.
236    hook: Option<Box<dyn FnOnce() + Send>>,
237}
238
239/// Single-use result sender for a request or reply. Its one-slot channel never
240/// needs to wait for the application to receive the result.
241pub(super) struct ResultSender {
242    /// Requests expect peer answers; replies expect local write completion.
243    pub(super) response: bool,
244    /// Receives exactly one result before this sender is released.
245    result: mpsc::SyncSender<Result<PromiseResult, Error>>,
246    /// Serializes publication and notification with registration and promise drop.
247    notification: Arc<Mutex<NotificationState>>,
248}
249
250impl ResultSender {
251    /// Publishes before notifying, preserving disconnection for byte admission.
252    pub(super) fn send(
253        self,
254        result: Result<PromiseResult, Error>,
255    ) -> Result<(), mpsc::SendError<Result<PromiseResult, Error>>> {
256        // Lock before publishing: a concurrent waiter must not drop the promise
257        // and clear its hook between receiving the result and our notification.
258        let mut notification = self.notification.lock().expect("notification not poisoned");
259        self.result.send(result)?;
260        notification.done = true;
261        if let Some(hook) = notification.hook.take() {
262            hook();
263        }
264        Ok(())
265    }
266}
267
268/// An operation result before the application waits on its promise.
269pub(super) enum PromiseResult {
270    /// Original response bytes, decoded only when a request promise is observed.
271    Response(IncomingEnvelope),
272    /// Local reply writing and flushing completed; no incoming message exists.
273    Written,
274}
275
276impl PromiseResult {
277    /// Decodes the response carried by a request operation's result channel.
278    fn response(self) -> Result<Message, Error> {
279        match self {
280            Self::Response(message) => message.decode(),
281            Self::Written => unreachable!("requests complete with responses"),
282        }
283    }
284
285    /// Checks that a reply operation reported its local write completion.
286    fn written(self) -> Result<(), Error> {
287        match self {
288            Self::Written => Ok(()),
289            Self::Response(_) => unreachable!("replies complete with write results"),
290        }
291    }
292}
293
294/// Checks that both promise owners can be transferred to application threads.
295#[cfg(test)]
296#[cfg_attr(coverage_nightly, coverage(off))]
297mod tests {
298    use super::{Error, Message, Promise, PromiseResult};
299    use std::panic::{AssertUnwindSafe, catch_unwind};
300    use std::sync::{Weak, mpsc};
301    use std::time::{Duration, Instant};
302
303    /// Misuse panics before poisoning the lock, keeping the original registration
304    /// and result usable whether the promise was pending or already completed.
305    #[test]
306    fn test_duplicate_notification() {
307        for completed in [false, true] {
308            let (sender, mut promise) = Promise::<()>::pair(Weak::new(), Instant::now(), false);
309            let (events, receiver) = mpsc::channel();
310            promise.notify(events.clone(), 1);
311            let sender = if completed {
312                assert!(sender.send(Ok(PromiseResult::Written)).is_ok());
313                None
314            } else {
315                Some(sender)
316            };
317            assert!(catch_unwind(AssertUnwindSafe(|| promise.notify(events, 2))).is_err());
318            if let Some(sender) = sender {
319                assert!(sender.send(Ok(PromiseResult::Written)).is_ok());
320            }
321            assert_eq!(receiver.try_recv(), Ok(1));
322            assert!(receiver.try_recv().is_err());
323            promise.wait().unwrap();
324        }
325    }
326
327    /// Losing the event consumer does not change success or failure, including
328    /// when registration happens after publication and sender destruction.
329    #[test]
330    fn test_disconnected_notification() {
331        for completed in [false, true] {
332            for success in [false, true] {
333                let (sender, mut promise) = Promise::<()>::pair(Weak::new(), Instant::now(), false);
334                let (events, receiver) = mpsc::channel();
335                drop(receiver);
336                let result = if success {
337                    Ok(PromiseResult::Written)
338                } else {
339                    Err(Error::Timeout)
340                };
341                if completed {
342                    assert!(sender.send(result).is_ok());
343                    promise.notify(events, 1);
344                } else {
345                    promise.notify(events, 1);
346                    assert!(sender.send(result).is_ok());
347                }
348                match promise.wait() {
349                    Ok(()) => assert!(success),
350                    Err(Error::Timeout) => assert!(!success),
351                    result => panic!("unexpected result: {result:?}"),
352                }
353            }
354        }
355    }
356
357    /// A waiter can receive immediately after publication, but its Drop must not
358    /// clear the hook before the sender has emitted the completion event.
359    #[test]
360    fn test_notification_with_waiter() {
361        for _ in 0..32 {
362            let (sender, mut promise) =
363                Promise::<()>::pair(Weak::new(), Instant::now() + Duration::from_secs(5), false);
364            let (events, receiver) = mpsc::channel();
365            promise.notify(events, 1);
366            let waiting = promise.watch_wait();
367            let waiter = std::thread::spawn(move || promise.wait());
368            waiting.recv_timeout(Duration::from_secs(5)).unwrap();
369            assert!(sender.send(Ok(PromiseResult::Written)).is_ok());
370            waiter.join().unwrap().unwrap();
371            assert_eq!(receiver.try_recv(), Ok(1));
372            assert!(receiver.try_recv().is_err());
373        }
374    }
375
376    /// Checks the send bound required to transfer ownership to an application thread.
377    #[test]
378    fn test_thread_capabilities() {
379        /// Requires an owned value to be transferable to a background thread.
380        fn movable<T: Send + 'static>() {}
381        movable::<Promise<Message>>();
382        movable::<Promise<()>>();
383    }
384}