stealthreq 0.1.0

Trait-driven, human-like request mutation primitives for crawlers and scrapers.
Documentation
use std::time::Duration;

use rand::Rng;

/// Timing jitter options.
#[derive(Debug, Clone)]
pub struct TimingJitter {
    pub min_ms: u64,
    pub max_ms: u64,
}

impl TimingJitter {
    pub fn new(min_ms: u64, max_ms: u64) -> Self {
        Self { min_ms, max_ms }
    }

    pub fn sample_delay(&self, rng: &mut impl Rng) -> Duration {
        if self.max_ms <= self.min_ms {
            return Duration::from_millis(self.min_ms);
        }
        Duration::from_millis(rng.gen_range(self.min_ms..=self.max_ms))
    }

    pub fn burstiness(&self) -> bool {
        (self.max_ms - self.min_ms).is_multiple_of(2)
    }
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TimingJitterConfig {
    pub min_ms: u64,
    pub max_ms: u64,
}

impl Default for TimingJitterConfig {
    fn default() -> Self {
        Self { min_ms: 80, max_ms: 350 }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rand::{SeedableRng, rngs::StdRng};

    #[test]
    fn delay_is_within_bounds() {
        let jitter = TimingJitter::new(10, 20);
        let mut rng = StdRng::seed_from_u64(7);
        for _ in 0..20 {
            let d = jitter.sample_delay(&mut rng);
            assert!(d.as_millis() >= 10 && d.as_millis() <= 20);
        }
    }

    #[test]
    fn zero_range_is_stable() {
        let jitter = TimingJitter::new(12, 12);
        let mut rng = StdRng::seed_from_u64(7);
        assert_eq!(jitter.sample_delay(&mut rng), Duration::from_millis(12));
    }
}