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#[cfg(feature = "gossip")]
23use iroh::EndpointId;
24#[cfg(feature = "gossip")]
25use iroh_gossip::api::{GossipReceiver, GossipSender};
26
27/// Default inbox capacity for services.
28pub const DEFAULT_INBOX_CAPACITY: usize = 256;
29
30/// Default protocol ID for unqualified send/request calls.
31pub const DEFAULT_DELIVERY_PROTOCOL_ID: &str = INBOX_PROTOCOL_ID;
32
33/// Shared interface for ma transport endpoints.
34///
35/// Each implementation provides inbox/outbox
36/// messaging and advertises its registered services for DID documents.
37#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
38#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
39pub trait MaEndpoint: Send + Sync {
40    /// The endpoint's public identifier (hex string).
41    fn id(&self) -> String;
42
43    /// Register a service protocol and return an [`Inbox`] for receiving messages.
44    ///
45    /// Implementations should ensure the service is reachable for inbound delivery
46    /// once it has been registered, so callers do not need a second explicit
47    /// "listen" step in the common case.
48    fn service(&mut self, protocol: &str) -> Inbox<Message>;
49
50    /// Return service strings for all registered protocols.
51    ///
52    /// Each entry is suitable for inclusion in a DID document's `ma.services` array.
53    fn services(&self) -> Vec<String>;
54
55    /// Return service strings as a JSON array value.
56    fn services_json(&self) -> serde_json::Value {
57        serde_json::Value::Array(
58            self.services()
59                .into_iter()
60                .map(serde_json::Value::String)
61                .collect(),
62        )
63    }
64
65    /// Build a [`crate::MaExtension`] pre-populated with this endpoint's
66    /// service strings.
67    ///
68    /// Use this as the starting point when constructing the `ma:` field for a
69    /// DID document. Chain additional builder methods on the returned value
70    /// before passing it to [`crate::config::SecretBundle::build_document`] or
71    /// [`crate::Document::set_ma_extension`]:
72    ///
73    /// ```ignore
74    /// let ma = endpoint.ma_extension().kind("world");
75    /// let document = bundle.build_document(ma)?;
76    /// ```
77    fn ma_extension(&self) -> crate::doc::MaExtension {
78        crate::doc::MaExtension::new().services(self.services())
79    }
80
81    /// Fire-and-forget to a target on a specific protocol.
82    async fn send_to(&self, target: &str, protocol: &str, message: &Message) -> Result<()>;
83
84    /// Gracefully shut down the endpoint, closing all cached connections.
85    async fn close(&mut self);
86
87    /// Open a transport-agnostic outbox to a remote DID and protocol.
88    ///
89    /// Resolves the DID document, checks `ma.services` for the requested
90    /// protocol, and delegates the actual transport connection to
91    /// [`Self::connect_outbox`]. Override this only for non-standard resolution.
92    #[cfg(feature = "iroh")]
93    async fn outbox(
94        &self,
95        resolver: &dyn DidDocumentResolver,
96        did: &str,
97        protocol: &str,
98    ) -> Result<Outbox> {
99        let doc = resolver.resolve(did).await?;
100
101        let services = doc
102            .ma
103            .as_ref()
104            .and_then(|ma| ma.get("services").ok().flatten())
105            .and_then(|services| serde_json::to_value(services).ok());
106
107        let endpoint_id =
108            resolve_endpoint_for_protocol(services.as_ref(), protocol).ok_or_else(|| {
109                Error::NoInboxTransport(format!("{did} has no service for {protocol}"))
110            })?;
111
112        self.connect_outbox(&doc, &endpoint_id, did, protocol).await
113    }
114
115    /// Open a transport-level outbox given a pre-resolved document and endpoint ID.
116    ///
117    /// Implementors use `doc` for transport-specific routing hints (e.g. relay URLs)
118    /// and `endpoint_id` as the peer address on their transport layer.
119    #[cfg(feature = "iroh")]
120    async fn connect_outbox(
121        &self,
122        doc: &Document,
123        endpoint_id: &str,
124        did: &str,
125        protocol: &str,
126    ) -> Result<Outbox>;
127
128    /// Subscribe to a gossip topic and return a sender/receiver pair.
129    ///
130    /// Requires that `enable_gossip` was called before the first `service`
131    /// call so the gossip protocol handler is registered in the router.
132    /// Returns `Err` if gossip was not enabled.
133    #[cfg(feature = "gossip")]
134    async fn gossip_subscribe(
135        &self,
136        _topic_id: [u8; 32],
137        _peers: Vec<EndpointId>,
138    ) -> Result<(GossipSender, GossipReceiver)> {
139        Err(crate::error::Error::Transport(
140            "gossip not enabled — call enable_gossip() before service()".to_string(),
141        ))
142    }
143
144    /// Opt the endpoint into gossip support.
145    ///
146    /// **Must be called before the first `service` call** so that the
147    /// gossip ALPN handler is included when the router is built.
148    /// Calling after the router has started has no effect on inbound
149    /// gossip connections.  Safe to call multiple times (no-op after first).
150    #[cfg(feature = "gossip")]
151    fn enable_gossip(&mut self) {}
152
153    /// Fire-and-forget to a target on the default inbox protocol.
154    async fn send(&self, target: &str, message: &Message) -> Result<()> {
155        self.send_to(target, DEFAULT_DELIVERY_PROTOCOL_ID, message)
156            .await
157    }
158}