Skip to main content

vaea_flash_sdk/
warm_cache.rs

1/// VAEA Flash — Warm Cache (Rust)
2///
3/// Background capacity pre-warming via polling.
4/// Matches the scanner's 2-second refresh interval.
5/// Uses our own /v1/capacity API — zero external dependencies.
6
7use crate::types::{CapacityResponse, TokenCapacity, VAEA_API_URL};
8use crate::local_builder::update_registry_from_capacity;
9use std::sync::{Arc, Mutex};
10use tokio::task::JoinHandle;
11
12/// Background capacity cache with automatic polling.
13///
14/// # Example
15/// ```rust,no_run
16/// use vaea_flash_sdk::warm_cache::WarmCache;
17///
18/// let cache = WarmCache::new(None, None);
19/// cache.start().await;
20///
21/// // Later, in your hot loop:
22/// if let Some(sol) = cache.get_token_capacity("SOL") {
23///     println!("SOL available: {}", sol.max_amount);
24/// }
25///
26/// cache.stop();
27/// ```
28pub struct WarmCache {
29    api_url: String,
30    refresh_ms: u64,
31    inner: Arc<Mutex<WarmCacheInner>>,
32    task_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
33}
34
35struct WarmCacheInner {
36    capacity: Option<CapacityResponse>,
37    listeners: Vec<Box<dyn Fn(&CapacityResponse) + Send + 'static>>,
38}
39
40impl WarmCache {
41    /// Create a new WarmCache.
42    ///
43    /// * `api_url` — API base URL (default: VAEA_API_URL)
44    /// * `refresh_ms` — Polling interval in milliseconds (default: 2000)
45    pub fn new(api_url: Option<&str>, refresh_ms: Option<u64>) -> Self {
46        Self {
47            api_url: api_url.unwrap_or(VAEA_API_URL).to_string(),
48            refresh_ms: refresh_ms.unwrap_or(2000),
49            inner: Arc::new(Mutex::new(WarmCacheInner {
50                capacity: None,
51                listeners: Vec::new(),
52            })),
53            task_handle: Arc::new(Mutex::new(None)),
54        }
55    }
56
57    /// Start background polling. First refresh is synchronous.
58    pub async fn start(&self) {
59        // Initial fetch
60        self.refresh().await;
61
62        // Start background task
63        let api_url = self.api_url.clone();
64        let refresh_ms = self.refresh_ms;
65        let inner = Arc::clone(&self.inner);
66
67        let handle = tokio::spawn(async move {
68            let client = reqwest::Client::new();
69            let mut interval = tokio::time::interval(std::time::Duration::from_millis(refresh_ms));
70            loop {
71                interval.tick().await;
72                if let Ok(res) = client.get(&format!("{}/v1/capacity", api_url)).send().await {
73                    if let Ok(capacity) = res.json::<CapacityResponse>().await {
74                        // Auto-sync the TokenRegistry
75                        update_registry_from_capacity(&capacity.tokens);
76                        let mut guard = inner.lock().unwrap();
77                        guard.capacity = Some(capacity.clone());
78                        for listener in &guard.listeners {
79                            listener(&capacity);
80                        }
81                    }
82                }
83            }
84        });
85
86        *self.task_handle.lock().unwrap() = Some(handle);
87    }
88
89    /// Stop background polling.
90    pub fn stop(&self) {
91        if let Some(handle) = self.task_handle.lock().unwrap().take() {
92            handle.abort();
93        }
94    }
95
96    /// Register a listener for capacity updates.
97    pub fn on_update<F: Fn(&CapacityResponse) + Send + 'static>(&self, handler: F) {
98        self.inner.lock().unwrap().listeners.push(Box::new(handler));
99    }
100
101    /// Get cached capacity (None if not yet loaded).
102    pub fn get_capacity(&self) -> Option<CapacityResponse> {
103        self.inner.lock().unwrap().capacity.clone()
104    }
105
106    /// Get cached capacity for a single token.
107    pub fn get_token_capacity(&self, symbol: &str) -> Option<TokenCapacity> {
108        let guard = self.inner.lock().unwrap();
109        guard.capacity.as_ref()?.tokens.iter()
110            .find(|t| t.symbol.eq_ignore_ascii_case(symbol))
111            .cloned()
112    }
113
114    /// Check if cache is warm (has data).
115    pub fn is_warm(&self) -> bool {
116        self.inner.lock().unwrap().capacity.is_some()
117    }
118
119    async fn refresh(&self) {
120        let client = reqwest::Client::new();
121        if let Ok(res) = client.get(&format!("{}/v1/capacity", self.api_url)).send().await {
122            if let Ok(capacity) = res.json::<CapacityResponse>().await {
123                // Auto-sync the TokenRegistry
124                update_registry_from_capacity(&capacity.tokens);
125                let mut guard = self.inner.lock().unwrap();
126                guard.capacity = Some(capacity.clone());
127                for listener in &guard.listeners {
128                    listener(&capacity);
129                }
130            }
131        }
132    }
133}