ugi 0.2.1

Runtime-agnostic Rust request client with HTTP/1.1, HTTP/2, HTTP/3, H2C, WebSocket, SSE, and gRPC support
Documentation
use std::sync::Arc;
use std::time::{Duration, Instant};

/// Whether a progress event relates to uploading or downloading.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ProgressPhase {
    /// Upload phase (writing the request body).
    Upload,
    /// Download phase (reading the response body).
    Download,
}

/// A snapshot of transfer progress at a point in time.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Progress {
    phase: ProgressPhase,
    transferred: usize,
    total: Option<usize>,
    done: bool,
}

impl Progress {
    /// Create a new progress snapshot.
    pub fn new(phase: ProgressPhase, transferred: usize, total: Option<usize>, done: bool) -> Self {
        Self {
            phase,
            transferred,
            total,
            done,
        }
    }

    /// The transfer phase (upload or download).
    pub fn phase(&self) -> ProgressPhase {
        self.phase
    }

    /// Total bytes transferred so far.
    pub fn transferred(&self) -> usize {
        self.transferred
    }

    /// Known total size in bytes, if provided by `Content-Length`.
    pub fn total(&self) -> Option<usize> {
        self.total
    }

    /// Returns `true` if the transfer is complete.
    pub fn is_done(&self) -> bool {
        self.done
    }
}

/// Controls how frequently progress callbacks are fired.
///
/// Events are emitted only when *at least* `min_bytes` have been transferred
/// since the last event **or** `min_interval` has elapsed, whichever comes
/// first.  This prevents flooding the caller with high-frequency callbacks on
/// fast connections.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProgressConfig {
    min_interval: Duration,
    min_bytes: usize,
}

impl Default for ProgressConfig {
    fn default() -> Self {
        Self {
            min_interval: DEFAULT_PROGRESS_INTERVAL,
            min_bytes: DEFAULT_PROGRESS_BYTES,
        }
    }
}

impl ProgressConfig {
    /// Create a custom progress config.
    pub fn new(min_interval: Duration, min_bytes: usize) -> Self {
        Self {
            min_interval,
            min_bytes,
        }
    }

    /// Minimum time between consecutive progress events.
    pub fn min_interval(&self) -> Duration {
        self.min_interval
    }

    /// Minimum bytes transferred between consecutive progress events.
    pub fn min_bytes(&self) -> usize {
        self.min_bytes
    }
}

pub(crate) const DEFAULT_PROGRESS_INTERVAL: Duration = Duration::from_millis(100);
pub(crate) const DEFAULT_PROGRESS_BYTES: usize = 64 * 1024;

pub(crate) struct ProgressReporter {
    callback: Arc<dyn Fn(Progress) + Send + Sync + 'static>,
    config: ProgressConfig,
    phase: ProgressPhase,
    total: Option<usize>,
    transferred: usize,
    last_emitted_at: Option<Instant>,
    last_emitted_bytes: usize,
}

impl ProgressReporter {
    pub(crate) fn new(
        callback: Arc<dyn Fn(Progress) + Send + Sync + 'static>,
        phase: ProgressPhase,
        total: Option<usize>,
        config: ProgressConfig,
    ) -> Self {
        Self {
            callback,
            config,
            phase,
            total,
            transferred: 0,
            last_emitted_at: None,
            last_emitted_bytes: 0,
        }
    }

    pub(crate) fn record(&mut self, delta: usize) {
        if delta == 0 {
            return;
        }
        self.transferred += delta;
        if self.should_emit() {
            self.emit(false);
        }
    }

    pub(crate) fn finish(&mut self) {
        self.emit(true);
    }

    fn should_emit(&self) -> bool {
        if self.transferred.saturating_sub(self.last_emitted_bytes) >= self.config.min_bytes {
            return true;
        }
        match self.last_emitted_at {
            Some(last) => last.elapsed() >= self.config.min_interval,
            None => true,
        }
    }

    fn emit(&mut self, done: bool) {
        self.last_emitted_at = Some(Instant::now());
        self.last_emitted_bytes = self.transferred;
        (self.callback)(Progress::new(
            self.phase,
            self.transferred,
            self.total,
            done,
        ));
    }
}