From 0dd768af9dd7a72cb86b0bc7e5e769f5f43bbc65 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Fri, 28 Feb 2025 23:40:45 +0100 Subject: [PATCH] feat: implement cache --- crates/forest/src/plan_reconciler.rs | 10 ++++ crates/forest/src/plan_reconciler/cache.rs | 68 ++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 crates/forest/src/plan_reconciler/cache.rs diff --git a/crates/forest/src/plan_reconciler.rs b/crates/forest/src/plan_reconciler.rs index a7d1c33..c13a4d7 100644 --- a/crates/forest/src/plan_reconciler.rs +++ b/crates/forest/src/plan_reconciler.rs @@ -7,6 +7,8 @@ use crate::model::Project; pub mod git; pub mod local; +mod cache; + #[derive(Default)] pub struct PlanReconciler {} @@ -25,11 +27,17 @@ impl PlanReconciler { tracing::debug!("no plan, returning"); return Ok(None); } + let cache = cache::Cache::new(destination); // prepare the plan dir // TODO: We're always deleting, consider some form of caching let plan_dir = destination.join(".forest").join("plan"); if plan_dir.exists() { + if let Some(secs) = cache.is_cache_valid().await? { + tracing::debug!("cache is valid for {} seconds", secs); + return Ok(Some(plan_dir.join("forest.kdl"))); + } + tokio::fs::remove_dir_all(&plan_dir).await?; } tokio::fs::create_dir_all(&plan_dir) @@ -55,6 +63,8 @@ impl PlanReconciler { tracing::info!("reconciled project"); + cache.upsert_cache().await?; + Ok(Some(plan_dir.join("forest.kdl"))) } } diff --git a/crates/forest/src/plan_reconciler/cache.rs b/crates/forest/src/plan_reconciler/cache.rs new file mode 100644 index 0000000..cc723ae --- /dev/null +++ b/crates/forest/src/plan_reconciler/cache.rs @@ -0,0 +1,68 @@ +use std::{ + path::{Path, PathBuf}, + time::UNIX_EPOCH, +}; + +use serde::{Deserialize, Serialize}; +use tokio::io::AsyncWriteExt; + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct CacheFile { + last_update: u64, +} + +pub struct Cache { + path: PathBuf, +} + +impl Cache { + pub fn new(destination: &Path) -> Self { + Self { + path: destination.join(".forest").join("plan.cache.json"), + } + } + + pub async fn is_cache_valid(&self) -> anyhow::Result> { + if !self.path.exists() { + return Ok(None); + } + + if let Ok(cache_config) = std::env::var("FOREST_CACHE").map(|c| c.trim().to_lowercase()) { + if cache_config.eq("no") || cache_config.eq("false") || cache_config.eq("0") { + return Ok(None); + } + } + + let file = tokio::fs::read_to_string(&self.path).await?; + let cache_file: CacheFile = serde_json::from_str(&file)?; + let unix_cache = std::time::Duration::from_secs(cache_file.last_update); + let now = std::time::SystemTime::now().duration_since(UNIX_EPOCH)?; + + let cache_expire = now + .as_secs() + .saturating_sub(std::time::Duration::from_secs(60 * 60 * 8).as_secs()); + + if unix_cache.as_secs() > cache_expire { + return Ok(Some(unix_cache.as_secs().saturating_sub(cache_expire))); + } + + Ok(None) + } + + pub async fn upsert_cache(&self) -> anyhow::Result<()> { + if let Some(parent) = self.path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + + let unix = std::time::SystemTime::now().duration_since(UNIX_EPOCH)?; + let cache_file = CacheFile { + last_update: unix.as_secs(), + }; + let val = serde_json::to_string_pretty(&cache_file)?; + + let mut file = tokio::fs::File::create(&self.path).await?; + file.write_all(val.as_bytes()).await?; + + Ok(()) + } +}