use std::fmt;
use std::net::SocketAddr;
use crate::connect_handle::ConnectHandle;
use crate::dialer;
use crate::peer;
use crate::ticket::Ticket;
use crate::transport;
#[derive(Debug)]
#[non_exhaustive]
pub enum ConnectError {
PeerUnreachable,
Bind(std::io::Error),
Endpoint(std::io::Error),
InvalidRelay {
url: String,
},
}
impl ConnectError {
pub const fn is_retryable(&self) -> bool {
match self {
Self::PeerUnreachable | Self::Endpoint(_) => true,
Self::Bind(_) | Self::InvalidRelay { .. } => 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"),
Self::InvalidRelay { url } => {
write!(
f,
"{url} does not parse as a relay URL — check the value passed as the relay"
)
}
}
}
}
impl std::error::Error for ConnectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::PeerUnreachable | Self::InvalidRelay { .. } => None,
Self::Bind(e) | Self::Endpoint(e) => Some(e),
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct ConnectOptions {
pub bind: Option<SocketAddr>,
pub relay: Option<String>,
pub port_mapping: bool,
pub discovery: bool,
}
impl Default for ConnectOptions {
fn default() -> Self {
Self {
bind: None,
relay: None,
port_mapping: true,
discovery: true,
}
}
}
pub async fn connect(ticket: &Ticket, opts: ConnectOptions) -> Result<ConnectHandle, ConnectError> {
if let Some(relay) = opts.relay.as_deref() {
transport::validate_relay_for_connect(relay)?;
}
let (state, listener) = dialer::dial(ticket, &opts).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");
}
#[test]
fn an_unparseable_relay_is_permanent_and_names_the_value() {
let e = ConnectError::InvalidRelay {
url: "not a url".to_owned(),
};
assert!(!e.is_retryable());
assert!(e.to_string().contains("not a url"));
assert!(std::error::Error::source(&e).is_none());
}
#[test]
fn the_default_options_keep_every_network_contact_on() {
let opts = ConnectOptions::default();
assert!(opts.port_mapping);
assert!(opts.discovery);
assert!(opts.relay.is_none());
}
}