pivx_throttled_jsonrpc/
lib.rs

1//! # Throttled JSON-RPC Client
2//!
3//! A macro-based JSON-RPC client generator with built-in rate limiting,
4//! concurrency control, and request batching.
5//!
6//! ## Features
7//!
8//! - **Declarative API**: Define your RPC client with a simple macro syntax
9//! - **Rate Limiting**: Control requests-per-second (RPS) to avoid overwhelming servers
10//! - **Concurrency Control**: Limit simultaneous in-flight requests
11//! - **Request Batching**: Efficiently batch multiple RPC calls
12//! - **Flexible Response Types**: Support for both single-type and enum variant responses
13//!
14//! ## Throttling Behavior
15//!
16//! This library provides **synchronous/blocking** throttling using `std::thread::sleep`:
17//!
18//! ### Rate Limiting (RPS)
19//! - **When**: `rps > 0`
20//! - **How**: Enforces minimum time `1/rps` seconds between consecutive requests
21//! - **Behavior**: Thread sleeps if previous request was too recent
22//! - **Scope**: Global across all threads using the same client instance
23//!
24//! ### Concurrency Limiting
25//! - **When**: `max_concurrency > 0`
26//! - **How**: Limits number of simultaneous in-flight requests
27//! - **Behavior**: Thread blocks (via Condvar) until a slot is available
28//! - **Scope**: Global across all threads using the same client instance
29//!
30//! ### Important Notes
31//! - This is a **blocking/synchronous** client - threads will sleep/block
32//! - For async workloads, consider wrapping calls in `tokio::task::spawn_blocking`
33//! - Timeouts are controlled by the underlying `reqwest` client (default: 30s connect, no read timeout)
34//!
35//! ## Example
36//!
37//! ```no_run
38//! use throttled_json_rpc::jsonrpc_client;
39//!
40//! jsonrpc_client!(pub struct MyRpcClient {
41//!     single:
42//!         /// Get block hash by height
43//!         pub fn getblockhash(&self, height: u64) -> Result<String>;
44//!     enum:
45//! });
46//!
47//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
48//! let client = MyRpcClient::new(
49//!     "http://localhost:8332".to_string(),
50//!     Some("rpcuser".to_string()),
51//!     Some("rpcpass".to_string()),
52//!     5,    // max 5 concurrent requests
53//!     10,   // max 10 requests per second
54//!     0,    // no batching (0 = disabled)
55//! );
56//!
57//! let block_hash = client.getblockhash(100)?;
58//! println!("Block hash: {}", block_hash);
59//! # Ok(())
60//! # }
61//! ```
62
63use thiserror::Error;
64
65/// Error types for JSON-RPC operations
66#[derive(Error, Debug)]
67pub enum RpcError {
68    /// HTTP request failed
69    #[error("HTTP request failed: {0}")]
70    HttpError(#[from] reqwest::Error),
71
72    /// JSON deserialization failed
73    #[error("JSON deserialization failed: {source}\nBody: {body}")]
74    JsonError {
75        source: serde_json::Error,
76        body: String,
77    },
78
79    /// RPC server returned an error
80    #[error("RPC error: {error:?}")]
81    RpcError { error: serde_json::Value },
82
83    /// Response missing required ID field
84    #[error("Response missing ID field")]
85    MissingId,
86
87    /// Response missing in batch result
88    #[error("Missing response in batch result")]
89    MissingResponse,
90
91    /// RPC returned null result
92    #[error("RPC returned null result")]
93    NullResponse,
94
95    /// Wrong enum variant for response
96    #[error("Wrong variant of {enum_name}: expected {expected}")]
97    WrongVariant {
98        enum_name: &'static str,
99        expected: &'static str,
100    },
101
102    /// Cannot deserialize to any enum variant
103    #[error("Cannot deserialize to any variant of {enum_name}:\n{body}")]
104    CannotDeserialize {
105        enum_name: &'static str,
106        body: String,
107    },
108}
109
110#[macro_use]
111mod macros;
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_macro_expansion() {
119        jsonrpc_client!(pub struct TestClient {
120            single:
121                pub fn test_method(&self, arg: u64) -> Result<String>;
122            enum:
123                pub fn poly_method(&self) -> Result<A(String)|B(u64)>;
124        });
125
126        // Test that the macro expands without errors
127        let _client = TestClient::new("http://localhost:8332".to_string(), None, None, 0, 0, 0);
128    }
129}