diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 74c4642..7c6885e 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -201,10 +201,12 @@ fn read_rates_cache(app: &AppHandle) -> Option { } async fn fetch_rates_online() -> Result { - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(15)) - .build() - .map_err(|e| e.to_string())?; + let client = HTTP_CLIENT.get_or_init(|| { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .unwrap_or_default() + }); let body: serde_json::Value = client .get("https://open.er-api.com/v6/latest/USD") @@ -380,6 +382,8 @@ fn valid_date_str(date: &str) -> bool { && parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit())) } +static HTTP_CLIENT: std::sync::OnceLock = std::sync::OnceLock::new(); + #[tauri::command] async fn fetch_historical_rates( app: AppHandle, @@ -394,10 +398,12 @@ async fn fetch_historical_rates( return Ok(cached); } } - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(15)) - .build() - .map_err(|e| e.to_string())?; + let client = HTTP_CLIENT.get_or_init(|| { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .unwrap_or_default() + }); // api.frankfurter.app 301-redirects here now, and the old domain is // flaky over reqwest/rustls-tls — go straight to the stable host let url = format!("https://api.frankfurter.dev/v1/{}?from=USD", date); @@ -425,6 +431,86 @@ async fn fetch_historical_rates( Ok(rates) } +#[tauri::command] +async fn fetch_historical_rates_batch( + app: AppHandle, + dates: Vec, +) -> Result>, String> { + let mut results = std::collections::HashMap::new(); + let mut missing_dates = Vec::new(); + + for date in &dates { + if !valid_date_str(date) { + continue; + } + let cache_path = data_dir(&app).join(format!("rates-{}.json", date)); + if let Ok(raw) = std::fs::read_to_string(&cache_path) { + if let Ok(cached) = serde_json::from_str::>(&raw) + { + results.insert(date.clone(), cached); + continue; + } + } + missing_dates.push(date.clone()); + } + + if missing_dates.is_empty() { + return Ok(results); + } + + let client = HTTP_CLIENT.get_or_init(|| { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .unwrap_or_default() + }); + + let mut futures = Vec::new(); + + for date in missing_dates { + let url = format!("https://api.frankfurter.dev/v1/{}?from=USD", date); + let client_clone = client.clone(); + let app_clone = app.clone(); + + futures.push(async move { + let mut rate_map = None; + if let Ok(resp) = client_clone.get(&url).send().await { + if let Ok(body) = resp.json::().await { + if let Some(rates_obj) = body["rates"].as_object() { + let mut daily_rates = std::collections::HashMap::new(); + daily_rates.insert("USD".to_string(), 1.0_f64); + for (code, val) in rates_obj { + if let Some(v) = val.as_f64() { + if v > 0.0 { + daily_rates.insert(code.to_uppercase(), v); + } + } + } + let cache_path = data_dir(&app_clone).join(format!("rates-{}.json", date)); + if let Ok(serialized) = serde_json::to_string(&daily_rates) { + let _ = crate::write_atomic(&cache_path, &serialized); + } + rate_map = Some((date, daily_rates)); + } + } + } + rate_map + }); + } + + use futures::stream::StreamExt; + let fetched_results = futures::stream::iter(futures) + .buffer_unordered(5) + .collect::>() + .await; + + for (date, rates) in fetched_results.into_iter().flatten() { + results.insert(date, rates); + } + + Ok(results) +} + // ---------- documents in the (possibly custom) data folder #[tauri::command] @@ -747,6 +833,7 @@ fn main() { migrate_data_dir, fetch_rates, fetch_historical_rates, + fetch_historical_rates_batch, fetch_market_data, load_extensions, open_extensions_folder, diff --git a/src/main.ts b/src/main.ts index 6f7eac6..552da7d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,7 +5,7 @@ import { SumEditor } from "./ui/editor"; import { AppData, DocMeta, SettingsData, defaultSettingsData, loadAppData, saveAppData, flushAppData, onAppQuit, loadSettings, saveSettings, - fetchRates, fetchMarketData, fetchHistoricalRates, loadExtensionScripts, openExtensionsFolder, isTauri, + fetchRates, fetchMarketData, fetchHistoricalRatesBatch, loadExtensionScripts, openExtensionsFolder, isTauri, getLaunchFile, onOpenFile, onFileDrop, setDataDir, runBackups, backupDeletedSheet, openBackupsFolder, chooseFolder, dataDirHasDocuments, migrateDataDir, @@ -605,13 +605,9 @@ async function fetchNeededHistoricalRates(text: string): Promise { const datesToFetch = Array.from(dates).filter((date) => !engine.hasHistoricalRates(date)); let fetched = false; if (datesToFetch.length > 0) { - const results = await Promise.all( - datesToFetch.map(async (date) => { - const rates = await fetchHistoricalRates(date); - return { date, rates }; - }) - ); - for (const { date, rates } of results) { + const results = await fetchHistoricalRatesBatch(datesToFetch); + for (const date of datesToFetch) { + const rates = results[date]; if (rates) { engine.setHistoricalRates(date, rates); fetched = true; diff --git a/src/storage.ts b/src/storage.ts index c316eda..833aab1 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -213,6 +213,26 @@ export async function fetchRates(force = false): Promise { // ---------- historical rates +export async function fetchHistoricalRatesBatch(dates: string[]): Promise>> { + if (dates.length === 0) return {}; + try { + if (isTauri()) { + return await invoke>>("fetch_historical_rates_batch", { dates }); + } + const results: Record> = {}; + await Promise.all( + dates.map(async (date) => { + const rates = await fetchHistoricalRates(date); + if (rates) results[date] = rates; + }) + ); + return results; + } catch (e) { + console.warn("fetchHistoricalRatesBatch failed", e); + return {}; + } +} + export async function fetchHistoricalRates(date: string): Promise | null> { try { if (isTauri()) {