diff --git a/backend/Cargo.lock b/backend/Cargo.lock index a580744..6d6f640 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -304,6 +304,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.48.5", ] @@ -1172,6 +1173,7 @@ name = "powertools" version = "1.5.0-ng1" dependencies = [ "async-trait", + "chrono", "clap", "community_settings_core", "libc", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d81c417..06cdd2e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -28,12 +28,20 @@ simplelog = "0.12" # limits & driver functionality limits_core = { version = "3", path = "./limits_core" } -community_settings_core = { version = "0.1", path = "./community_settings_core" } regex = "1" + +# steam deck libs smokepatio = { version = "0.1", features = [ "std" ], path = "../../smokepatio" } libc = "0.2" + +# online settings +community_settings_core = { version = "0.1", path = "./community_settings_core" } +chrono = { version = "0.4", features = [ "serde" ] } + +# hardware enablement #libryzenadj = { version = "0.14", path = "../../libryzenadj-rs-14" } libryzenadj = { version = "0.13" } + # ureq's tls feature does not like musl targets ureq = { version = "2", features = ["json", "gzip", "brotli", "charset"], default-features = false, optional = true } diff --git a/backend/src/api/web.rs b/backend/src/api/web.rs index 965be44..c5f2d2f 100644 --- a/backend/src/api/web.rs +++ b/backend/src/api/web.rs @@ -3,11 +3,26 @@ use std::sync::{Arc, Mutex, RwLock}; use usdpl_back::core::serdes::Primitive; use usdpl_back::AsyncCallable; +use chrono::{offset::Utc, DateTime}; +use serde::{Deserialize, Serialize}; + use super::handler::{ApiMessage, GeneralMessage}; const BASE_URL_FALLBACK: &'static str = "https://powertools.ngni.us"; static BASE_URL: RwLock> = RwLock::new(None); +const MAX_CACHE_DURATION: std::time::Duration = + std::time::Duration::from_secs(60 * 60 * 24 * 7 /* 7 days */); + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CachedData { + data: T, + updated: DateTime, +} + +type StoreCache = + std::collections::HashMap>>; + pub fn set_base_url(base_url: String) { *BASE_URL .write() @@ -34,35 +49,124 @@ fn url_upload_config() -> String { format!("{}/api/setting", get_base_url()) } +fn cache_path() -> std::path::PathBuf { + crate::utility::settings_dir().join(crate::consts::WEB_SETTINGS_CACHE) +} + +fn load_cache() -> StoreCache { + let path = cache_path(); + let file = match std::fs::File::open(&path) { + Ok(f) => f, + Err(e) => { + log::warn!("Failed to open store cache {}: {}", path.display(), e); + return StoreCache::default(); + } + }; + let mut file = std::io::BufReader::new(file); + match ron::de::from_reader(&mut file) { + Ok(cache) => cache, + Err(e) => { + log::error!("Failed to parse store cache {}: {}", path.display(), e); + return StoreCache::default(); + } + } +} + +fn save_cache(cache: &StoreCache) { + let path = cache_path(); + let file = match std::fs::File::create(&path) { + Ok(f) => f, + Err(e) => { + log::warn!("Failed to create store cache {}: {}", path.display(), e); + return; + } + }; + let mut file = std::io::BufWriter::new(file); + if let Err(e) = + ron::ser::to_writer_pretty(&mut file, cache, crate::utility::ron_pretty_config()) + { + log::error!("Failed to parse store cache {}: {}", path.display(), e); + } +} + +fn get_maybe_cached(steam_app_id: u32) -> Vec { + let mut cache = load_cache(); + if let Some(cached_result) = cache.get(&steam_app_id) { + if cached_result.updated < (Utc::now() - MAX_CACHE_DURATION) { + // cache needs update + if let Ok(result) = search_by_app_id_online(steam_app_id) { + cache.insert( + steam_app_id, + CachedData { + data: result.clone(), + updated: Utc::now(), + }, + ); + save_cache(&cache); + result + } else { + // if all else fails, out of date results are better than no results + cached_result.data.to_owned() + } + } else { + // cache is ok, use it + cached_result.data.to_owned() + } + } else { + if let Ok(result) = search_by_app_id_online(steam_app_id) { + cache.insert( + steam_app_id, + CachedData { + data: result.clone(), + updated: Utc::now(), + }, + ); + save_cache(&cache); + result + } else { + Vec::with_capacity(0) + } + } +} + +fn search_by_app_id_online( + steam_app_id: u32, +) -> std::io::Result> { + let req_url = url_search_by_app_id(steam_app_id); + match ureq::get(&req_url).call() { + Ok(response) => { + let json_res: std::io::Result> = + response.into_json(); + match json_res { + Ok(search_results) => Ok(search_results), + Err(e) => { + log::error!("Cannot parse response from `{}`: {}", req_url, e); + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + } + } + } + Err(e) => { + log::warn!("Cannot get search results from `{}`: {}", req_url, e); + Err(std::io::Error::new( + std::io::ErrorKind::ConnectionAborted, + e, + )) + } + } +} + /// Get search results web method pub fn search_by_app_id() -> impl AsyncCallable { let getter = move || { move |steam_app_id: u32| { - let req_url = url_search_by_app_id(steam_app_id); - match ureq::get(&req_url).call() { - Ok(response) => { - let json_res: std::io::Result> = - response.into_json(); - match json_res { - Ok(search_results) => { - // search results may be quite large, so let's do the JSON string conversion in the background (blocking) thread - match serde_json::to_string(&search_results) { - Err(e) => log::error!( - "Cannot convert search results from `{}` to JSON: {}", - req_url, - e - ), - Ok(s) => return s, - } - } - Err(e) => { - log::error!("Cannot parse response from `{}`: {}", req_url, e) - } - } + let search_results = get_maybe_cached(steam_app_id); + match serde_json::to_string(&search_results) { + Err(e) => { + log::error!("Cannot convert search results to JSON: {}", e); + "[]".to_owned() } - Err(e) => log::warn!("Cannot get search results from `{}`: {}", req_url, e), + Ok(s) => s, } - "[]".to_owned() } }; super::async_utils::AsyncIsh { diff --git a/backend/src/consts.rs b/backend/src/consts.rs index 502f6ce..01707e0 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -10,4 +10,6 @@ pub const DEFAULT_SETTINGS_VARIANT_NAME: &str = "Primary"; pub const LIMITS_FILE: &str = "limits_cache.ron"; pub const LIMITS_OVERRIDE_FILE: &str = "limits_override.ron"; +pub const WEB_SETTINGS_CACHE: &str = "store_cache.ron"; + pub const MESSAGE_SEEN_ID_FILE: &str = "seen_message.bin"; diff --git a/backend/src/utility.rs b/backend/src/utility.rs index 2aa20f4..d9ec7b1 100644 --- a/backend/src/utility.rs +++ b/backend/src/utility.rs @@ -174,6 +174,7 @@ mod generate { ); let savefile = crate::persist::FileJson { version: 0, + app_id: 0, name: crate::consts::DEFAULT_SETTINGS_NAME.to_owned(), variants: mini_variants, };