use std::fmt;
use std::net::SocketAddr;
use crate::connect_handle::ConnectHandle;
use crate::dialer;
use crate::peer;
use crate::ticket::Ticket;
#[derive(Debug)]
#[non_exhaustive]
pub enum ConnectError {
PeerUnreachable,
Bind(std::io::Error),
Endpoint(std::io::Error),
}
impl ConnectError {
pub const fn is_retryable(&self) -> bool {
match self {
Self::PeerUnreachable | Self::Endpoint(_) => true,
Self::Bind(_) => false,
}
}
}
impl fmt::Display for ConnectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PeerUnreachable => {
write!(f, "could not reach the serve side, directly or via a relay")
}
Self::Bind(_) => f.write_str("could not bind the requested local address"),
Self::Endpoint(_) => f.write_str("could not set up the p2p endpoint"),
}
}
}
impl std::error::Error for ConnectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::PeerUnreachable => None,
Self::Bind(e) | Self::Endpoint(e) => Some(e),
}
}
}
#[derive(Debug, Default)]
#[non_exhaustive]
pub struct ConnectOptions {
pub bind: Option<SocketAddr>,
}
pub async fn connect(ticket: &Ticket, opts: ConnectOptions) -> Result<ConnectHandle, ConnectError> {
let (state, listener) = dialer::dial(ticket, opts.bind).await?;
tokio::spawn(dialer::local_loop(state.clone(), listener));
let watching = state.clone();
tokio::spawn(async move { peer::keep_connected(&watching.peer, &watching.lifecycle).await });
Ok(ConnectHandle::new(state))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unreachable_peer_is_retryable() {
assert!(ConnectError::PeerUnreachable.is_retryable());
}
#[test]
fn failing_to_bind_the_p2p_endpoint_is_retryable() {
let e = ConnectError::Endpoint(std::io::Error::other("too many open files"));
assert!(e.is_retryable(), "{e} should be retryable");
}
#[test]
fn a_connect_bind_failure_is_not_retryable_because_the_caller_chose_the_address() {
let e = ConnectError::Bind(std::io::Error::other("address in use"));
assert!(!e.is_retryable(), "{e} should not be retryable");
}
}