use url::Url;
use crate::{ConnectionId, DialerId, UnixTimestamp, network::BackoffConfig};
#[derive(Debug, Clone)]
pub(crate) struct DialerState {
#[expect(dead_code)]
pub(crate) dialer_id: DialerId,
pub(crate) url: Url,
pub(crate) backoff_config: BackoffConfig,
pub(crate) status: DialerStatus,
pub(crate) attempts: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum DialerStatus {
NeedTransport,
TransportPending,
Connected { connection_id: ConnectionId },
WaitingToRetry { retry_at: UnixTimestamp },
Failed,
}
impl DialerState {
pub(crate) fn new(dialer_id: DialerId, url: Url, backoff: BackoffConfig) -> Self {
Self {
dialer_id,
url,
backoff_config: backoff,
status: DialerStatus::NeedTransport,
attempts: 0,
}
}
pub(crate) fn handle_connection_lost<R: rand::Rng>(
&mut self,
rng: &mut R,
now: UnixTimestamp,
connection_id: ConnectionId,
) -> ConnectionLostOutcome {
if !matches!(self.status, DialerStatus::Connected { connection_id: cid } if cid == connection_id)
{
return ConnectionLostOutcome::NotOurs;
}
self.attempts += 1;
if let Some(max) = self.backoff_config.max_retries
&& self.attempts > max
{
self.status = DialerStatus::Failed;
return ConnectionLostOutcome::MaxRetriesReached;
}
let retry_at = compute_retry_time(rng, now, &self.backoff_config, self.attempts);
self.status = DialerStatus::WaitingToRetry { retry_at };
ConnectionLostOutcome::WillRetry { retry_at }
}
pub(crate) fn handle_dial_failed<R: rand::Rng>(
&mut self,
rng: &mut R,
now: UnixTimestamp,
) -> ConnectionLostOutcome {
if !matches!(self.status, DialerStatus::TransportPending) {
return ConnectionLostOutcome::NotOurs;
}
self.attempts += 1;
if let Some(max) = self.backoff_config.max_retries
&& self.attempts > max
{
self.status = DialerStatus::Failed;
return ConnectionLostOutcome::MaxRetriesReached;
}
let retry_at = compute_retry_time(rng, now, &self.backoff_config, self.attempts);
self.status = DialerStatus::WaitingToRetry { retry_at };
ConnectionLostOutcome::WillRetry { retry_at }
}
pub(crate) fn set_connected(&mut self, connection_id: ConnectionId) -> bool {
if !matches!(self.status, DialerStatus::TransportPending) {
return false;
}
self.status = DialerStatus::Connected { connection_id };
true
}
pub(crate) fn reset_backoff(&mut self) {
self.attempts = 0;
}
pub(crate) fn check_retry(&mut self, now: UnixTimestamp) -> bool {
if let DialerStatus::WaitingToRetry { retry_at } = self.status
&& now >= retry_at
{
self.status = DialerStatus::NeedTransport;
return true;
}
false
}
pub(crate) fn mark_transport_pending(&mut self) -> bool {
if matches!(self.status, DialerStatus::NeedTransport) {
self.status = DialerStatus::TransportPending;
true
} else {
false
}
}
pub(crate) fn active_connection(&self) -> Option<ConnectionId> {
if let DialerStatus::Connected { connection_id } = &self.status {
Some(*connection_id)
} else {
None
}
}
}
pub(crate) enum ConnectionLostOutcome {
NotOurs,
WillRetry { retry_at: UnixTimestamp },
MaxRetriesReached,
}
fn compute_retry_time<R: rand::Rng>(
rng: &mut R,
now: UnixTimestamp,
config: &BackoffConfig,
attempts: u32,
) -> UnixTimestamp {
use std::time::Duration;
let base_delay = config
.initial_delay
.saturating_mul(2u32.saturating_pow(attempts.saturating_sub(1)));
let capped_delay = std::cmp::min(base_delay, config.max_delay);
let jitter: f64 = rng.random_range(0.5..1.0);
let jittered_millis = (capped_delay.as_millis() as f64 * jitter) as u64;
let jittered_delay = Duration::from_millis(jittered_millis);
now + jittered_delay
}