mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
batch historical rates IPC calls with a pooled http client
This commit is contained in:
parent
bba294c6d5
commit
55d8401a44
3 changed files with 119 additions and 16 deletions
|
|
@ -201,10 +201,12 @@ fn read_rates_cache(app: &AppHandle) -> Option<RatesCache> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_rates_online() -> Result<RatesPayload, String> {
|
async fn fetch_rates_online() -> Result<RatesPayload, String> {
|
||||||
let client = reqwest::Client::builder()
|
let client = HTTP_CLIENT.get_or_init(|| {
|
||||||
|
reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(15))
|
.timeout(std::time::Duration::from_secs(15))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
|
||||||
let body: serde_json::Value = client
|
let body: serde_json::Value = client
|
||||||
.get("https://open.er-api.com/v6/latest/USD")
|
.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()))
|
&& parts.iter().all(|p| p.chars().all(|c| c.is_ascii_digit()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static HTTP_CLIENT: std::sync::OnceLock<reqwest::Client> = std::sync::OnceLock::new();
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn fetch_historical_rates(
|
async fn fetch_historical_rates(
|
||||||
app: AppHandle,
|
app: AppHandle,
|
||||||
|
|
@ -394,10 +398,12 @@ async fn fetch_historical_rates(
|
||||||
return Ok(cached);
|
return Ok(cached);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let client = reqwest::Client::builder()
|
let client = HTTP_CLIENT.get_or_init(|| {
|
||||||
|
reqwest::Client::builder()
|
||||||
.timeout(std::time::Duration::from_secs(15))
|
.timeout(std::time::Duration::from_secs(15))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
// api.frankfurter.app 301-redirects here now, and the old domain is
|
// api.frankfurter.app 301-redirects here now, and the old domain is
|
||||||
// flaky over reqwest/rustls-tls — go straight to the stable host
|
// flaky over reqwest/rustls-tls — go straight to the stable host
|
||||||
let url = format!("https://api.frankfurter.dev/v1/{}?from=USD", date);
|
let url = format!("https://api.frankfurter.dev/v1/{}?from=USD", date);
|
||||||
|
|
@ -425,6 +431,86 @@ async fn fetch_historical_rates(
|
||||||
Ok(rates)
|
Ok(rates)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn fetch_historical_rates_batch(
|
||||||
|
app: AppHandle,
|
||||||
|
dates: Vec<String>,
|
||||||
|
) -> Result<std::collections::HashMap<String, std::collections::HashMap<String, f64>>, 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::<std::collections::HashMap<String, f64>>(&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::<serde_json::Value>().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::<Vec<_>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
for (date, rates) in fetched_results.into_iter().flatten() {
|
||||||
|
results.insert(date, rates);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- documents in the (possibly custom) data folder
|
// ---------- documents in the (possibly custom) data folder
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|
@ -747,6 +833,7 @@ fn main() {
|
||||||
migrate_data_dir,
|
migrate_data_dir,
|
||||||
fetch_rates,
|
fetch_rates,
|
||||||
fetch_historical_rates,
|
fetch_historical_rates,
|
||||||
|
fetch_historical_rates_batch,
|
||||||
fetch_market_data,
|
fetch_market_data,
|
||||||
load_extensions,
|
load_extensions,
|
||||||
open_extensions_folder,
|
open_extensions_folder,
|
||||||
|
|
|
||||||
12
src/main.ts
12
src/main.ts
|
|
@ -5,7 +5,7 @@ import { SumEditor } from "./ui/editor";
|
||||||
import {
|
import {
|
||||||
AppData, DocMeta, SettingsData, defaultSettingsData,
|
AppData, DocMeta, SettingsData, defaultSettingsData,
|
||||||
loadAppData, saveAppData, flushAppData, onAppQuit, loadSettings, saveSettings,
|
loadAppData, saveAppData, flushAppData, onAppQuit, loadSettings, saveSettings,
|
||||||
fetchRates, fetchMarketData, fetchHistoricalRates, loadExtensionScripts, openExtensionsFolder, isTauri,
|
fetchRates, fetchMarketData, fetchHistoricalRatesBatch, loadExtensionScripts, openExtensionsFolder, isTauri,
|
||||||
getLaunchFile, onOpenFile, onFileDrop,
|
getLaunchFile, onOpenFile, onFileDrop,
|
||||||
setDataDir, runBackups, backupDeletedSheet, openBackupsFolder,
|
setDataDir, runBackups, backupDeletedSheet, openBackupsFolder,
|
||||||
chooseFolder, dataDirHasDocuments, migrateDataDir,
|
chooseFolder, dataDirHasDocuments, migrateDataDir,
|
||||||
|
|
@ -605,13 +605,9 @@ async function fetchNeededHistoricalRates(text: string): Promise<void> {
|
||||||
const datesToFetch = Array.from(dates).filter((date) => !engine.hasHistoricalRates(date));
|
const datesToFetch = Array.from(dates).filter((date) => !engine.hasHistoricalRates(date));
|
||||||
let fetched = false;
|
let fetched = false;
|
||||||
if (datesToFetch.length > 0) {
|
if (datesToFetch.length > 0) {
|
||||||
const results = await Promise.all(
|
const results = await fetchHistoricalRatesBatch(datesToFetch);
|
||||||
datesToFetch.map(async (date) => {
|
for (const date of datesToFetch) {
|
||||||
const rates = await fetchHistoricalRates(date);
|
const rates = results[date];
|
||||||
return { date, rates };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
for (const { date, rates } of results) {
|
|
||||||
if (rates) {
|
if (rates) {
|
||||||
engine.setHistoricalRates(date, rates);
|
engine.setHistoricalRates(date, rates);
|
||||||
fetched = true;
|
fetched = true;
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,26 @@ export async function fetchRates(force = false): Promise<RatesPayload | null> {
|
||||||
|
|
||||||
// ---------- historical rates
|
// ---------- historical rates
|
||||||
|
|
||||||
|
export async function fetchHistoricalRatesBatch(dates: string[]): Promise<Record<string, Record<string, number>>> {
|
||||||
|
if (dates.length === 0) return {};
|
||||||
|
try {
|
||||||
|
if (isTauri()) {
|
||||||
|
return await invoke<Record<string, Record<string, number>>>("fetch_historical_rates_batch", { dates });
|
||||||
|
}
|
||||||
|
const results: Record<string, Record<string, number>> = {};
|
||||||
|
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<Record<string, number> | null> {
|
export async function fetchHistoricalRates(date: string): Promise<Record<string, number> | null> {
|
||||||
try {
|
try {
|
||||||
if (isTauri()) {
|
if (isTauri()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue