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; pub const GITHUB_TTL_SECS: u64 = 60 * 60; const 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
90pub 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, };
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}