hypersync_client_solana/
rate_limit.rs1#[derive(Debug, Clone, Default)]
12pub struct RateLimitInfo {
13 pub limit: Option<u64>,
18 pub remaining: Option<u64>,
23 pub reset_secs: Option<u64>,
27 pub cost: Option<u64>,
32}
33
34impl std::fmt::Display for RateLimitInfo {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 let mut parts = Vec::new();
37 if let (Some(remaining), Some(limit)) = (self.remaining, self.limit) {
38 let cost = self.cost.filter(|cost| *cost > 0).unwrap_or(1);
39 parts.push(format!(
40 "remaining={}/{} reqs",
41 remaining / cost,
42 limit / cost,
43 ));
44 } else {
45 if let Some(remaining) = self.remaining {
46 parts.push(format!("remaining={remaining}"));
47 }
48 if let Some(limit) = self.limit {
49 parts.push(format!("limit={limit}"));
50 }
51 }
52 if let Some(reset) = self.reset_secs {
53 parts.push(format!("resets_in={reset}s"));
54 }
55 write!(f, "{}", parts.join(", "))
56 }
57}
58
59impl RateLimitInfo {
60 pub(crate) fn from_response(res: &reqwest::Response) -> Self {
64 Self {
65 limit: Self::parse_limit_header(res),
66 remaining: Self::parse_u64_header(res, "x-ratelimit-remaining"),
67 reset_secs: Self::parse_u64_header(res, "x-ratelimit-reset"),
68 cost: Self::parse_u64_header(res, "x-ratelimit-cost"),
69 }
70 }
71
72 pub fn is_rate_limited(&self) -> bool {
74 self.remaining == Some(0)
75 }
76
77 pub fn suggested_wait_secs(&self) -> Option<u64> {
79 self.reset_secs
80 }
81
82 fn parse_limit_header(res: &reqwest::Response) -> Option<u64> {
85 let value = res.headers().get("x-ratelimit-limit")?.to_str().ok()?;
86 let first = value.split(',').next()?.trim();
88 first.parse().ok()
89 }
90
91 fn parse_u64_header(res: &reqwest::Response, name: &str) -> Option<u64> {
93 res.headers().get(name)?.to_str().ok()?.trim().parse().ok()
94 }
95}
96
97#[derive(Debug, Clone)]
107pub struct QueryResponseWithRateLimit<T> {
108 pub response: T,
110 pub rate_limit: RateLimitInfo,
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_is_rate_limited() {
120 let info = RateLimitInfo {
121 remaining: Some(0),
122 ..Default::default()
123 };
124 assert!(info.is_rate_limited());
125
126 let info = RateLimitInfo {
127 remaining: Some(5),
128 ..Default::default()
129 };
130 assert!(!info.is_rate_limited());
131
132 let info = RateLimitInfo::default();
133 assert!(!info.is_rate_limited());
134 }
135
136 #[test]
137 fn test_from_response_header_case_insensitive() {
138 let http_resp = http::Response::builder()
141 .header("X-RateLimit-Remaining", "42")
142 .header("X-RATELIMIT-RESET", "30")
143 .header("X-Ratelimit-Limit", "100, 100;w=60")
144 .header("X-Ratelimit-Cost", "10")
145 .body("")
146 .unwrap();
147 let resp: reqwest::Response = http_resp.into();
148
149 let info = RateLimitInfo::from_response(&resp);
150 assert_eq!(
151 (info.limit, info.remaining, info.reset_secs, info.cost),
152 (Some(100), Some(42), Some(30), Some(10))
153 );
154 }
155
156 #[test]
157 fn test_from_response_ignores_malformed_headers() {
158 let http_resp = http::Response::builder()
159 .header("x-ratelimit-limit", "not-a-number, 50;w=60")
160 .header("x-ratelimit-remaining", "-1")
161 .header("x-ratelimit-reset", "tomorrow")
162 .header("x-ratelimit-cost", "1.5")
163 .body("")
164 .unwrap();
165 let resp: reqwest::Response = http_resp.into();
166
167 let info = RateLimitInfo::from_response(&resp);
168 assert_eq!(
169 (info.limit, info.remaining, info.reset_secs, info.cost),
170 (None, None, None, None)
171 );
172 }
173
174 #[test]
175 fn test_suggested_wait_secs() {
176 let info = RateLimitInfo {
177 reset_secs: Some(30),
178 ..Default::default()
179 };
180 assert_eq!(info.suggested_wait_secs(), Some(30));
181
182 let info = RateLimitInfo::default();
183 assert_eq!(info.suggested_wait_secs(), None);
184 }
185
186 #[test]
187 fn test_display_full() {
188 let info = RateLimitInfo {
189 limit: Some(50),
190 remaining: Some(0),
191 reset_secs: Some(59),
192 cost: Some(10),
193 };
194 assert_eq!(info.to_string(), "remaining=0/5 reqs, resets_in=59s");
195 }
196
197 #[test]
198 fn test_display_partial() {
199 let info = RateLimitInfo {
200 remaining: Some(3),
201 reset_secs: Some(30),
202 ..Default::default()
203 };
204 assert_eq!(info.to_string(), "remaining=3, resets_in=30s");
205 }
206
207 #[test]
208 fn test_display_empty() {
209 let info = RateLimitInfo::default();
210 assert_eq!(info.to_string(), "");
211 }
212
213 #[test]
214 fn test_display_zero_cost_does_not_panic() {
215 let info = RateLimitInfo {
216 limit: Some(50),
217 remaining: Some(0),
218 cost: Some(0),
219 ..Default::default()
220 };
221 assert_eq!(info.to_string(), "remaining=0/50 reqs");
222 }
223}