cargo fmt
This commit is contained in:
parent
f818f3623b
commit
e37a2d9263
6 changed files with 55 additions and 127 deletions
|
|
@ -54,9 +54,7 @@ impl IntervalsRateLimiters {
|
|||
.expect("Intervals IP limiter mutex poisoned");
|
||||
|
||||
let next = match *last {
|
||||
Some(previous) => {
|
||||
std::cmp::max(now, previous + IP_REQUEST_INTERVAL)
|
||||
}
|
||||
Some(previous) => std::cmp::max(now, previous + IP_REQUEST_INTERVAL),
|
||||
None => now,
|
||||
};
|
||||
|
||||
|
|
@ -183,15 +181,13 @@ impl Config {
|
|||
|
||||
cookie_secure,
|
||||
|
||||
intervals_rate_limiters:
|
||||
IntervalsRateLimiters::default(),
|
||||
intervals_rate_limiters: IntervalsRateLimiters::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn required(name: &str) -> Result<String> {
|
||||
let value = env::var(name)
|
||||
.with_context(|| format!("{name} is not configured"))?;
|
||||
let value = env::var(name).with_context(|| format!("{name} is not configured"))?;
|
||||
|
||||
if value.trim().is_empty() {
|
||||
anyhow::bail!("{name} is empty");
|
||||
|
|
@ -201,9 +197,7 @@ fn required(name: &str) -> Result<String> {
|
|||
}
|
||||
|
||||
fn optional(name: &str) -> Option<String> {
|
||||
env::var(name)
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
env::var(name).ok().filter(|value| !value.trim().is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -221,9 +215,7 @@ mod tests {
|
|||
limiters.wait_for_ip_slot().await;
|
||||
}
|
||||
|
||||
assert!(
|
||||
Instant::now().duration_since(start) >= Duration::from_millis(190)
|
||||
);
|
||||
assert!(Instant::now().duration_since(start) >= Duration::from_millis(190));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -234,14 +226,10 @@ mod tests {
|
|||
|
||||
let start = Instant::now();
|
||||
limiters.wait_for_caller("caller-a").await;
|
||||
assert!(
|
||||
Instant::now().duration_since(start) >= Duration::from_millis(70)
|
||||
);
|
||||
assert!(Instant::now().duration_since(start) >= Duration::from_millis(70));
|
||||
|
||||
let start = Instant::now();
|
||||
limiters.wait_for_caller("caller-b").await;
|
||||
assert!(
|
||||
Instant::now().duration_since(start) < Duration::from_millis(40)
|
||||
);
|
||||
assert!(Instant::now().duration_since(start) < Duration::from_millis(40));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,7 +232,6 @@ pub async fn upsert_oauth_athlete(
|
|||
Ok(row.try_get("id")?)
|
||||
}
|
||||
|
||||
|
||||
/// Ensure that an athlete accessible through the current session exists
|
||||
/// locally so activities can reference it. The session credential itself is
|
||||
/// intentionally not stored here; callers continue to use their authenticated
|
||||
|
|
|
|||
|
|
@ -20,10 +20,7 @@ pub struct IntervalsClient {
|
|||
|
||||
impl IntervalsClient {
|
||||
pub fn new(config: Config) -> Self {
|
||||
let authentication = config
|
||||
.intervals_api_key
|
||||
.clone()
|
||||
.map(Authentication::ApiKey);
|
||||
let authentication = config.intervals_api_key.clone().map(Authentication::ApiKey);
|
||||
|
||||
Self {
|
||||
client: Client::new(),
|
||||
|
|
@ -43,10 +40,7 @@ impl IntervalsClient {
|
|||
|
||||
/// Create a client using an OAuth access token.
|
||||
/// Requests are authenticated with HTTP Bearer auth.
|
||||
pub fn with_access_token(
|
||||
config: Config,
|
||||
access_token: impl Into<String>,
|
||||
) -> Self {
|
||||
pub fn with_access_token(config: Config, access_token: impl Into<String>) -> Self {
|
||||
Self {
|
||||
client: Client::new(),
|
||||
config,
|
||||
|
|
@ -70,9 +64,7 @@ impl IntervalsClient {
|
|||
.intervals_client_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| token.clone())),
|
||||
None => Err(anyhow!(
|
||||
"Intervals.icu authentication is not configured"
|
||||
)),
|
||||
None => Err(anyhow!("Intervals.icu authentication is not configured")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,29 +76,17 @@ impl IntervalsClient {
|
|||
)
|
||||
}
|
||||
|
||||
fn authenticated(
|
||||
&self,
|
||||
request: reqwest::RequestBuilder,
|
||||
) -> Result<reqwest::RequestBuilder> {
|
||||
fn authenticated(&self, request: reqwest::RequestBuilder) -> Result<reqwest::RequestBuilder> {
|
||||
match self.authentication.as_ref() {
|
||||
Some(Authentication::ApiKey(token)) => {
|
||||
Ok(request.basic_auth("API_KEY", Some(token)))
|
||||
}
|
||||
Some(Authentication::Bearer(token)) => {
|
||||
Ok(request.bearer_auth(token))
|
||||
}
|
||||
None => Err(anyhow!(
|
||||
"Intervals.icu authentication is not configured"
|
||||
)),
|
||||
Some(Authentication::ApiKey(token)) => Ok(request.basic_auth("API_KEY", Some(token))),
|
||||
Some(Authentication::Bearer(token)) => Ok(request.bearer_auth(token)),
|
||||
None => Err(anyhow!("Intervals.icu authentication is not configured")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_json(&self, url: String) -> Result<Value> {
|
||||
let caller = self.rate_limit_caller()?;
|
||||
let limiters = self
|
||||
.config
|
||||
.intervals_rate_limiters
|
||||
.clone();
|
||||
let limiters = self.config.intervals_rate_limiters.clone();
|
||||
|
||||
// Intervals.icu enforces an additional limit of ten calls/sec per
|
||||
// source IP. Pace every request start globally for this process.
|
||||
|
|
@ -126,10 +106,8 @@ impl IntervalsClient {
|
|||
.context("Intervals.icu request failed")?;
|
||||
|
||||
let status = response.status();
|
||||
let retry_after =
|
||||
parse_retry_after(response.headers().get("Retry-After"));
|
||||
let rate_limit =
|
||||
parse_rate_limit_headers(response.headers());
|
||||
let retry_after = parse_retry_after(response.headers().get("Retry-After"));
|
||||
let rate_limit = parse_rate_limit_headers(response.headers());
|
||||
|
||||
if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
let body = response
|
||||
|
|
@ -173,11 +151,7 @@ impl IntervalsClient {
|
|||
.context("cannot read Intervals.icu response")?;
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(anyhow!(
|
||||
"Intervals.icu returned HTTP {}: {}",
|
||||
status,
|
||||
body
|
||||
));
|
||||
return Err(anyhow!("Intervals.icu returned HTTP {}: {}", status, body));
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
|
|
@ -190,12 +164,7 @@ impl IntervalsClient {
|
|||
);
|
||||
|
||||
return serde_json::from_str(&body)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"invalid JSON returned by Intervals.icu: {}",
|
||||
body
|
||||
)
|
||||
});
|
||||
.with_context(|| format!("invalid JSON returned by Intervals.icu: {}", body));
|
||||
}
|
||||
|
||||
unreachable!("rate-limit retry loop must return")
|
||||
|
|
@ -519,10 +488,7 @@ impl IntervalsClient {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fn parse_retry_after(
|
||||
value: Option<&reqwest::header::HeaderValue>,
|
||||
) -> Option<std::time::Duration> {
|
||||
fn parse_retry_after(value: Option<&reqwest::header::HeaderValue>) -> Option<std::time::Duration> {
|
||||
let value = value?.to_str().ok()?.trim();
|
||||
let seconds = value.parse::<u64>().ok()?;
|
||||
Some(std::time::Duration::from_secs(seconds))
|
||||
|
|
@ -537,9 +503,7 @@ fn parse_retry_after(
|
|||
fn parse_rate_limit_headers(
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
) -> (Option<u64>, Option<u64>, Option<u64>, Option<u64>) {
|
||||
fn pair(
|
||||
value: Option<&reqwest::header::HeaderValue>,
|
||||
) -> (Option<u64>, Option<u64>) {
|
||||
fn pair(value: Option<&reqwest::header::HeaderValue>) -> (Option<u64>, Option<u64>) {
|
||||
let Some(value) = value else {
|
||||
return (None, None);
|
||||
};
|
||||
|
|
@ -556,17 +520,10 @@ fn parse_rate_limit_headers(
|
|||
(first, second)
|
||||
}
|
||||
|
||||
let (limit_15m, limit_daily) =
|
||||
pair(headers.get("X-RateLimit-Limit"));
|
||||
let (remaining_15m, remaining_daily) =
|
||||
pair(headers.get("X-RateLimit-Remaining"));
|
||||
let (limit_15m, limit_daily) = pair(headers.get("X-RateLimit-Limit"));
|
||||
let (remaining_15m, remaining_daily) = pair(headers.get("X-RateLimit-Remaining"));
|
||||
|
||||
(
|
||||
limit_15m,
|
||||
limit_daily,
|
||||
remaining_15m,
|
||||
remaining_daily,
|
||||
)
|
||||
(limit_15m, limit_daily, remaining_15m, remaining_daily)
|
||||
}
|
||||
|
||||
fn parse_datetime(value: &str) -> Result<DateTime<Utc>> {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ pub async fn api(
|
|||
build(&state.db, query.group_id, offset, limit)
|
||||
.await
|
||||
.map(|groups| {
|
||||
let returned = groups.iter().map(|group| group.rows.len() as i64).sum::<i64>();
|
||||
let returned = groups
|
||||
.iter()
|
||||
.map(|group| group.rows.len() as i64)
|
||||
.sum::<i64>();
|
||||
Json(LeaderboardPage {
|
||||
groups,
|
||||
next_offset: offset + returned,
|
||||
|
|
@ -52,7 +55,12 @@ pub async fn api(
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn build(db: &PgPool, group_id: Option<i64>, offset: i64, limit: i64) -> Result<Vec<LeaderboardGroup>> {
|
||||
pub async fn build(
|
||||
db: &PgPool,
|
||||
group_id: Option<i64>,
|
||||
offset: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<LeaderboardGroup>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
WITH bucketed AS (
|
||||
|
|
|
|||
38
src/main.rs
38
src/main.rs
|
|
@ -165,12 +165,8 @@ async fn sync_index(
|
|||
.await
|
||||
.map_err(http_error)?;
|
||||
|
||||
let client =
|
||||
IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
|
||||
let athletes = client
|
||||
.athletes()
|
||||
.await
|
||||
.map_err(http_error)?;
|
||||
let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
|
||||
let athletes = client.athletes().await.map_err(http_error)?;
|
||||
let athletes = athletes
|
||||
.as_array()
|
||||
.ok_or_else(|| http_error(anyhow!("athletes response is not an array")))?;
|
||||
|
|
@ -310,11 +306,7 @@ async fn sync_athlete(
|
|||
*/
|
||||
let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
|
||||
|
||||
let target_display_name = ensure_accessible_athlete(
|
||||
&state,
|
||||
&client,
|
||||
&athlete_id,
|
||||
)
|
||||
let target_display_name = ensure_accessible_athlete(&state, &client, &athlete_id)
|
||||
.await
|
||||
.map_err(http_error)?;
|
||||
|
||||
|
|
@ -770,11 +762,7 @@ async fn sync_import_activity(
|
|||
|
||||
let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
|
||||
|
||||
ensure_accessible_athlete(
|
||||
&state,
|
||||
&client,
|
||||
&athlete_id,
|
||||
)
|
||||
ensure_accessible_athlete(&state, &client, &athlete_id)
|
||||
.await
|
||||
.map_err(http_error)?;
|
||||
|
||||
|
|
@ -876,11 +864,7 @@ async fn sync_import_visible(
|
|||
|
||||
let client = IntervalsClient::with_api_key(state.config.clone(), session.access_token.clone());
|
||||
|
||||
ensure_accessible_athlete(
|
||||
&state,
|
||||
&client,
|
||||
&athlete_id,
|
||||
)
|
||||
ensure_accessible_athlete(&state, &client, &athlete_id)
|
||||
.await
|
||||
.map_err(http_error)?;
|
||||
|
||||
|
|
@ -975,10 +959,7 @@ async fn ensure_accessible_athlete(
|
|||
client: &IntervalsClient,
|
||||
athlete_id: &str,
|
||||
) -> Result<String> {
|
||||
let athlete = client
|
||||
.athlete(athlete_id)
|
||||
.await
|
||||
.with_context(|| {
|
||||
let athlete = client.athlete(athlete_id).await.with_context(|| {
|
||||
format!(
|
||||
"cannot access Intervals.icu athlete {} with current authentication token",
|
||||
athlete_id
|
||||
|
|
@ -992,12 +973,7 @@ async fn ensure_accessible_athlete(
|
|||
.unwrap_or(athlete_id)
|
||||
.to_string();
|
||||
|
||||
db::ensure_local_athlete(
|
||||
&state.db,
|
||||
athlete_id,
|
||||
&display_name,
|
||||
)
|
||||
.await?;
|
||||
db::ensure_local_athlete(&state.db, athlete_id, &display_name).await?;
|
||||
|
||||
Ok(display_name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -728,7 +728,7 @@ function initLeaderboardElements(root){
|
|||
const leaderboardList=document.getElementById('leaderboard-list');
|
||||
if(leaderboardList){
|
||||
const status=document.getElementById('leaderboard-status'),sentinel=document.getElementById('leaderboard-sentinel'),groups=new Map();
|
||||
let offset=0,loading=false,hasMore=true; const pageSize=50;
|
||||
let offset=0,loading=false,hasMore=true; const pageSize=25;
|
||||
async function loadLeaderboard(){
|
||||
if(loading||!hasMore)return; loading=true;
|
||||
try{
|
||||
|
|
|
|||
Loading…
Reference in a new issue