feat: caching

This commit is contained in:
Mark
2026-07-01 13:19:55 +02:00
parent 333eae6816
commit f862be3916
2 changed files with 43 additions and 17 deletions

View File

@@ -1,10 +1,15 @@
use std::collections::{BTreeMap, HashMap}; use std::{
collections::{BTreeMap, HashMap},
time::Instant,
};
use reqwest::Url; use reqwest::Url;
use serde::Deserialize; use serde::Deserialize;
#[derive(Default)] #[derive(Default)]
pub struct DwdCache {} pub struct DwdCache {
dwd_responses: HashMap<Vec<String>, (Instant, Result<Forecast, String>)>,
}
pub type Forecast = HashMap<String, ForecastDatas>; pub type Forecast = HashMap<String, ForecastDatas>;
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -168,7 +173,32 @@ impl ForecastData {
} }
} }
pub async fn forecast(ids: &[&str], cache: &mut DwdCache) -> Result<Forecast, String> { pub async fn forecast<'a>(
ids: Vec<String>,
cache: &'a mut DwdCache,
) -> Result<&'a Forecast, &'a String> {
let now = Instant::now();
cache.dwd_responses.retain(|_, (time, cached)| {
now.saturating_duration_since(*time).as_secs() < if cached.is_ok() { 600 } else { 90 }
});
let v = if cache.dwd_responses.contains_key(&ids) {
// cache is fresh, don't remove it,
// don't fetch new data, or_insert_with will not run.
None
} else {
// there is no cache (never was or it was outdated).
// fetch new data since or_insert_with will run.
Some(forecast_impl(&ids).await)
};
cache
.dwd_responses
.entry(ids)
// closure will run exactly when `v` is `Some`
.or_insert_with(|| (now, v.unwrap()))
.1
.as_ref()
}
async fn forecast_impl(ids: &[String]) -> Result<Forecast, String> {
dbg!(ids); dbg!(ids);
let mut url = let mut url =
Url::parse("https://app-prod-ws.warnwetter.de/v30/stationOverviewExtended").unwrap(); Url::parse("https://app-prod-ws.warnwetter.de/v30/stationOverviewExtended").unwrap();

View File

@@ -196,22 +196,18 @@ async fn weather_overview(q: &str, data: &State, accept: &Accept) -> Page {
let q = q.to_lowercase(); let q = q.to_lowercase();
dwd_stations dwd_stations
.range((q.clone(), 0)..=(q, usize::MAX)) .range((q.clone(), 0)..=(q, usize::MAX))
.map(|(_, station)| station.station.as_str()) .map(|(_, station)| station.station.clone())
.take(WEATHER_MAX_STATIONS) .take(WEATHER_MAX_STATIONS)
.collect() .collect()
} }
LocationString::Station(q) => q LocationString::Station(q) => q.iter().take(WEATHER_MAX_STATIONS).cloned().collect(),
.iter()
.map(|s| s.as_str())
.take(WEATHER_MAX_STATIONS)
.collect(),
LocationString::Coords(lat, lon) => vec![ LocationString::Coords(lat, lon) => vec![
dwd_stations dwd_stations
.values() .values()
.min_by(|a, b| dist2(a, *lat, *lon).total_cmp(&dist2(b, *lat, *lon))) .min_by(|a, b| dist2(a, *lat, *lon).total_cmp(&dist2(b, *lat, *lon)))
.unwrap() .unwrap()
.station .station
.as_str(), .clone(),
], ],
}; };
if stations.is_empty() { if stations.is_empty() {
@@ -220,7 +216,7 @@ async fn weather_overview(q: &str, data: &State, accept: &Accept) -> Page {
"could not find any such station".to_owned(), "could not find any such station".to_owned(),
))); )));
} }
match crate::dwd::forecast(&stations, dwd_cache).await { match crate::dwd::forecast(stations, dwd_cache).await {
Ok(forecast) => { Ok(forecast) => {
fn forecasts( fn forecasts(
forecast: &HashMap<String, ForecastDatas>, forecast: &HashMap<String, ForecastDatas>,
@@ -230,7 +226,7 @@ async fn weather_overview(q: &str, data: &State, accept: &Accept) -> Page {
.flat_map(|(_, forecast)| forecast.forecasts()) .flat_map(|(_, forecast)| forecast.forecasts())
} }
let mut timer = TimeStepper::invalid(); let mut timer = TimeStepper::invalid();
for forecast in forecasts(&forecast) { for forecast in forecasts(forecast) {
timer.add(forecast.time_start(), forecast.time_step()); timer.add(forecast.time_start(), forecast.time_step());
} }
if timer.count() == 0 { if timer.count() == 0 {
@@ -274,7 +270,7 @@ async fn weather_overview(q: &str, data: &State, accept: &Accept) -> Page {
(35.0, "#FF0"), (35.0, "#FF0"),
], ],
|time| { |time| {
avg(forecasts(&forecast) avg(forecasts(forecast)
.flat_map(move |forecast| forecast.temperature(time))) .flat_map(move |forecast| forecast.temperature(time)))
}, },
additional, additional,
@@ -286,7 +282,7 @@ async fn weather_overview(q: &str, data: &State, accept: &Accept) -> Page {
(i(), Some(0.0), None), (i(), Some(0.0), None),
&[(0.0, "#333"), (1.0, "#446"), (5.0, "#22A"), (20.0, "#00F")], &[(0.0, "#333"), (1.0, "#446"), (5.0, "#22A"), (20.0, "#00F")],
|time| { |time| {
avg(forecasts(&forecast) avg(forecasts(forecast)
.flat_map(move |forecast| forecast.precipitation(time))) .flat_map(move |forecast| forecast.precipitation(time)))
}, },
additional, additional,
@@ -298,7 +294,7 @@ async fn weather_overview(q: &str, data: &State, accept: &Accept) -> Page {
(i(), Some(0.0), None), (i(), Some(0.0), None),
&[(0.0, "#333"), (60.0, "#DD0"), (360.0, "#F90")], &[(0.0, "#333"), (60.0, "#DD0"), (360.0, "#F90")],
|time| { |time| {
avg(forecasts(&forecast) avg(forecasts(forecast)
.flat_map(move |forecast| forecast.sunshine(time))) .flat_map(move |forecast| forecast.sunshine(time)))
}, },
additional, additional,
@@ -310,7 +306,7 @@ async fn weather_overview(q: &str, data: &State, accept: &Accept) -> Page {
(i(), Some(0.0), Some(100.0)), (i(), Some(0.0), Some(100.0)),
&[(0.0, "#884"), (40.0, "#333"), (100.0, "#A0A")], &[(0.0, "#884"), (40.0, "#333"), (100.0, "#A0A")],
|time| { |time| {
avg(forecasts(&forecast) avg(forecasts(forecast)
.flat_map(move |forecast| forecast.humidity(time))) .flat_map(move |forecast| forecast.humidity(time)))
}, },
additional, additional,
@@ -329,7 +325,7 @@ async fn weather_overview(q: &str, data: &State, accept: &Accept) -> Page {
} }
} }
} }
Err(e) => Err(Err((Status::InternalServerError, e))), Err(e) => Err(Err((Status::InternalServerError, e.clone()))),
} }
} else { } else {
Err(Err(( Err(Err((