rusty-pv 0.1.0

Pipe viewer — a Rust port of Andrew Wood's `pv(1)` with progress bar, ETA, rate display, token-bucket rate limiting, IEC/SI unit math, SIGWINCH-aware terminal redraw, SIGUSR1 size refresh, multi-instance cursor coordination, and a typed library API.
Documentation
//! Token-bucket throttle for `-L RATE` (FR-022, AD-005, HINT-001).
//!
//! Per-iteration check: `expected_bytes = (now - start) · rate`. If
//! `bytes_done > expected_bytes`, sleep `(bytes_done - expected_bytes) / rate`
//! before the next read. 10 ms minimum sleep floor — finer granularity is
//! unreliable on Windows.
//!
//! All timing uses `std::time::Instant` (monotonic) — never `SystemTime` —
//! per AD-005, immune to wall-clock jumps.

use std::time::{Duration, Instant};

/// Token-bucket rate-limit throttle.
#[derive(Debug, Clone)]
pub struct TokenBucket {
    rate_bytes_per_sec: u64,
    start: Instant,
}

impl TokenBucket {
    /// Construct a new throttle at `rate_bytes_per_sec`. The throttle's clock
    /// starts at construction time.
    #[must_use]
    pub fn new(rate_bytes_per_sec: u64) -> Self {
        TokenBucket {
            rate_bytes_per_sec,
            start: Instant::now(),
        }
    }

    /// Inspect whether the loop is currently over-budget and, if so, return
    /// the recommended sleep duration to converge to the rate. Returns
    /// `Duration::ZERO` when the throttle is happy with current progress.
    #[must_use]
    pub fn next_sleep(&self, bytes_done: u64) -> Duration {
        let elapsed = self.start.elapsed().as_secs_f64();
        let expected = elapsed * self.rate_bytes_per_sec as f64;
        let over = bytes_done as f64 - expected;
        if over <= 0.0 {
            return Duration::ZERO;
        }
        let want = over / self.rate_bytes_per_sec as f64;
        // 10ms minimum so the sleep is meaningful on Windows.
        Duration::from_secs_f64(want.max(0.010))
    }

    /// Sleep for the recommended interval if any. Returns `true` if a sleep
    /// was performed, `false` if no sleep was needed.
    pub fn maybe_sleep(&self, bytes_done: u64) -> bool {
        let d = self.next_sleep(bytes_done);
        if d == Duration::ZERO {
            return false;
        }
        std::thread::sleep(d);
        true
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn zero_when_under_budget() {
        let tb = TokenBucket::new(1_000_000);
        // Newly constructed: elapsed ≈ 0, expected ≈ 0, bytes_done = 0 → not over.
        assert_eq!(tb.next_sleep(0), Duration::ZERO);
    }

    #[test]
    fn sleep_when_over_budget() {
        let tb = TokenBucket::new(1_000_000);
        // Pretend we've done 2 MB in zero elapsed time → over-budget.
        let d = tb.next_sleep(2_000_000);
        assert!(d >= Duration::from_millis(10));
        assert!(d >= Duration::from_millis(500));
    }
}