use crate::error::Result;
use crate::utils::CacheEntry;
use std::collections::HashMap;
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
pub(crate) struct DomainCache<V> {
ttl: Option<Duration>,
entries: RwLock<HashMap<String, CacheEntry<V>>>,
guard: Mutex<()>,
}
impl<V: Clone> DomainCache<V> {
pub(crate) fn new(ttl: Option<Duration>) -> Self {
Self {
ttl,
entries: RwLock::new(HashMap::new()),
guard: Mutex::new(()),
}
}
pub(crate) async fn get_or_try<F, Fut>(&self, key: String, f: F) -> Result<V>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<V>>,
{
let Some(ttl) = self.ttl else {
return f().await;
};
if let Some(entry) = self.entries.read().await.get(&key)
&& entry.is_fresh(ttl)
{
return Ok(entry.value.clone());
}
let _g = self.guard.lock().await;
if let Some(entry) = self.entries.read().await.get(&key)
&& entry.is_fresh(ttl)
{
return Ok(entry.value.clone());
}
let value = f().await?;
self.entries
.write()
.await
.insert(key, CacheEntry::new(value.clone()));
Ok(value)
}
}
macro_rules! domain_handle {
($(#[$meta:meta])* pub struct $name:ident { $field:ident, $accessor:ident } cache: $val:ty) => {
$(#[$meta])*
pub struct $name {
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
cache: crate::domains::DomainCache<$val>,
}
impl $name {
pub(crate) fn with_providers(
$field: std::sync::Arc<str>,
providers: std::sync::Arc<crate::providers::ProviderSet>,
) -> Self {
Self {
$field,
providers,
cache: crate::domains::DomainCache::new(None),
}
}
pub fn cache(mut self, ttl: std::time::Duration) -> Self {
self.cache = crate::domains::DomainCache::new(Some(ttl));
self
}
pub fn $accessor(&self) -> &str {
&self.$field
}
}
};
}
macro_rules! fetch_via {
($self:expr, $field:ident, $cap:ident, $fetch:ident, $ret:ty) => {{
let __sym = $self.$field.clone();
let __providers = std::sync::Arc::clone(&$self.providers);
$self
.cache
.get_or_try(String::new(), move || async move {
__providers
.fetch(crate::providers::Capability::$cap, move |p| {
let __s = __sym.clone();
let p = p.clone();
async move { p.$fetch(&__s).await }
})
.await
})
.await
}};
}
#[allow(unused_macros)]
macro_rules! fetch_via_with {
($self:expr, $field:ident, $cap:ident, $fetch:ident, $arg:expr, $ret:ty) => {{
let __sym = $self.$field.clone();
let __arg = ($arg).to_string();
let __providers = std::sync::Arc::clone(&$self.providers);
$self
.cache
.get_or_try(__arg.clone(), move || async move {
__providers
.fetch(crate::providers::Capability::$cap, move |p| {
let __s = __sym.clone();
let __a = __arg.clone();
let p = p.clone();
async move { p.$fetch(&__s, &__a).await }
})
.await
})
.await
}};
}
#[cfg(any(feature = "fmp", feature = "alphavantage"))]
pub(crate) mod commodities;
#[cfg(any(
feature = "alphavantage",
feature = "crypto",
feature = "fmp",
feature = "polygon"
))]
pub(crate) mod crypto;
#[cfg(any(feature = "fred", feature = "alphavantage", feature = "polygon"))]
pub(crate) mod economic;
pub(crate) mod filings;
#[cfg(any(feature = "polygon", feature = "fmp", feature = "alphavantage"))]
pub(crate) mod forex;
#[cfg(feature = "polygon")]
pub(crate) mod futures;
#[cfg(any(feature = "polygon", feature = "fmp"))]
pub(crate) mod indices;
#[cfg(any(feature = "fmp", feature = "alphavantage"))]
pub use commodities::Commodity;
#[cfg(any(
feature = "alphavantage",
feature = "crypto",
feature = "fmp",
feature = "polygon"
))]
pub use crypto::CryptoCoin;
#[cfg(any(feature = "fred", feature = "alphavantage", feature = "polygon"))]
pub use economic::EconomicIndicator;
pub use filings::Filings;
#[cfg(any(feature = "polygon", feature = "fmp", feature = "alphavantage"))]
pub use forex::ForexPair;
#[cfg(feature = "polygon")]
pub use futures::FuturesContract;
#[cfg(any(feature = "polygon", feature = "fmp"))]
pub use indices::Index;
#[cfg(test)]
mod tests {
use super::*;
use crate::error::FinanceError;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
fn err() -> FinanceError {
FinanceError::NoProviderAvailable { operation: "test" }
}
#[tokio::test]
async fn no_ttl_fetches_every_call() {
let cache: DomainCache<u32> = DomainCache::new(None);
let calls = Arc::new(AtomicUsize::new(0));
for _ in 0..3 {
let c = calls.clone();
let v = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(42)
})
.await
.unwrap();
assert_eq!(v, 42);
}
assert_eq!(calls.load(Ordering::SeqCst), 3);
}
#[tokio::test]
async fn ttl_caches_within_window() {
let cache: DomainCache<u32> = DomainCache::new(Some(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
for _ in 0..3 {
let c = calls.clone();
let v = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(7)
})
.await
.unwrap();
assert_eq!(v, 7);
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn distinct_keys_cached_separately() {
let cache: DomainCache<String> = DomainCache::new(Some(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
for key in ["usd", "eur", "usd", "eur"] {
let c = calls.clone();
let owned = key.to_string();
let v = cache
.get_or_try(key.to_string(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<String, FinanceError>(owned)
})
.await
.unwrap();
assert_eq!(v, key);
}
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn errors_are_not_cached() {
let cache: DomainCache<u32> = DomainCache::new(Some(Duration::from_secs(60)));
let calls = Arc::new(AtomicUsize::new(0));
let c = calls.clone();
let first = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Err::<u32, FinanceError>(err())
})
.await;
assert!(first.is_err());
let c = calls.clone();
let second = cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
Ok::<u32, FinanceError>(5)
})
.await
.unwrap();
assert_eq!(second, 5);
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn concurrent_misses_dedup_to_one_fetch() {
let cache: Arc<DomainCache<u32>> =
Arc::new(DomainCache::new(Some(Duration::from_secs(60))));
let calls = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::new();
for _ in 0..8 {
let cache = Arc::clone(&cache);
let c = calls.clone();
handles.push(tokio::spawn(async move {
cache
.get_or_try(String::new(), move || async move {
c.fetch_add(1, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(20)).await;
Ok::<u32, FinanceError>(1)
})
.await
.unwrap()
}));
}
for h in handles {
assert_eq!(h.await.unwrap(), 1);
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
}