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}