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 automatically
36    /// maintains an unintentionally lost selected session.
37    pub async fn connect(
38        self: &Arc<Self>,
39        endpoints: impl Into<EndpointSet>,
40    ) -> Result<PeerConnection, ConnectError> {
41        self.connect_using(endpoints.into(), None, None).await
42    }
43
44    #[cfg(feature = "hosting")]
45    pub(crate) async fn connect_expected(
46        self: &Arc<Self>,
47        endpoints: impl Into<EndpointSet>,
48        expected_peer: &str,
49    ) -> Result<PeerConnection, ConnectError> {
50        self.connect_using(endpoints.into(), None, Some(expected_peer))
51            .await
52    }
53
54    /// Connect using a platform-owned transport constructor.
55    ///
56    /// Endpoint ordering, establishment, identity verification, route sync,
57    /// logical-handle convergence, and reconnect coordination remain owned by
58    /// the server. Only construction of each candidate transport is delegated.
59    #[doc(hidden)]
60    pub async fn connect_with_dialer(
61        self: &Arc<Self>,
62        endpoints: impl Into<EndpointSet>,
63        dialer: Arc<dyn EndpointDialer>,
64    ) -> Result<PeerConnection, ConnectError> {
65        self.connect_using(endpoints.into(), Some(dialer), None)
66            .await
67    }
68
69    async fn connect_using(
70        self: &Arc<Self>,
71        set: EndpointSet,
72        dialer: Option<Arc<dyn EndpointDialer>>,
73        expected_peer: Option<&str>,
74    ) -> Result<PeerConnection, ConnectError> {
75        let key = set.cache_key();
76        let ordered = self
77            .dial_policy
78            .ordered_candidates(&key, &set)
79            .into_iter()
80            .filter(|endpoint| {
81                dialer
82                    .as_ref()
83                    .is_none_or(|dialer| dialer.supports(endpoint.kind))
84            })
85            .collect::<Vec<_>>();
86        if ordered.is_empty() {
87            return Err(ConnectError::NoSupportedEndpoint);
88        }
89        let mut last_error = ConnectError::NoSupportedEndpoint;
90        for endpoint in ordered {
91            match self
92                .try_candidate(&endpoint, expected_peer, dialer.as_ref())
93                .await
94            {
95                Ok((candidate, outcome)) => {
96                    let identity = match outcome {
97                        CandidateOutcome::Promoted(identity)
98                        | CandidateOutcome::Duplicate(identity) => identity,
99                    };
100                    let Some(link) = self.peer(&identity.node_id).await else {
101                        candidate.wire.shutdown();
102                        let _ = candidate.cleaned.await;
103                        last_error = ConnectError::Establishment {
104                            message: format!(
105                                "verified peer {:?} has no selected live session",
106                                identity.node_id
107                            ),
108                        };
109                        continue;
110                    };
111                    if n0_future::time::timeout(
112                        crate::session::ROUTE_SYNC_TIMEOUT,
113                        self.wait_for_selected_route(&identity.node_id, &link.session_id),
114                    )
115                    .await
116                    .is_err()
117                    {
118                        candidate.wire.shutdown();
119                        let _ = candidate.cleaned.await;
120                        last_error = ConnectError::Establishment {
121                            message: format!(
122                                "verified peer {:?} did not publish its synchronized node route",
123                                identity.node_id
124                            ),
125                        };
126                        continue;
127                    }
128                    let connection = {
129                        let mut connections = self
130                            .connections
131                            .write()
132                            .unwrap_or_else(|poisoned| poisoned.into_inner());
133                        if let Some(connection) = connections
134                            .get(&identity.node_id)
135                            .filter(|connection| !connection.is_terminal())
136                            .cloned()
137                        {
138                            if connection.bind(
139                                identity.clone(),
140                                link.session_id.clone(),
141                                link.wire.clone(),
142                            ) {
143                                connection.replace_endpoints(set.clone());
144                                connection.replace_dialer(dialer.clone());
145                                connection
146                            } else {
147                                let connection = PeerConnection::new(
148                                    Arc::downgrade(self),
149                                    identity.clone(),
150                                    set.clone(),
151                                    link.session_id.clone(),
152                                    link.wire.clone(),
153                                    dialer.clone(),
154                                );
155                                connections.insert(identity.node_id, connection.clone());
156                                connection
157                            }
158                        } else {
159                            let connection = PeerConnection::new(
160                                Arc::downgrade(self),
161                                identity.clone(),
162                                set.clone(),
163                                link.session_id.clone(),
164                                link.wire.clone(),
165                                dialer.clone(),
166                            );
167                            connections.insert(identity.node_id, connection.clone());
168                            connection
169                        }
170                    };
171                    self.dial_policy.record_winner(&key, endpoint.kind);
172                    return Ok(connection);
173                }
174                Err(error) => last_error = error,
175            }
176        }
177        Err(last_error)
178    }
179
180    async fn wait_for_selected_route(&self, peer: &str, session_id: &str) {
181        let mut changes = self.route_changes();
182        loop {
183            let selected_session = self.peer(peer).await.map(|selected| selected.session_id);
184            let direct_route = matches!(
185                self.snapshot.load().node_core.resolve(peer),
186                unb_core::Resolution::Route(next_hop) if next_hop == peer
187            );
188            if selected_session.as_deref() == Some(session_id) && direct_route {
189                return;
190            }
191            if changes.changed().await.is_err() {
192                return;
193            }
194        }
195    }
196
197    /// Wait until a directly linked peer is both selectable and routable.
198    ///
199    /// A link's transport handshake can finish before its first route snapshot
200    /// is applied. `link` promises immediate request readiness, so it must wait
201    /// for this local publication rather than merely for the handshake ACK.
202    async fn wait_for_direct_peer_route(&self, peer: &str) {
203        let mut changes = self.route_changes();
204        loop {
205            let selected = self.peer(peer).await.is_some();
206            let direct_route = matches!(
207                self.snapshot.load().node_core.resolve(peer),
208                unb_core::Resolution::Route(next_hop) if next_hop == peer
209            );
210            if selected && direct_route {
211                return;
212            }
213            if changes.changed().await.is_err() {
214                return;
215            }
216        }
217    }
218
219    async fn try_candidate(
220        self: &Arc<Self>,
221        endpoint: &Endpoint,
222        expected_peer: Option<&str>,
223        dialer: Option<&Arc<dyn EndpointDialer>>,
224    ) -> Result<(CandidateSession, CandidateOutcome), ConnectError> {
225        let deadline = self.dial_policy.attempt_timeout();
226        let candidate_dial = async {
227            match dialer {
228                Some(dialer) => dialer.dial(endpoint.clone()).await,
229                None => self.dial_policy.dial_candidate(endpoint).await,
230            }
231        };
232        let pipe = match n0_future::time::timeout(deadline, candidate_dial).await {
233            Ok(Ok(pipe)) => pipe,
234            Ok(Err(error)) => {
235                return Err(ConnectError::Dial {
236                    transport: endpoint.kind,
237                    message: error.to_string(),
238                })
239            }
240            Err(_) => {
241                return Err(ConnectError::DialTimedOut {
242                    transport: endpoint.kind,
243                })
244            }
245        };
246        let candidate = self.establish(pipe, expected_peer.map(str::to_owned)).await;
247        let outcome = candidate.observed_outcome().await;
248        match outcome {
249            Ok(outcome) => Ok((candidate, outcome)),
250            Err(failure) => {
251                candidate.wire.shutdown();
252                let _ = candidate.cleaned.await;
253                Err(match (expected_peer, failure) {
254                    (
255                        Some(expected),
256                        CandidateFailure::Retired {
257                            reason: unb_core::RetirementReason::UnexpectedPeer,
258                            identity,
259                        },
260                    ) => ConnectError::IdentityMismatch {
261                        expected: expected.to_string(),
262                        actual: identity.map(|identity| identity.node_id),
263                    },
264                    (_, CandidateFailure::Session(error)) => ConnectError::Establishment {
265                        message: error.to_string(),
266                    },
267                    (_, CandidateFailure::Retired { reason, .. }) => ConnectError::Establishment {
268                        message: format!("session retired during establishment: {reason:?}"),
269                    },
270                    (_, CandidateFailure::MissingIdentity) => ConnectError::Establishment {
271                        message: "session completed without an admitted identity".into(),
272                    },
273                })
274            }
275        }
276    }
277
278    pub(crate) async fn reconnect_peer(
279        self: &Arc<Self>,
280        peer: &str,
281        set: &EndpointSet,
282        dialer: Option<Arc<dyn EndpointDialer>>,
283    ) -> Result<ReconnectCandidate, ConnectError> {
284        let key = set.cache_key();
285        let ordered = self
286            .dial_policy
287            .ordered_candidates(&key, set)
288            .into_iter()
289            .filter(|endpoint| {
290                dialer
291                    .as_ref()
292                    .is_none_or(|dialer| dialer.supports(endpoint.kind))
293            })
294            .collect::<Vec<_>>();
295        if ordered.is_empty() {
296            return Err(ConnectError::NoSupportedEndpoint);
297        }
298        let mut last_error = ConnectError::NoSupportedEndpoint;
299        for endpoint in ordered {
300            match self
301                .try_candidate(&endpoint, Some(peer), dialer.as_ref())
302                .await
303            {
304                Ok((candidate, outcome)) => {
305                    let identity = match outcome {
306                        CandidateOutcome::Promoted(identity)
307                        | CandidateOutcome::Duplicate(identity) => identity,
308                    };
309                    let Some(selected) = self.peer(&identity.node_id).await else {
310                        candidate.wire.shutdown();
311                        let _ = candidate.cleaned.await;
312                        last_error = ConnectError::Establishment {
313                            message: format!(
314                                "verified peer {:?} has no selected live session",
315                                identity.node_id
316                            ),
317                        };
318                        continue;
319                    };
320                    if n0_future::time::timeout(
321                        crate::session::ROUTE_SYNC_TIMEOUT,
322                        self.wait_for_selected_route(&identity.node_id, &selected.session_id),
323                    )
324                    .await
325                    .is_err()
326                    {
327                        candidate.wire.shutdown();
328                        let _ = candidate.cleaned.await;
329                        last_error = ConnectError::Establishment {
330                            message: format!(
331                                "verified peer {:?} did not publish its synchronized node route",
332                                identity.node_id
333                            ),
334                        };
335                        continue;
336                    }
337                    let Some(selected) = self.peer(&identity.node_id).await else {
338                        candidate.wire.shutdown();
339                        let _ = candidate.cleaned.await;
340                        last_error = ConnectError::Establishment {
341                            message: format!(
342                                "verified peer {:?} lost its selected session after route synchronization",
343                                identity.node_id
344                            ),
345                        };
346                        continue;
347                    };
348                    self.dial_policy.record_winner(&key, endpoint.kind);
349                    return Ok(ReconnectCandidate {
350                        identity,
351                        selected,
352                        candidate_wire: candidate.wire,
353                    });
354                }
355                Err(error) => last_error = error,
356            }
357        }
358        Err(last_error)
359    }
360
361    pub async fn link(self: &Arc<Self>, other: &Arc<Node>) -> Result<(), WsError> {
362        if Arc::ptr_eq(self, other) || self.identity.node_id == other.identity.node_id {
363            return Err(WsError::Connect("a node cannot link to itself".into()));
364        }
365        let (dial_side, accept_side) = unb_client::pair();
366        let left = self
367            .establish(dial_side, Some(other.identity.node_id.clone()))
368            .await;
369        let right = other
370            .establish(accept_side, Some(self.identity.node_id.clone()))
371            .await;
372        let result = match tokio::join!(
373            left.outcome(&other.identity.node_id),
374            right.outcome(&self.identity.node_id)
375        ) {
376            (Ok(CandidateOutcome::Promoted(_)), Ok(CandidateOutcome::Promoted(_))) => {
377                n0_future::time::timeout(crate::session::ROUTE_SYNC_TIMEOUT, async {
378                    left.wire.routes_acked().await?;
379                    right.wire.routes_acked().await?;
380                    tokio::join!(
381                        self.wait_for_direct_peer_route(&other.identity.node_id),
382                        other.wait_for_direct_peer_route(&self.identity.node_id),
383                    );
384                    Ok::<(), WsError>(())
385                })
386                .await
387                .map_err(|_| {
388                    WsError::Connect(
389                        "linked peers did not publish their synchronized node routes".into(),
390                    )
391                })?
392            }
393            (Err(error), _) | (_, Err(error)) => Err(error),
394            _ => Err(WsError::Connect("link closed during establishment".into())),
395        };
396        if result.is_err() {
397            left.wire.shutdown();
398            right.wire.shutdown();
399            let _ = tokio::join!(left.cleaned, right.cleaned);
400        }
401        result
402    }
403
404    /// Attach one caller-provided outbound transport without imposing an expected peer name.
405    /// Browser bindings use this after JavaScript completes the platform dial.
406    pub async fn connect_transport_unchecked(
407        self: &Arc<Self>,
408        transport: unb_runtime::Pipe,
409    ) -> Result<(), WsError> {
410        let candidate = self.establish(transport, None).await;
411        match candidate.outcome("candidate").await {
412            Ok(CandidateOutcome::Promoted(_) | CandidateOutcome::Duplicate(_)) => {
413                let synchronized = n0_future::time::timeout(
414                    crate::session::ROUTE_SYNC_TIMEOUT,
415                    candidate.wire.routes_acked(),
416                )
417                .await
418                .map_err(|_| {
419                    WsError::Connect(
420                        "connected peer did not acknowledge its synchronized routes".into(),
421                    )
422                })
423                .and_then(|result| result);
424                if let Err(error) = synchronized {
425                    candidate.wire.shutdown();
426                    let _ = candidate.cleaned.await;
427                    return Err(error);
428                }
429                Ok(())
430            }
431            Err(error) => {
432                candidate.wire.shutdown();
433                let _ = candidate.cleaned.await;
434                Err(error)
435            }
436        }
437    }
438}