Skip to main content

ma_core/
endpoint.rs

1//! Endpoint trait.
2//!
3//! [`MaEndpoint`] defines the shared interface for all ma transport endpoints.
4//! The crate currently provides an internal iroh-backed transport implementation.
5
6use async_trait::async_trait;
7
8#[cfg(feature = "iroh")]
9use crate::error::Error;
10use crate::error::Result;
11use crate::inbox::Inbox;
12#[cfg(feature = "iroh")]
13use crate::ipfs::DidDocumentResolver;
14use crate::service::INBOX_PROTOCOL_ID;
15#[cfg(feature = "iroh")]
16use crate::transport::resolve_endpoint_for_protocol;
17#[cfg(feature = "iroh")]
18use crate::Document;
19use crate::Message;
20#[cfg(feature = "iroh")]
21use crate::Outbox;
22
23/// Default inbox capacity for services.
24pub const DEFAULT_INBOX_CAPACITY: usize = 256;
25
26/// Default protocol ID for unqualified send/request calls.
27pub const DEFAULT_DELIVERY_PROTOCOL_ID: &str = INBOX_PROTOCOL_ID;
28
29/// Shared interface for ma transport endpoints.
30///
31/// Each implementation provides inbox/outbox
32/// messaging and advertises its registered services for DID documents.
33#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
34#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
35pub trait MaEndpoint: Send + Sync {
36    /// The endpoint's public identifier (hex string).
37    fn id(&self) -> String;
38
39    /// Register a service protocol and return an [`Inbox`] for receiving messages.
40    ///
41    /// Implementations should ensure the service is reachable for inbound delivery
42    /// once it has been registered, so callers do not need a second explicit
43    /// "listen" step in the common case.
44    fn service(&mut self, protocol: &str) -> Inbox<Message>;
45
46    /// Return service strings for all registered protocols.
47    ///
48    /// Each entry is suitable for inclusion in a DID document's `ma.services` array.
49    fn services(&self) -> Vec<String>;
50
51    /// Return service strings as a JSON array value.
52    fn services_json(&self) -> serde_json::Value {
53        serde_json::Value::Array(
54            self.services()
55                .into_iter()
56                .map(serde_json::Value::String)
57                .collect(),
58        )
59    }
60
61    /// Build a [`crate::MaExtension`] pre-populated with this endpoint's
62    /// service strings.
63    ///
64    /// Use this as the starting point when constructing the `ma:` field for a
65    /// DID document. Chain additional builder methods on the returned value
66    /// before passing it to [`crate::config::SecretBundle::build_document`] or
67    /// [`crate::Document::set_ma_extension`]:
68    ///
69    /// ```ignore
70    /// let ma = endpoint.ma_extension().kind("world");
71    /// let document = bundle.build_document(ma)?;
72    /// ```
73    fn ma_extension(&self) -> crate::doc::MaExtension {
74        crate::doc::MaExtension::new().services(self.services())
75    }
76
77    /// Fire-and-forget to a target on a specific protocol.
78    async fn send_to(&self, target: &str, protocol: &str, message: &Message) -> Result<()>;
79
80    /// Gracefully shut down the endpoint, closing all cached connections.
81    async fn close(&mut self);
82
83    /// Open a transport-agnostic outbox to a remote DID and protocol.
84    ///
85    /// Resolves the DID document, checks `ma.services` for the requested
86    /// protocol, and delegates the actual transport connection to
87    /// [`Self::connect_outbox`]. Override this only for non-standard resolution.
88    #[cfg(feature = "iroh")]
89    async fn outbox(
90        &self,
91        resolver: &dyn DidDocumentResolver,
92        did: &str,
93        protocol: &str,
94    ) -> Result<Outbox> {
95        let doc = resolver.resolve(did).await?;
96
97        let services = doc
98            .ma
99            .as_ref()
100            .and_then(|ma| ma.get("services").ok().flatten())
101            .and_then(|services| serde_json::to_value(services).ok());
102
103        let endpoint_id =
104            resolve_endpoint_for_protocol(services.as_ref(), protocol).ok_or_else(|| {
105                Error::NoInboxTransport(format!("{did} has no service for {protocol}"))
106            })?;
107
108        self.connect_outbox(&doc, &endpoint_id, did, protocol).await
109    }
110
111    /// Open a transport-level outbox given a pre-resolved document and endpoint ID.
112    ///
113    /// Implementors use `doc` for transport-specific routing hints (e.g. relay URLs)
114    /// and `endpoint_id` as the peer address on their transport layer.
115    #[cfg(feature = "iroh")]
116    async fn connect_outbox(
117        &self,
118        doc: &Document,
119        endpoint_id: &str,
120        did: &str,
121        protocol: &str,
122    ) -> Result<Outbox>;
123
124    /// Fire-and-forget to a target on the default inbox protocol.
125    async fn send(&self, target: &str, message: &Message) -> Result<()> {
126        self.send_to(target, DEFAULT_DELIVERY_PROTOCOL_ID, message)
127            .await
128    }
129}