retri 0.1.1

Retri provides simple and highly compatible exponential retries.
Documentation
use std::{future::Future, time::Duration};

#[cfg(target_arch = "wasm32")]
use zduny_wasm_timer::Delay;

#[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep;

#[cfg(target_arch = "wasm32")]
async fn delay(duration: Duration) -> Result<(), std::io::Error> {
    Delay::new(duration).await
}

#[cfg(not(target_arch = "wasm32"))]
async fn delay(duration: Duration) -> Result<(), std::io::Error> {
    sleep(duration).await;
    Ok(())
}

#[derive(Clone, Copy)]
pub struct BackoffSettings {
    pub initial_backoff: Duration,
    pub total_retries: usize,
}

impl Default for BackoffSettings {
    fn default() -> Self {
        Self {
            initial_backoff: Duration::from_millis(50),
            total_retries: 5,
        }
    }
}

pub async fn retry<F, Fut, T, E>(op: F, settings: BackoffSettings) -> Result<T, E>
where
    F: Fn() -> Fut,
    Fut: Future<Output = Result<T, E>>,
    E: From<std::io::Error>,
{
    let mut counter = 0;
    let mut backoff = settings.initial_backoff;

    loop {
        let res = op().await;
        if res.is_ok() || counter >= settings.total_retries {
            break res;
        }

        delay(backoff).await?;
        counter += 1;
        backoff *= 2;
    }
}

#[cfg(test)]
mod tests {
    use crate::{retry, BackoffSettings};

    #[tokio::test]
    async fn test_retry() {
        retry(
            || async { Ok::<_, eyre::Report>(5) },
            BackoffSettings::default(),
        )
        .await
        .unwrap();
    }

    #[tokio::test]
    async fn test_retry_fails() {
        let res = retry(
            || async { Err::<usize, _>(eyre::eyre!("error")) },
            BackoffSettings::default(),
        )
        .await;

        assert!(res.is_err());
    }
}