use std::time::{Duration, SystemTime};
use serde::{Deserialize, Serialize};
use crate::vortix_core::profile::ProfileId;
pub const DEFAULT_RETRY_BUDGET_SECS: u64 = 300;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum FailureReason {
RetryBudgetExhausted { attempts: u32, elapsed: Duration },
HandshakeFailed(String),
AuthFailed(String),
ConfigInvalid(String),
Timeout(Duration),
NoNetworkLink,
ProfileGone(ProfileId),
Other(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DegradedReason {
HandshakeStale { seconds_since_last_handshake: u64 },
HighPacketLoss { loss_percent: f32 },
HighLatency { latency_ms: u64 },
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ConnectionHealth {
#[default]
Unknown,
Healthy,
Degraded {
reason: DegradedReason,
},
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DetailedConnectionInfo {
pub interface: String,
pub internal_ip: String,
pub endpoint: String,
pub mtu: String,
pub public_key: String,
pub listen_port: String,
pub transfer_rx: String,
pub transfer_tx: String,
pub latest_handshake: String,
pub pid: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum PromptKind {
TwoFactorCode,
Passphrase,
Generic { label: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Connection {
Disconnected { last_failure: Option<FailureReason> },
Connecting {
profile_id: ProfileId,
started_at: SystemTime,
attempt: u32,
retry_budget_remaining: Duration,
},
Connected {
profile_id: ProfileId,
since: SystemTime,
health: ConnectionHealth,
details: Box<DetailedConnectionInfo>,
},
Reconnecting {
profile_id: ProfileId,
started_at: SystemTime,
attempt: u32,
retry_budget_remaining: Duration,
last_error: Option<String>,
},
Disconnecting {
profile_id: ProfileId,
started_at: SystemTime,
},
AwaitingUserInput {
profile_id: ProfileId,
prompt_id: String,
prompt_kind: PromptKind,
since: SystemTime,
},
}
impl Default for Connection {
fn default() -> Self {
Self::Disconnected { last_failure: None }
}
}
impl Connection {
#[must_use]
pub fn profile_id(&self) -> Option<&ProfileId> {
match self {
Self::Disconnected { .. } => None,
Self::Connecting { profile_id, .. }
| Self::Connected { profile_id, .. }
| Self::Reconnecting { profile_id, .. }
| Self::Disconnecting { profile_id, .. }
| Self::AwaitingUserInput { profile_id, .. } => Some(profile_id),
}
}
#[must_use]
pub fn is_steady(&self) -> bool {
matches!(self, Self::Disconnected { .. } | Self::Connected { .. })
}
#[must_use]
pub fn is_connected(&self) -> bool {
matches!(self, Self::Connected { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_disconnected_with_no_failure() {
let s = Connection::default();
assert!(matches!(s, Connection::Disconnected { last_failure: None }));
}
#[test]
fn profile_id_is_none_for_disconnected() {
let s = Connection::default();
assert!(s.profile_id().is_none());
}
#[test]
fn profile_id_is_some_for_other_states() {
let p = ProfileId::new("corp");
let s = Connection::Connecting {
profile_id: p.clone(),
started_at: SystemTime::now(),
attempt: 1,
retry_budget_remaining: Duration::from_secs(300),
};
assert_eq!(s.profile_id(), Some(&p));
}
#[test]
fn is_steady_distinguishes_states() {
let p = ProfileId::new("corp");
assert!(Connection::default().is_steady());
assert!(!Connection::Connecting {
profile_id: p.clone(),
started_at: SystemTime::now(),
attempt: 1,
retry_budget_remaining: Duration::from_secs(300),
}
.is_steady());
}
#[test]
fn awaiting_user_input_carries_profile_id() {
let p = ProfileId::new("corp");
let s = Connection::AwaitingUserInput {
profile_id: p.clone(),
prompt_id: "2fa".into(),
prompt_kind: PromptKind::TwoFactorCode,
since: SystemTime::now(),
};
assert_eq!(s.profile_id(), Some(&p));
}
#[test]
fn awaiting_user_input_is_not_steady() {
let s = Connection::AwaitingUserInput {
profile_id: ProfileId::new("corp"),
prompt_id: "2fa".into(),
prompt_kind: PromptKind::TwoFactorCode,
since: SystemTime::now(),
};
assert!(!s.is_steady());
assert!(!s.is_connected());
}
#[test]
fn prompt_kind_roundtrips_through_json() {
let kinds = [
PromptKind::TwoFactorCode,
PromptKind::Passphrase,
PromptKind::Generic {
label: "Hardware token PIN".into(),
},
];
for k in kinds {
let json = serde_json::to_string(&k).unwrap();
let back: PromptKind = serde_json::from_str(&json).unwrap();
assert_eq!(k, back);
}
}
}