Skip to main content

ma_core/
outbox.rs

1//! Transport-agnostic send handle to a remote ma service.
2//!
3//! An `Outbox` wraps the transport details and exposes a single `send()`
4//! method. Outboxes are lightweight and meant to be kept alive for the
5//! duration of a session — `ma-core` manages the underlying connections.
6//!
7//! `send()` takes a [`Message`], validates it,
8//! serializes to CBOR, and transmits. Malformed or expired messages
9//! are rejected before anything hits the wire.
10//!
11//! Requires the `iroh` feature.
12//!
13//! ```ignore
14//! let mut outbox = ep.outbox(&resolver, "did:ma:k51qzi5uqu5d…", INBOX_PROTOCOL_ID).await?;
15//! outbox.send(&message).await?;
16//! // Keep the outbox alive — no need to close it.
17//! ```
18
19use crate::error::{Error, Result};
20use crate::Message;
21use async_trait::async_trait;
22
23#[async_trait]
24pub(crate) trait OutboxWire: Send + std::fmt::Debug {
25    async fn send_payload(&mut self, payload: &[u8]) -> Result<()>;
26    fn close_box(self: Box<Self>);
27}
28
29/// A transport-agnostic write handle to a remote service.
30///
31/// The caller doesn't need to know the underlying transport.
32#[derive(Debug)]
33pub struct Outbox {
34    inner: Option<Box<dyn OutboxWire>>,
35    did: String,
36    protocol: String,
37}
38
39impl Outbox {
40    /// Create an outbox backed by a transport implementation.
41    pub(crate) fn from_transport<T>(transport: T, did: String, protocol: String) -> Self
42    where
43        T: OutboxWire + 'static,
44    {
45        Self {
46            inner: Some(Box::new(transport)),
47            did,
48            protocol,
49        }
50    }
51
52    /// Send a ma message to the remote service.
53    ///
54    /// Validates the message headers, serializes to CBOR, and transmits.
55    ///
56    /// # Errors
57    /// Returns an error if validation, serialization, or transport send fails.
58    pub async fn send(&mut self, message: &Message) -> Result<()> {
59        message.headers().validate()?;
60        let cbor = message.encode()?;
61        match self.inner.as_mut() {
62            Some(transport) => transport.send_payload(&cbor).await,
63            None => Err(Error::ConnectionClosed("outbox is closed".to_string())),
64        }
65    }
66
67    /// The DID this outbox delivers to.
68    #[must_use]
69    pub fn did(&self) -> &str {
70        &self.did
71    }
72
73    /// The protocol this outbox is connected to.
74    #[must_use]
75    pub fn protocol(&self) -> &str {
76        &self.protocol
77    }
78
79    /// Close the outbox gracefully.
80    pub fn close(mut self) {
81        if let Some(transport) = self.inner.take() {
82            transport.close_box();
83        }
84    }
85}