use std::{fmt::Display, future::Future, time::Duration}; use cache::AppCache; use serde::{Serialize, de::DeserializeOwned}; use crate::{bare::GitBare, graphql::GraphqlContext}; const KEY_SEPARATOR: &str = ":"; pub fn cache_key(namespace: &str, parts: &[&str]) -> String { let mut segments: Vec<&str> = vec![namespace]; for part in parts { if !part.is_empty() { segments.push(part); } } segments.join(KEY_SEPARATOR) } pub fn cache_key_with_revision( namespace: &str, revision: impl Display, parts: &[&str], ) -> String { let mut key = cache_key(namespace, parts); key.push_str(KEY_SEPARATOR); key.push_str(&revision.to_string()); key } fn path_hash(repo: &GitBare) -> String { let path_str = repo.bare_dir.to_string_lossy(); let hash = simple_hash(&path_str); hash[..8].to_string() } fn simple_hash(s: &str) -> String { let mut h: u64 = 0; for b in s.bytes() { h = h.wrapping_mul(31).wrapping_add(b as u64); } format!("{:016x}", h) } pub async fn repo_revision(ctx: &GraphqlContext) -> String { let repo = ctx.repo.clone(); let result = tokio::task::spawn_blocking(move || { repo.git_command_stdout(vec![ "rev-parse".to_string(), "HEAD".to_string(), ]) }) .await; match result { Ok(Ok(oid)) => oid.trim().to_string(), _ => "unknown".to_string(), } } pub fn mutable_cache_key( ctx: &GraphqlContext, namespace: &str, parts: &[&str], revision: &str, ) -> String { let ph = path_hash(&ctx.repo); let mut all_parts: Vec<&str> = vec![&ph]; all_parts.extend_from_slice(parts); cache_key_with_revision(namespace, revision, &all_parts) } pub async fn cached_json( cache: &AppCache, key: &str, _ttl: Duration, build: F, ) -> anyhow::Result where T: Serialize + DeserializeOwned, F: FnOnce() -> Fut, Fut: Future>, { if let Ok(Some(cached)) = cache.get::(key).await { return Ok(cached); } let value = build().await?; cache.set(key, &value).await.ok(); Ok(value) } pub const IMMUTABLE_TTL: Duration = Duration::from_secs(86400); pub const MUTABLE_TTL: Duration = Duration::from_secs(300);