use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use super::token::TokenData;
use crate::error::Result;
pub async fn auto_refresh_token<F, Fut>(token: &mut TokenData, refresh: F) -> Result<()>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<TokenData>>,
{
if !token.is_expired() {
return Ok(());
}
let new_token = refresh().await?;
*token = new_token;
Ok(())
}
#[derive(Clone)]
pub struct TokenRefresher {
locks: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
}
impl TokenRefresher {
pub fn new() -> Self {
Self {
locks: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn ensure_valid<F, Fut>(
&self,
key: &str,
token: &mut TokenData,
refresh: F,
) -> Result<()>
where
F: FnOnce() -> Fut + Send,
Fut: std::future::Future<Output = Result<TokenData>> + Send,
{
if !token.is_expired() {
return Ok(());
}
let lock = {
let mut map = self.locks.lock().await;
map.entry(key.to_string())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone()
};
let _guard = lock.lock().await;
if !token.is_expired() {
return Ok(());
}
let new_token = refresh().await?;
*token = new_token;
Ok(())
}
}
impl Default for TokenRefresher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn unused_token_does_not_refresh() {
let mut token = TokenData {
access_token: "tok".into(),
refresh_token: None,
token_type: "Bearer".into(),
expires_in: 3600,
scope: None,
user_type: None,
company_id: None,
location_id: None,
issued_at: None,
};
let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
let replacement = token.clone();
let cc = call_count.clone();
auto_refresh_token(&mut token, move || {
cc.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let t = replacement.clone();
async { Ok(t) }
})
.await
.unwrap();
assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 0);
}
#[tokio::test]
async fn expired_token_triggers_refresh() {
let mut token = TokenData {
access_token: "tok".into(),
refresh_token: None,
token_type: "Bearer".into(),
expires_in: 3600,
scope: None,
user_type: None,
company_id: None,
location_id: None,
issued_at: Some(1),
};
let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
let replacement = TokenData {
access_token: "new_tok".into(),
refresh_token: None,
token_type: "Bearer".into(),
expires_in: 3600,
scope: None,
user_type: None,
company_id: None,
location_id: None,
issued_at: None,
};
let cc = call_count.clone();
auto_refresh_token(&mut token, move || {
cc.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let t = replacement.clone();
async { Ok(t) }
})
.await
.unwrap();
assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(token.access_token, "new_tok");
}
#[tokio::test]
async fn single_flight_refreshes_once() {
let refresher = TokenRefresher::new();
let shared = Arc::new(tokio::sync::Mutex::new(TokenData {
access_token: "tok".into(),
refresh_token: None,
token_type: "Bearer".into(),
expires_in: 3600,
scope: None,
user_type: None,
company_id: None,
location_id: None,
issued_at: Some(1),
}));
let call_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
let mut tasks = vec![];
for _ in 0..5 {
let r = refresher.clone();
let cc = call_count.clone();
let shared = shared.clone();
tasks.push(tokio::spawn(async move {
let mut token = shared.lock().await;
r.ensure_valid("loc_1", &mut *token, || {
cc.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
async move {
Ok(TokenData {
access_token: "refreshed".into(),
refresh_token: None,
token_type: "Bearer".into(),
expires_in: 3600,
scope: None,
user_type: None,
company_id: None,
location_id: None,
issued_at: None,
})
}
})
.await
.unwrap();
token.access_token.clone()
}));
}
for task in tasks {
assert_eq!(task.await.unwrap(), "refreshed");
}
assert_eq!(call_count.load(std::sync::atomic::Ordering::SeqCst), 1);
}
}