vaea-flash-sdk 0.1.0

VAEA Flash — Universal Flash Loan SDK for Solana. Borrow any SPL token atomically in one call.
Documentation
/// VAEA Flash — Warm Cache (Rust)
///
/// Background capacity pre-warming via polling.
/// Matches the scanner's 2-second refresh interval.
/// Uses our own /v1/capacity API — zero external dependencies.

use crate::types::{CapacityResponse, TokenCapacity, VAEA_API_URL};
use crate::local_builder::update_registry_from_capacity;
use std::sync::{Arc, Mutex};
use tokio::task::JoinHandle;

/// Background capacity cache with automatic polling.
///
/// # Example
/// ```rust,no_run
/// use vaea_flash_sdk::warm_cache::WarmCache;
///
/// let cache = WarmCache::new(None, None);
/// cache.start().await;
///
/// // Later, in your hot loop:
/// if let Some(sol) = cache.get_token_capacity("SOL") {
///     println!("SOL available: {}", sol.max_amount);
/// }
///
/// cache.stop();
/// ```
pub struct WarmCache {
    api_url: String,
    refresh_ms: u64,
    inner: Arc<Mutex<WarmCacheInner>>,
    task_handle: Arc<Mutex<Option<JoinHandle<()>>>>,
}

struct WarmCacheInner {
    capacity: Option<CapacityResponse>,
    listeners: Vec<Box<dyn Fn(&CapacityResponse) + Send + 'static>>,
}

impl WarmCache {
    /// Create a new WarmCache.
    ///
    /// * `api_url` — API base URL (default: VAEA_API_URL)
    /// * `refresh_ms` — Polling interval in milliseconds (default: 2000)
    pub fn new(api_url: Option<&str>, refresh_ms: Option<u64>) -> Self {
        Self {
            api_url: api_url.unwrap_or(VAEA_API_URL).to_string(),
            refresh_ms: refresh_ms.unwrap_or(2000),
            inner: Arc::new(Mutex::new(WarmCacheInner {
                capacity: None,
                listeners: Vec::new(),
            })),
            task_handle: Arc::new(Mutex::new(None)),
        }
    }

    /// Start background polling. First refresh is synchronous.
    pub async fn start(&self) {
        // Initial fetch
        self.refresh().await;

        // Start background task
        let api_url = self.api_url.clone();
        let refresh_ms = self.refresh_ms;
        let inner = Arc::clone(&self.inner);

        let handle = tokio::spawn(async move {
            let client = reqwest::Client::new();
            let mut interval = tokio::time::interval(std::time::Duration::from_millis(refresh_ms));
            loop {
                interval.tick().await;
                if let Ok(res) = client.get(&format!("{}/v1/capacity", api_url)).send().await {
                    if let Ok(capacity) = res.json::<CapacityResponse>().await {
                        // Auto-sync the TokenRegistry
                        update_registry_from_capacity(&capacity.tokens);
                        let mut guard = inner.lock().unwrap();
                        guard.capacity = Some(capacity.clone());
                        for listener in &guard.listeners {
                            listener(&capacity);
                        }
                    }
                }
            }
        });

        *self.task_handle.lock().unwrap() = Some(handle);
    }

    /// Stop background polling.
    pub fn stop(&self) {
        if let Some(handle) = self.task_handle.lock().unwrap().take() {
            handle.abort();
        }
    }

    /// Register a listener for capacity updates.
    pub fn on_update<F: Fn(&CapacityResponse) + Send + 'static>(&self, handler: F) {
        self.inner.lock().unwrap().listeners.push(Box::new(handler));
    }

    /// Get cached capacity (None if not yet loaded).
    pub fn get_capacity(&self) -> Option<CapacityResponse> {
        self.inner.lock().unwrap().capacity.clone()
    }

    /// Get cached capacity for a single token.
    pub fn get_token_capacity(&self, symbol: &str) -> Option<TokenCapacity> {
        let guard = self.inner.lock().unwrap();
        guard.capacity.as_ref()?.tokens.iter()
            .find(|t| t.symbol.eq_ignore_ascii_case(symbol))
            .cloned()
    }

    /// Check if cache is warm (has data).
    pub fn is_warm(&self) -> bool {
        self.inner.lock().unwrap().capacity.is_some()
    }

    async fn refresh(&self) {
        let client = reqwest::Client::new();
        if let Ok(res) = client.get(&format!("{}/v1/capacity", self.api_url)).send().await {
            if let Ok(capacity) = res.json::<CapacityResponse>().await {
                // Auto-sync the TokenRegistry
                update_registry_from_capacity(&capacity.tokens);
                let mut guard = self.inner.lock().unwrap();
                guard.capacity = Some(capacity.clone());
                for listener in &guard.listeners {
                    listener(&capacity);
                }
            }
        }
    }
}