pb-mapper-client 0.8.1

Register, connect, status, and admin client implementation for pb-mapper
//! Small value types shared across the SDK surface, and their conversions from
//! the wire types in `pb-mapper-protocol`.
//!
//! Keeping owned mirrors here is what lets a caller depend on the SDK alone: the
//! protocol crate stays an implementation detail.

use pb_mapper_auth::LegacyProtocolPolicy;
use pb_mapper_protocol::command::{PbConnStatusResp, PbServiceConnStatus};

/// Transport used by a registered service or a local subscriber.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Transport {
    Tcp,
    Udp,
}

impl Transport {
    pub(crate) fn is_datagram(self) -> bool {
        matches!(self, Self::Udp)
    }
}

/// Observed lifecycle of a [`super::Registration`] or [`super::Connection`].
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TunnelStatus {
    Starting,
    Connected,
    Retrying,
    Stopped,
    Failed(String),
}

impl TunnelStatus {
    pub(crate) fn from_callback(status: &str) -> Self {
        match status {
            "connected" => return Self::Connected,
            "retrying" => return Self::Retrying,
            "failed" => return Self::Failed("failed".into()),
            _ => {}
        }
        // A permanent rejection arrives as `failed: <reason>`; the reason is the
        // only description of it the caller ever gets, so it has to survive here
        // rather than collapsing into `Retrying` and hanging `wait_ready`.
        match status.strip_prefix("failed:") {
            Some(reason) => Self::Failed(reason.trim().to_string()),
            None => Self::Retrying,
        }
    }
}

/// Relay mapping dump from `status remote-id`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RemoteId {
    pub server_map: String,
    pub active: String,
    pub idle: String,
}

impl RemoteId {
    pub(crate) fn from_status(status: PbConnStatusResp) -> super::Result<Self> {
        match status {
            PbConnStatusResp::RemoteId {
                server_map,
                active,
                idle,
            } => Ok(Self {
                server_map,
                active,
                idle,
            }),
            other => Err(super::Error::protocol(format!(
                "expected remote-id status, got {other:?}"
            ))),
        }
    }
}

/// One control connection the relay is holding for a service key.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ServiceConnection {
    pub conn_id: u32,
    pub generation: u64,
    pub protocol_version: u16,
    pub healthy: bool,
    pub last_rx_age_ms: u64,
}

impl From<PbServiceConnStatus> for ServiceConnection {
    fn from(value: PbServiceConnStatus) -> Self {
        Self {
            conn_id: value.conn_id,
            generation: value.generation,
            protocol_version: value.protocol_version,
            healthy: value.healthy,
            last_rx_age_ms: value.last_rx_age_ms,
        }
    }
}

/// Legacy framing policy, matching `pb-mapper admin legacy-protocol`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LegacyProtocol {
    Allow,
    Deny,
}

impl From<LegacyProtocol> for LegacyProtocolPolicy {
    fn from(value: LegacyProtocol) -> Self {
        match value {
            LegacyProtocol::Allow => Self::Allow,
            LegacyProtocol::Deny => Self::Deny,
        }
    }
}

impl From<LegacyProtocolPolicy> for LegacyProtocol {
    fn from(value: LegacyProtocolPolicy) -> Self {
        match value {
            LegacyProtocolPolicy::Allow => Self::Allow,
            LegacyProtocolPolicy::Deny => Self::Deny,
        }
    }
}

/// Per-connect path policy.
///
/// The default is [`Self::Auto`] when the SDK is built with the `p2p` feature,
/// and [`Self::RelayOnly`] when it is not, since only `Auto` needs it.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PathPolicy {
    /// Use the relay for every connection, with the pre-direct protocol.
    RelayOnly,
    /// Try a direct QUIC path first; fall back to the relay if it cannot be
    /// set up, before any application byte is sent.
    Auto,
}

impl Default for PathPolicy {
    fn default() -> Self {
        if cfg!(feature = "p2p") {
            Self::Auto
        } else {
            Self::RelayOnly
        }
    }
}

#[derive(Clone, Copy, Debug, Default)]
pub struct ConnectOptions {
    pub path: PathPolicy,
}

#[derive(Clone, Copy, Debug)]
pub struct RegisterOptions {
    /// Accept direct sessions from authorized consumers. Defaults to `true`
    /// with the `p2p` feature. Allowing it costs nothing until a consumer asks:
    /// the UDP socket is opened on the first offer.
    pub allow_direct: bool,
    pub idle_timeout: Option<std::time::Duration>,
}

impl Default for RegisterOptions {
    fn default() -> Self {
        Self {
            allow_direct: cfg!(feature = "p2p"),
            idle_timeout: None,
        }
    }
}