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}