use std::sync::{Mutex, MutexGuard, PoisonError};
use crate::client::limit::{Gate, Meter, Spent, action_weight, base_weight, per_items, read_weight};
use crate::client::network::Network;
use crate::client::transport::{Http, INFO, Request, Transport, TransportError};
use crate::utils::json::Json;
const EXCHANGE_KIND: &str = "exchange";
pub type Clock = fn() -> u64;
#[derive(Debug, Default)]
struct Limiter {
gate: Gate,
meter: Meter,
}
pub struct Api<H> {
http: H,
network: Network,
clock: Clock,
limiter: Mutex<Limiter>,
}
impl<H: Http> Api<H> {
pub fn new(http: H, network: Network, clock: Clock) -> Self {
Api { http, network, clock, limiter: Mutex::new(Limiter::default()) }
}
#[must_use]
pub const fn network(&self) -> Network {
self.network
}
#[must_use]
pub const fn http(&self) -> &H {
&self.http
}
pub fn spent(&self) -> Spent {
let now = (self.clock)();
let mut limiter = self.limiter();
let cooldown_ms = limiter.gate.cooldown_ms(now);
Spent { cooldown_ms, ..limiter.meter.spent(now) }
}
fn limiter(&self) -> MutexGuard<'_, Limiter> {
self.limiter.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl<H: Http> Transport for Api<H> {
async fn post(&self, path: &str, body: String) -> Result<String, TransportError> {
let is_read = path == INFO;
let kind = match is_read {
true => Json::parse(&body)
.ok()
.and_then(|request| request.get("type").and_then(Json::as_str).map(ToOwned::to_owned))
.unwrap_or_default(),
false => EXCHANGE_KIND.to_owned(),
};
let batched = match is_read {
true => 0,
false => batched(&body),
};
let ticket = {
let now = (self.clock)();
let mut limiter = self.limiter();
let ticket = limiter.gate.open(now).map_err(|wait| rate_limited(Some(wait)))?;
if is_read && let Some(wait) = limiter.meter.wait_for(now, base_weight(&kind)) {
return Err(rate_limited(Some(wait)));
}
ticket
};
let request = Request::post_json(format!("{}{path}", self.network.api_url()), body);
let answer = self.http.send(request).await;
let now = (self.clock)();
let mut limiter = self.limiter();
let response = answer?;
if response.status == 429 {
limiter.meter.refuse(now);
let retry_after_ms = response.header("retry-after").and_then(|seconds| seconds.trim().parse::<u64>().ok());
let wait = limiter.gate.struck(ticket, now, retry_after_ms.map(|seconds| seconds.saturating_mul(1_000)));
return Err(rate_limited(Some(wait)));
}
let text = response.text();
let weight = match (is_read, response.is_success()) {
(true, true) => read_weight(&kind, per_items(&kind).map_or(0, |_| items(&text))),
(true, false) => base_weight(&kind),
(false, _) => action_weight(batched),
};
limiter.meter.record(now, &kind, weight);
match response.is_success() {
true => {
limiter.gate.passed(ticket);
Ok(text)
}
false => Err(TransportError::Status { status: response.status, body: text }),
}
}
}
const fn rate_limited(retry_after_ms: Option<u64>) -> TransportError {
TransportError::RateLimited { retry_after_ms }
}
fn batched(body: &str) -> usize {
let Ok(body) = Json::parse(body) else { return 0 };
let Some(action) = body.get("action") else { return 0 };
["orders", "cancels", "modifies"]
.iter()
.find_map(|key| action.get(key).and_then(Json::as_array))
.map_or(0, <[Json]>::len)
}
fn items(text: &str) -> usize {
serde_json::from_str::<Vec<serde::de::IgnoredAny>>(text).map_or(0, |list| list.len())
}
#[cfg(test)]
#[path = "tests/api.rs"]
mod tests;