move cache file I/O off the async runtime thread

rates.json, market.json and rates-<date>.json reads/writes ran
straight on the tokio worker calling the Tauri command, blocking the
shared async runtime for the duration. Wrap them in
tauri::async_runtime::spawn_blocking via read_file_async/write_atomic_async.
This commit is contained in:
Horizontal42 2026-08-12 19:57:55 +03:00
parent 2d682448c6
commit dea7314842

View file

@ -195,8 +195,19 @@ fn now_secs() -> u64 {
.as_secs() .as_secs()
} }
fn read_rates_cache(app: &AppHandle) -> Option<RatesCache> { async fn read_file_async(path: PathBuf) -> Option<String> {
let raw = fs::read_to_string(data_dir(app).join("rates.json")).ok()?; tauri::async_runtime::spawn_blocking(move || fs::read_to_string(path).ok())
.await
.ok()
.flatten()
}
async fn write_atomic_async(path: PathBuf, contents: String) {
let _ = tauri::async_runtime::spawn_blocking(move || write_atomic(&path, &contents)).await;
}
async fn read_rates_cache(app: &AppHandle) -> Option<RatesCache> {
let raw = read_file_async(data_dir(app).join("rates.json")).await?;
serde_json::from_str(&raw).ok() serde_json::from_str(&raw).ok()
} }
@ -253,7 +264,7 @@ async fn fetch_rates_online() -> Result<RatesPayload, String> {
#[tauri::command] #[tauri::command]
async fn fetch_rates(app: AppHandle, force: bool) -> Result<RatesPayload, String> { async fn fetch_rates(app: AppHandle, force: bool) -> Result<RatesPayload, String> {
if !force { if !force {
if let Some(cache) = read_rates_cache(&app) { if let Some(cache) = read_rates_cache(&app).await {
if now_secs().saturating_sub(cache.fetched_at) < RATES_TTL_SECS { if now_secs().saturating_sub(cache.fetched_at) < RATES_TTL_SECS {
let mut payload = cache.payload; let mut payload = cache.payload;
payload.fetched_at = cache.fetched_at; payload.fetched_at = cache.fetched_at;
@ -268,11 +279,12 @@ async fn fetch_rates(app: AppHandle, force: bool) -> Result<RatesPayload, String
payload: payload.clone(), payload: payload.clone(),
}; };
if let Ok(raw) = serde_json::to_string(&cache) { if let Ok(raw) = serde_json::to_string(&cache) {
write_atomic(&data_dir(&app).join("rates.json"), &raw).ok(); write_atomic_async(data_dir(&app).join("rates.json"), raw).await;
} }
Ok(payload) Ok(payload)
} }
Err(e) => read_rates_cache(&app) Err(e) => read_rates_cache(&app)
.await
.map(|c| { .map(|c| {
let mut payload = c.payload; let mut payload = c.payload;
payload.fetched_at = c.fetched_at; payload.fetched_at = c.fetched_at;
@ -300,7 +312,7 @@ async fn fetch_market_data(
let cache_path = data_dir(&app).join("market.json"); let cache_path = data_dir(&app).join("market.json");
// try cache first // try cache first
if let Ok(raw) = fs::read_to_string(&cache_path) { if let Some(raw) = read_file_async(cache_path.clone()).await {
if let Ok(cache) = serde_json::from_str::<MarketCache>(&raw) { if let Ok(cache) = serde_json::from_str::<MarketCache>(&raw) {
if now_secs().saturating_sub(cache.fetched_at) < MARKET_TTL_SECS { if now_secs().saturating_sub(cache.fetched_at) < MARKET_TTL_SECS {
let syms: std::collections::HashSet<&str> = let syms: std::collections::HashSet<&str> =
@ -364,7 +376,7 @@ async fn fetch_market_data(
prices: prices.clone(), prices: prices.clone(),
}; };
if let Ok(raw) = serde_json::to_string(&cache) { if let Ok(raw) = serde_json::to_string(&cache) {
write_atomic(&cache_path, &raw).ok(); write_atomic_async(cache_path, raw).await;
} }
} }
@ -410,7 +422,7 @@ async fn fetch_historical_rates(
return Err("invalid date format".into()); return Err("invalid date format".into());
} }
let cache_path = data_dir(&app).join(format!("rates-{}.json", date)); let cache_path = data_dir(&app).join(format!("rates-{}.json", date));
if let Ok(raw) = fs::read_to_string(&cache_path) { if let Some(raw) = read_file_async(cache_path.clone()).await {
if let Ok(cached) = serde_json::from_str::<std::collections::HashMap<String, f64>>(&raw) { if let Ok(cached) = serde_json::from_str::<std::collections::HashMap<String, f64>>(&raw) {
return Ok(cached); return Ok(cached);
} }
@ -434,7 +446,7 @@ async fn fetch_historical_rates(
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let rates = parse_rate_map(body["rates"].as_object().ok_or("no rates in response")?); let rates = parse_rate_map(body["rates"].as_object().ok_or("no rates in response")?);
if let Ok(serialized) = serde_json::to_string(&rates) { if let Ok(serialized) = serde_json::to_string(&rates) {
write_atomic(&cache_path, &serialized).ok(); write_atomic_async(cache_path, serialized).await;
} }
Ok(rates) Ok(rates)
} }
@ -452,7 +464,7 @@ async fn fetch_historical_rates_batch(
continue; continue;
} }
let cache_path = data_dir(&app).join(format!("rates-{}.json", date)); let cache_path = data_dir(&app).join(format!("rates-{}.json", date));
if let Ok(raw) = std::fs::read_to_string(&cache_path) { if let Some(raw) = read_file_async(cache_path).await {
if let Ok(cached) = serde_json::from_str::<std::collections::HashMap<String, f64>>(&raw) if let Ok(cached) = serde_json::from_str::<std::collections::HashMap<String, f64>>(&raw)
{ {
results.insert(date.clone(), cached); results.insert(date.clone(), cached);
@ -488,7 +500,7 @@ async fn fetch_historical_rates_batch(
let daily_rates = parse_rate_map(rates_obj); let daily_rates = parse_rate_map(rates_obj);
let cache_path = data_dir(&app_clone).join(format!("rates-{}.json", date)); let cache_path = data_dir(&app_clone).join(format!("rates-{}.json", date));
if let Ok(serialized) = serde_json::to_string(&daily_rates) { if let Ok(serialized) = serde_json::to_string(&daily_rates) {
let _ = crate::write_atomic(&cache_path, &serialized); write_atomic_async(cache_path, serialized).await;
} }
rate_map = Some((date, daily_rates)); rate_map = Some((date, daily_rates));
} }