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}