hivecomb 0.1.2

A Rust library for the Hive blockchain: keys, Graphene serialization, transaction signing and RPC.
Documentation
//! A working [`AsyncTransport`](super::AsyncTransport) on `reqwest`, behind the
//! `reqwest-transport` feature.
//!
//! This is the batteries-included option. The async layer itself pulls in no executor;
//! this module is where tokio and reqwest enter, and only if you ask for them.

use super::async_client::AsyncTransport;
use crate::error::{Error, Result};
use std::future::Future;
use std::time::Duration;

/// An async HTTP transport.
///
/// Holds one `reqwest::Client`, so connections are pooled across calls and across
/// nodes — which matters when racing several at once.
#[derive(Debug, Clone)]
pub struct ReqwestTransport {
    client: reqwest::Client,
}

impl ReqwestTransport {
    /// Build a transport with a pooled client.
    pub fn new() -> Result<Self> {
        let client = reqwest::Client::builder()
            .user_agent(concat!("hivecomb/", env!("CARGO_PKG_VERSION")))
            .build()
            .map_err(|e| Error::Rpc(format!("could not build HTTP client: {e}")))?;
        Ok(ReqwestTransport { client })
    }

    /// Wrap an existing client, so an application's own configuration — proxies,
    /// certificates, connection limits — carries over.
    ///
    /// # Two timeouts, and the shorter one wins
    ///
    /// A timeout configured on `client` stays in force, and this transport *also*
    /// applies the per-request timeout that [`NodeClient`](super::NodeClient) passes
    /// down. reqwest enforces both, so the effective limit is whichever is shorter.
    ///
    /// That is the safe direction and it is deliberate, but it is worth knowing: setting
    /// a long timeout here does not widen `with_timeout`, and setting a short one here
    /// narrows it without the client being aware. A peer found the opposite arrangement
    /// in another project — one component deciding a node was unacceptable at eight
    /// seconds while the component actually talking to it waited sixty — where only the
    /// lenient limit was on the hot path. Checking this crate for the same shape is why
    /// the interaction is written down.
    pub fn from_client(client: reqwest::Client) -> Self {
        ReqwestTransport { client }
    }
}

impl AsyncTransport for ReqwestTransport {
    fn post_json(
        &self,
        url: &str,
        body: &str,
        timeout: Duration,
    ) -> impl Future<Output = Result<String>> + Send {
        // Clone into the future so it borrows nothing: the client is cheap to clone
        // (it is an Arc internally) and this keeps the future `'static`, which is what
        // lets it be raced and spawned.
        let client = self.client.clone();
        let url = url.to_string();
        let body = body.to_string();
        async move {
            let response = client
                .post(&url)
                .header("Content-Type", "application/json")
                .timeout(timeout)
                .body(body)
                .send()
                .await
                .map_err(|e| Error::Rpc(e.to_string()))?;
            response
                .text()
                .await
                .map_err(|e| Error::Rpc(format!("could not read response body: {e}")))
        }
    }
}

/// A sleeper for [`AsyncNodeClient::with_retries`](super::AsyncNodeClient::with_retries),
/// on tokio.
///
/// The client takes the sleep rather than assuming one so that no executor is baked
/// into the async layer. This is the convenience for the common case:
///
/// ```no_run
/// use hivecomb::rpc::{AsyncNodeClient, ReqwestTransport, tokio_sleeper};
/// use std::time::Duration;
///
/// # fn main() -> hivecomb::Result<()> {
/// let client = AsyncNodeClient::new(
///     ReqwestTransport::new()?,
///     vec!["https://api.hive.blog".into()],
/// )?
/// .with_retries(3, Duration::from_millis(250), tokio_sleeper());
/// # let _ = client;
/// # Ok(())
/// # }
/// ```
///
/// `no_run`, not `ignore`: it is compiled on every `cargo test`, so a change to
/// either signature breaks the build rather than rotting silently in a comment. It
/// is not executed, because that would need a network and a tokio runtime.
pub fn tokio_sleeper() -> impl Fn(Duration) -> tokio::time::Sleep + Send + Sync + 'static {
    tokio::time::sleep
}