Skip to main content

xtui/
meta_cache.rs

1use anyhow::Result;
2use redb::{Database, TableDefinition};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::Path;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8pub const CRATES_IO_TTL_SECS: u64 = 60 * 60 * 24; // 24h
9pub const GITHUB_TTL_SECS: u64 = 60 * 60; // 1h
10
11const CRATES_IO_TABLE: TableDefinition<&str, &str> = TableDefinition::new("crates_io");
12const GITHUB_TABLE: TableDefinition<&str, &str> = TableDefinition::new("github");
13const MAP_KEY: &str = "map";
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct CachedEntry {
17    pub crates_io_latest: Option<String>,
18    pub github_url: Option<String>,
19    pub versions_behind: Option<u32>,
20    pub fetched_at: u64,
21}
22
23impl CachedEntry {
24    pub fn is_stale(&self, ttl_secs: u64) -> bool {
25        let now = SystemTime::now()
26            .duration_since(UNIX_EPOCH)
27            .unwrap_or_default()
28            .as_secs();
29        now.saturating_sub(self.fetched_at) > ttl_secs
30    }
31}
32
33pub struct RedbCache {
34    db: Database,
35}
36
37impl RedbCache {
38    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
39        let db = Database::create(path)?;
40        let tx = db.begin_write()?;
41        tx.open_table(CRATES_IO_TABLE)?;
42        tx.open_table(GITHUB_TABLE)?;
43        tx.commit()?;
44        Ok(Self { db })
45    }
46
47    pub fn get_crates_io_map(&self) -> Result<HashMap<String, CachedEntry>> {
48        let tx = self.db.begin_read()?;
49        let table = tx.open_table(CRATES_IO_TABLE)?;
50        let json = table.get(MAP_KEY)?.map(|v| v.value().to_owned());
51        match json {
52            Some(s) => Ok(serde_json::from_str(&s)?),
53            None => Ok(HashMap::new()),
54        }
55    }
56
57    pub fn set_crates_io_map(&self, map: &HashMap<String, CachedEntry>) -> Result<()> {
58        let json = serde_json::to_string(map)?;
59        let tx = self.db.begin_write()?;
60        {
61            let mut table = tx.open_table(CRATES_IO_TABLE)?;
62            table.insert(MAP_KEY, json.as_str())?;
63        }
64        tx.commit()?;
65        Ok(())
66    }
67
68    pub fn get_github_map(&self) -> Result<HashMap<String, CachedEntry>> {
69        let tx = self.db.begin_read()?;
70        let table = tx.open_table(GITHUB_TABLE)?;
71        let json = table.get(MAP_KEY)?.map(|v| v.value().to_owned());
72        match json {
73            Some(s) => Ok(serde_json::from_str(&s)?),
74            None => Ok(HashMap::new()),
75        }
76    }
77
78    pub fn set_github_map(&self, map: &HashMap<String, CachedEntry>) -> Result<()> {
79        let json = serde_json::to_string(map)?;
80        let tx = self.db.begin_write()?;
81        {
82            let mut table = tx.open_table(GITHUB_TABLE)?;
83            table.insert(MAP_KEY, json.as_str())?;
84        }
85        tx.commit()?;
86        Ok(())
87    }
88}
89
90/// Returns the path to the global metadata cache file.
91pub fn cache_path() -> std::path::PathBuf {
92    dirs::config_dir()
93        .unwrap_or_else(|| std::path::PathBuf::from("."))
94        .join("xtui")
95        .join("meta.redb")
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use std::collections::HashMap;
102
103    #[test]
104    fn redb_cache_roundtrip() {
105        let dir = std::env::temp_dir().join("xtui-test-cache");
106        std::fs::create_dir_all(&dir).unwrap();
107        let cache = RedbCache::open(dir.join("test.redb")).unwrap();
108        let mut map = HashMap::new();
109        map.insert(
110            "foo".to_string(),
111            CachedEntry {
112                crates_io_latest: Some("2.0.0".into()),
113                github_url: None,
114                versions_behind: None,
115                fetched_at: 0,
116            },
117        );
118        cache.set_crates_io_map(&map).unwrap();
119        let loaded = cache.get_crates_io_map().unwrap();
120        assert_eq!(
121            loaded.get("foo").unwrap().crates_io_latest.as_deref(),
122            Some("2.0.0")
123        );
124        std::fs::remove_dir_all(&dir).unwrap();
125    }
126
127    #[test]
128    fn stale_entry_detected() {
129        let entry = CachedEntry {
130            crates_io_latest: None,
131            github_url: None,
132            versions_behind: None,
133            fetched_at: 0, // epoch — always stale
134        };
135        assert!(entry.is_stale(CRATES_IO_TTL_SECS));
136    }
137
138    #[test]
139    fn fresh_entry_not_stale() {
140        let now = SystemTime::now()
141            .duration_since(UNIX_EPOCH)
142            .unwrap()
143            .as_secs();
144        let entry = CachedEntry {
145            crates_io_latest: None,
146            github_url: None,
147            versions_behind: None,
148            fetched_at: now,
149        };
150        assert!(!entry.is_stale(CRATES_IO_TTL_SECS));
151    }
152}