Skip to main content

unb_server/
connect.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use unb_client::{Endpoint, EndpointSet};
6use unb_runtime::WsError;
7
8use crate::connection::{ConnectError, PeerConnection};
9use crate::node::{Node, PeerLink};
10use crate::session::{CandidateFailure, CandidateOutcome, CandidateSession};
11
12/// Supplies a transport for one already-ordered endpoint candidate.
13///
14/// This specialist hook lets platform bindings retain the server's connection
15/// lifecycle while constructing transports in their native runtime.
16#[doc(hidden)]
17pub trait EndpointDialer: Send + Sync {
18    fn supports(&self, kind: unb_client::TransportKind) -> bool;
19
20    fn dial(
21        &self,
22        endpoint: Endpoint,
23    ) -> Pin<Box<dyn Future<Output = Result<unb_runtime::Pipe, WsError>> + Send + 'static>>;
24}
25
26pub(crate) struct ReconnectCandidate {
27    pub(crate) identity: unb_core::NodeIdentity,
28    pub(crate) selected: PeerLink,
29    pub(crate) candidate_wire: Arc<unb_runtime::Wire>,
30}
31
32impl Node {
33    /// Eagerly dial, verify the peer, and synchronize its routes.
34    ///
35    /// The returned logical connection is retained by the node and remains
36    /// observable after transport loss. No automatic retry is started; call
37    /// [`PeerConnection::reconnect`] from a caller-owned policy when desired.
38    pub async fn connect(
39        self: &Arc<Self>,
40        endpoints: impl Into<EndpointSet>,
41    ) -> Result<PeerConnection, ConnectError> {
42        self.connect_using(endpoints.into(), None).await
43    }
44
45    /// Connect using a platform-owned transport constructor.
46    ///
47    /// Endpoint ordering, establishment, identity verification, route sync,
48    /// logical-handle convergence, and reconnect coordination remain owned by
49    /// the server. Only construction of each candidate transport is delegated.
50    #[doc(hidden)]
51    pub async fn connect_with_dialer(
52        self: &Arc<Self>,
53        endpoints: impl Into<EndpointSet>,
54        dialer: Arc<dyn EndpointDialer>,
55    ) -> Result<PeerConnection, ConnectError> {
56        self.connect_using(endpoints.into(), Some(dialer)).await
57    }
58
59    async fn connect_using(
60        self: &Arc<Self>,
61        set: EndpointSet,
62        dialer: Option<Arc<dyn EndpointDialer>>,
63    ) -> Result<PeerConnection, ConnectError> {
64        let key = set.cache_key();
65        let ordered = self
66            .dial_policy
67            .ordered_candidates(&key, &set)
68            .into_iter()
69            .filter(|endpoint| {
70                dialer
71                    .as_ref()
72                    .is_none_or(|dialer| dialer.supports(endpoint.kind))
73            })
74            .collect::<Vec<_>>();
75        if ordered.is_empty() {
76            return Err(ConnectError::NoSupportedEndpoint);
77        }
78        let mut last_error = ConnectError::NoSupportedEndpoint;
79        for endpoint in ordered {
80            match self.try_candidate(&endpoint, None, dialer.as_ref()).await {
81                Ok((candidate, outcome)) => {
82                    let identity = match outcome {
83                        CandidateOutcome::Promoted(identity)
84                        | CandidateOutcome::Duplicate(identity) => identity,
85                    };
86                    let Some(link) = self.peer(&identity.node_id).await else {
87                        candidate.wire.shutdown();
88                        let _ = candidate.cleaned.await;
89                        last_error = ConnectError::Establishment {
90                            message: format!(
91                                "verified peer {:?} has no selected live session",
92                                identity.node_id
93                            ),
94                        };
95                        continue;
96                    };
97                    let connection = {
98                        let mut connections = self
99                            .connections
100                            .write()
101                            .unwrap_or_else(|poisoned| poisoned.into_inner());
102                        if let Some(connection) = connections.get(&identity.node_id).cloned() {
103                            connection.bind(identity, link.session_id.clone(), link.wire.clone());
104                            connection.replace_endpoints(set.clone());
105                            connection.replace_dialer(dialer.clone());
106                            connection
107                        } else {
108                            let connection = PeerConnection::new(
109                                Arc::downgrade(self),
110                                identity.clone(),
111                                set.clone(),
112                                link.session_id.clone(),
113                                link.wire.clone(),
114                                dialer.clone(),
115                            );
116                            connections.insert(identity.node_id, connection.clone());
117                            connection
118                        }
119                    };
120                    self.dial_policy.record_winner(&key, endpoint.kind);
121                    return Ok(connection);
122                }
123                Err(error) => last_error = error,
124            }
125        }
126        Err(last_error)
127    }
128
129    async fn try_candidate(
130        self: &Arc<Self>,
131        endpoint: &Endpoint,
132        expected_peer: Option<&str>,
133        dialer: Option<&Arc<dyn EndpointDialer>>,
134    ) -> Result<(CandidateSession, CandidateOutcome), ConnectError> {
135        let deadline = self.dial_policy.attempt_timeout();
136        let candidate_dial = async {
137            match dialer {
138                Some(dialer) => dialer.dial(endpoint.clone()).await,
139                None => self.dial_policy.dial_candidate(endpoint).await,
140            }
141        };
142        let pipe = match n0_future::time::timeout(deadline, candidate_dial).await {
143            Ok(Ok(pipe)) => pipe,
144            Ok(Err(error)) => {
145                return Err(ConnectError::Dial {
146                    transport: endpoint.kind,
147                    message: error.to_string(),
148                })
149            }
150            Err(_) => {
151                return Err(ConnectError::DialTimedOut {
152                    transport: endpoint.kind,
153                })
154            }
155        };
156        let candidate = self.establish(pipe, expected_peer.map(str::to_owned)).await;
157        let outcome = candidate.observed_outcome().await;
158        match outcome {
159            Ok(outcome) => Ok((candidate, outcome)),
160            Err(failure) => {
161                candidate.wire.shutdown();
162                let _ = candidate.cleaned.await;
163                Err(match (expected_peer, failure) {
164                    (
165                        Some(expected),
166                        CandidateFailure::Retired {
167                            reason: unb_core::RetirementReason::UnexpectedPeer,
168                            identity,
169                        },
170                    ) => ConnectError::IdentityMismatch {
171                        expected: expected.to_string(),
172                        actual: identity.map(|identity| identity.node_id),
173                    },
174                    (_, CandidateFailure::Session(error)) => ConnectError::Establishment {
175                        message: error.to_string(),
176                    },
177                    (_, CandidateFailure::Retired { reason, .. }) => ConnectError::Establishment {
178                        message: format!("session retired during establishment: {reason:?}"),
179                    },
180                    (_, CandidateFailure::MissingIdentity) => ConnectError::Establishment {
181                        message: "session completed without an admitted identity".into(),
182                    },
183                })
184            }
185        }
186    }
187
188    pub(crate) async fn reconnect_peer(
189        self: &Arc<Self>,
190        peer: &str,
191        set: &EndpointSet,
192        dialer: Option<Arc<dyn EndpointDialer>>,
193    ) -> Result<ReconnectCandidate, ConnectError> {
194        let key = set.cache_key();
195        let ordered = self
196            .dial_policy
197            .ordered_candidates(&key, set)
198            .into_iter()
199            .filter(|endpoint| {
200                dialer
201                    .as_ref()
202                    .is_none_or(|dialer| dialer.supports(endpoint.kind))
203            })
204            .collect::<Vec<_>>();
205        if ordered.is_empty() {
206            return Err(ConnectError::NoSupportedEndpoint);
207        }
208        let mut last_error = ConnectError::NoSupportedEndpoint;
209        for endpoint in ordered {
210            match self
211                .try_candidate(&endpoint, Some(peer), dialer.as_ref())
212                .await
213            {
214                Ok((candidate, outcome)) => {
215                    let identity = match outcome {
216                        CandidateOutcome::Promoted(identity)
217                        | CandidateOutcome::Duplicate(identity) => identity,
218                    };
219                    let Some(selected) = self.peer(&identity.node_id).await else {
220                        candidate.wire.shutdown();
221                        let _ = candidate.cleaned.await;
222                        last_error = ConnectError::Establishment {
223                            message: format!(
224                                "verified peer {:?} has no selected live session",
225                                identity.node_id
226                            ),
227                        };
228                        continue;
229                    };
230                    self.dial_policy.record_winner(&key, endpoint.kind);
231                    return Ok(ReconnectCandidate {
232                        identity,
233                        selected,
234                        candidate_wire: candidate.wire,
235                    });
236                }
237                Err(error) => last_error = error,
238            }
239        }
240        Err(last_error)
241    }
242
243    pub async fn link(self: &Arc<Self>, other: &Arc<Node>) -> Result<(), WsError> {
244        if Arc::ptr_eq(self, other) || self.identity.node_id == other.identity.node_id {
245            return Err(WsError::Connect("a node cannot link to itself".into()));
246        }
247        let (dial_side, accept_side) = unb_client::pair();
248        let left = self
249            .establish(dial_side, Some(other.identity.node_id.clone()))
250            .await;
251        let right = other
252            .establish(accept_side, Some(self.identity.node_id.clone()))
253            .await;
254        let result = match tokio::join!(
255            left.outcome(&other.identity.node_id),
256            right.outcome(&self.identity.node_id)
257        ) {
258            (Ok(CandidateOutcome::Promoted(_)), Ok(CandidateOutcome::Promoted(_))) => {
259                let _ = n0_future::time::timeout(crate::session::ROUTE_SYNC_TIMEOUT, async {
260                    tokio::join!(left.wire.routes_acked(), right.wire.routes_acked())
261                })
262                .await;
263                Ok(())
264            }
265            (Err(error), _) | (_, Err(error)) => Err(error),
266            _ => Err(WsError::Connect("link closed during establishment".into())),
267        };
268        if result.is_err() {
269            left.wire.shutdown();
270            right.wire.shutdown();
271            let _ = tokio::join!(left.cleaned, right.cleaned);
272        }
273        result
274    }
275
276    /// Attach one caller-provided outbound transport without imposing an expected peer name.
277    /// Browser bindings use this after JavaScript completes the platform dial.
278    pub async fn connect_transport_unchecked(
279        self: &Arc<Self>,
280        transport: unb_runtime::Pipe,
281    ) -> Result<(), WsError> {
282        let candidate = self.establish(transport, None).await;
283        match candidate.outcome("candidate").await {
284            Ok(CandidateOutcome::Promoted(_) | CandidateOutcome::Duplicate(_)) => {
285                let _ = n0_future::time::timeout(
286                    crate::session::ROUTE_SYNC_TIMEOUT,
287                    candidate.wire.routes_acked(),
288                )
289                .await;
290                Ok(())
291            }
292            Err(error) => {
293                candidate.wire.shutdown();
294                let _ = candidate.cleaned.await;
295                Err(error)
296            }
297        }
298    }
299}