use anyhow::Result;
use lru::LruCache;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::sync::{Arc, LazyLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::fs;
use tokio::sync::RwLock;
const CACHE_TTL: Duration = Duration::from_secs(86400); const PACKAGE_INFO_TTL: Duration = Duration::from_secs(43200); const SEARCH_TTL: Duration = Duration::from_secs(7200); const DEPENDENCY_RESOLVE_TTL: Duration = Duration::from_secs(604800);
type MemoryCacheType = LazyLock<Arc<RwLock<LruCache<String, (JsonValue, u64)>>>>;
static MEMORY_CACHE: MemoryCacheType = LazyLock::new(|| {
Arc::new(RwLock::new(LruCache::new(
NonZeroUsize::new(10000).unwrap(),
)))
});
use crate::core::cache_utils::{CacheEntry, get_cache_dir, get_cache_file_path};
async fn load_from_cache(cache_type: &str, key: &str) -> Option<JsonValue> {
let cache_key = format!("{cache_type}:{key}");
{
let cache = MEMORY_CACHE.read().await;
if let Some((value, timestamp)) = cache.peek(&cache_key) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let ttl = match cache_type {
"package_info" => PACKAGE_INFO_TTL.as_secs(),
"search" => SEARCH_TTL.as_secs(),
"dependency_resolution" => DEPENDENCY_RESOLVE_TTL.as_secs(),
_ => CACHE_TTL.as_secs(),
};
if now - timestamp <= ttl {
return Some(value.clone());
}
}
}
let file_path = get_cache_file_path(cache_type, key);
match fs::read_to_string(&file_path).await {
Ok(content) => {
match serde_json::from_str::<CacheEntry>(&content) {
Ok(entry) => {
if entry.is_expired() {
tokio::spawn(async move {
fs::remove_file(&file_path).await.ok();
});
None
} else {
{
let mut cache = MEMORY_CACHE.write().await;
cache.put(cache_key, (entry.data.clone(), entry.timestamp));
}
Some(entry.data)
}
}
Err(_) => None,
}
}
Err(_) => None,
}
}
async fn save_to_cache(
cache_type: &str,
key: &str,
value: &JsonValue,
ttl: Duration,
) -> Result<()> {
let cache_key = format!("{cache_type}:{key}");
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
{
let mut cache = MEMORY_CACHE.write().await;
cache.put(cache_key, (value.clone(), timestamp));
}
let cache_dir = get_cache_dir().join(cache_type);
let file_path = get_cache_file_path(cache_type, key);
let entry = CacheEntry::new(value.clone(), ttl);
tokio::spawn(async move {
if let Err(e) = fs::create_dir_all(&cache_dir).await {
eprintln!("Failed to create cache dir: {e}");
return;
}
if let Ok(content) = serde_json::to_string(&entry) {
if let Err(e) = fs::write(&file_path, content).await {
eprintln!("Failed to write cache file: {e}");
}
}
});
Ok(())
}
pub async fn cache_get_meta(key: &str) -> Option<JsonValue> {
load_from_cache("meta", key).await
}
pub async fn cache_set_meta(key: &str, val: JsonValue) {
save_to_cache("meta", key, &val, CACHE_TTL).await.ok();
}
pub async fn cache_get_package_info(key: &str) -> Option<JsonValue> {
load_from_cache("package_info", key).await
}
pub async fn cache_set_package_info(key: &str, val: JsonValue) {
save_to_cache("package_info", key, &val, PACKAGE_INFO_TTL)
.await
.ok();
}
pub async fn cache_get_search(key: &str) -> Option<JsonValue> {
load_from_cache("search", key).await
}
pub async fn cache_set_search(key: &str, val: JsonValue) {
save_to_cache("search", key, &val, SEARCH_TTL).await.ok();
}
pub async fn cache_get_multiple_package_info(keys: &[String]) -> HashMap<String, JsonValue> {
let mut results = HashMap::new();
let futures: Vec<_> = keys
.iter()
.map(|key| {
let key_clone = key.clone();
async move {
load_from_cache("package_info", &key_clone)
.await
.map(|value| (key_clone, value))
}
})
.collect();
let cache_results = futures::future::join_all(futures).await;
for (key, value) in cache_results.into_iter().flatten() {
results.insert(key, value);
}
results
}
pub async fn cache_set_multiple_package_info<S: ::std::hash::BuildHasher>(
data: HashMap<String, JsonValue, S>,
) {
let futures: Vec<_> = data
.into_iter()
.map(|(key, value)| async move {
save_to_cache("package_info", &key, &value, PACKAGE_INFO_TTL)
.await
.ok();
})
.collect();
futures::future::join_all(futures).await;
}
pub async fn cache_get_dependency_resolution(key: &str) -> Option<JsonValue> {
load_from_cache("dependency_resolution", key).await
}
pub async fn cache_set_dependency_resolution(key: &str, val: JsonValue) {
save_to_cache("dependency_resolution", key, &val, DEPENDENCY_RESOLVE_TTL)
.await
.ok();
}
pub async fn clear_cache() -> Result<()> {
let cache_dir = get_cache_dir();
if cache_dir.exists() {
fs::remove_dir_all(&cache_dir).await?;
}
Ok(())
}
pub async fn clear_cache_type(cache_type: &str) -> Result<()> {
let cache_dir = get_cache_dir().join(cache_type);
if cache_dir.exists() {
fs::remove_dir_all(&cache_dir).await?;
}
Ok(())
}
pub async fn get_cache_stats() -> Result<HashMap<String, usize>> {
let mut stats = HashMap::new();
let cache_dir = get_cache_dir();
if !cache_dir.exists() {
return Ok(stats);
}
let cache_types = ["meta", "package_info", "search"];
for cache_type in &cache_types {
let type_dir = cache_dir.join(cache_type);
if type_dir.exists() {
match fs::read_dir(&type_dir).await {
Ok(mut entries) => {
let mut count = 0;
while let Ok(Some(_)) = entries.next_entry().await {
count += 1;
}
stats.insert((*cache_type).to_string(), count);
}
Err(_) => {
stats.insert((*cache_type).to_string(), 0);
}
}
} else {
stats.insert((*cache_type).to_string(), 0);
}
}
Ok(stats)
}