use super::client::{ClientConfig, YahooClient};
use crate::error::Result;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
const SESSION_CAP: usize = 8;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct SessionKey {
runtime: tokio::runtime::Id,
timeout: Duration,
proxy: Option<String>,
lang: String,
region: String,
}
pub(crate) fn key_for(config: &ClientConfig) -> SessionKey {
SessionKey {
runtime: tokio::runtime::Handle::current().id(),
timeout: config.timeout,
proxy: config.proxy.clone(),
lang: config.lang.clone(),
region: config.region.clone(),
}
}
fn sessions() -> &'static Mutex<HashMap<SessionKey, Arc<YahooClient>>> {
static SESSIONS: OnceLock<Mutex<HashMap<SessionKey, Arc<YahooClient>>>> = OnceLock::new();
SESSIONS.get_or_init(|| Mutex::new(HashMap::new()))
}
pub(crate) async fn get_or_auth(config: &ClientConfig) -> Result<Arc<YahooClient>> {
let key = key_for(config);
if let Some(client) = sessions().lock().unwrap().get(&key) {
return Ok(Arc::clone(client));
}
let client = Arc::new(
YahooClient::new(config.clone())
.await?
.with_session_key(key.clone()),
);
let mut map = sessions().lock().unwrap();
if map.len() >= SESSION_CAP {
map.retain(|_, c| Arc::strong_count(c) > 1);
if map.len() >= SESSION_CAP {
map.clear();
}
}
Ok(Arc::clone(
map.entry(key).or_insert_with(|| Arc::clone(&client)),
))
}
#[cfg(test)]
pub(crate) fn invalidate(config: &ClientConfig) {
invalidate_key(&key_for(config));
}
pub(crate) fn invalidate_key(key: &SessionKey) {
sessions().lock().unwrap().remove(key);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn key_includes_runtime_and_config() {
let a = ClientConfig::default();
let b = ClientConfig {
lang: "ja-JP".to_string(),
..ClientConfig::default()
};
assert_eq!(key_for(&a), key_for(&a));
assert_ne!(key_for(&a), key_for(&b));
}
#[test]
fn keys_differ_across_runtimes() {
let config = ClientConfig::default();
let rt1 = tokio::runtime::Runtime::new().unwrap();
let rt2 = tokio::runtime::Runtime::new().unwrap();
let k1 = rt1.block_on(async { key_for(&config) });
let k2 = rt2.block_on(async { key_for(&config) });
assert_ne!(k1, k2);
}
#[tokio::test]
#[ignore = "requires network access"]
async fn same_runtime_reuses_one_client() {
let config = ClientConfig::default();
let a = get_or_auth(&config).await.unwrap();
let b = get_or_auth(&config).await.unwrap();
assert!(Arc::ptr_eq(&a, &b));
}
#[tokio::test]
#[ignore = "requires network access"]
async fn invalidate_forces_a_new_client() {
let config = ClientConfig::default();
let a = get_or_auth(&config).await.unwrap();
invalidate(&config);
let b = get_or_auth(&config).await.unwrap();
assert!(!Arc::ptr_eq(&a, &b));
}
}