use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::ledger::Record;
use crate::model::{Edge, Symbol};
pub const DEFAULT_TTL: u64 = 300;
#[derive(Debug, Default, Serialize, Deserialize)]
struct Meta {
versions: Option<(u64, u64)>,
}
pub fn is_remote(index_root: &str) -> bool {
index_root.contains("://")
}
pub fn dir(index_root: &str) -> Option<PathBuf> {
let home = std::env::var_os("HOME")?;
let key = blake3::hash(index_root.as_bytes()).to_hex();
Some(
PathBuf::from(home)
.join(".cache/comindly/remote")
.join(&key[..16]),
)
}
pub fn read(cache: &Path) -> Option<(Vec<Symbol>, Vec<Edge>, Vec<Record>)> {
let graph = cache.join("_graph");
if !graph.is_dir() {
return None;
}
let (symbols, edges) = crate::index::read_graph_blocking(graph.to_str()?).ok()?;
let records = std::fs::read(cache.join("ledger.json"))
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_default();
Some((symbols, edges, records))
}
pub fn stale(cache: &Path, ttl: Duration) -> bool {
std::fs::metadata(cache.join("checked"))
.and_then(|m| m.modified())
.ok()
.and_then(|t| SystemTime::now().duration_since(t).ok())
.is_none_or(|age| age > ttl)
}
pub fn invalidate(index_root: &str) {
if let Some(d) = dir(index_root) {
let _ = std::fs::remove_file(d.join("checked"));
}
}
pub fn refresh(index_root: &str, cache: &Path) -> Result<bool> {
std::fs::create_dir_all(cache)?;
let lock = cache.join("refresh.lock");
let held = std::fs::metadata(&lock)
.and_then(|m| m.modified())
.ok()
.and_then(|t| SystemTime::now().duration_since(t).ok())
.is_some_and(|age| age < Duration::from_secs(600));
if held {
return Ok(false);
}
std::fs::write(&lock, b"")?;
let out = sync(index_root, cache);
let _ = std::fs::remove_file(&lock);
out
}
fn sync(index_root: &str, cache: &Path) -> Result<bool> {
let remote_graph = format!("{}/_graph", index_root.trim_end_matches('/'));
let meta_path = cache.join("meta.json");
let mut meta: Meta = std::fs::read(&meta_path)
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_default();
let now = crate::index::latest_versions_blocking(&remote_graph).context("remote index")?;
let copied = meta.versions != Some(now) || !cache.join("_graph").is_dir();
if copied {
let (symbols, edges) = crate::index::read_graph_blocking(&remote_graph)?;
let tmp = cache.join("_graph.new");
let _ = std::fs::remove_dir_all(&tmp);
crate::index::write_graph_blocking(tmp.to_str().context("cache path")?, &symbols, &edges)?;
let _ = std::fs::remove_dir_all(cache.join("_graph"));
std::fs::rename(&tmp, cache.join("_graph"))?;
meta.versions = Some(now);
}
let records = crate::ledger::all_blocking(index_root)?;
let tmp = cache.join("ledger.json.new");
std::fs::write(&tmp, serde_json::to_vec(&records)?)?;
std::fs::rename(&tmp, cache.join("ledger.json"))?;
std::fs::write(&meta_path, serde_json::to_vec(&meta)?)?;
std::fs::write(cache.join("checked"), b"")?;
Ok(copied)
}
pub fn refresh_in_background(index_root: &str) {
let Ok(exe) = std::env::current_exe() else {
return;
};
let _ = std::process::Command::new(exe)
.args(["cache", "refresh", "--index-dir", index_root])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mirror_follows_the_remote() {
let base = std::env::temp_dir().join(format!("comindly-cache-{}", std::process::id()));
let remote = base.join("remote");
let cache = base.join("cache");
let root = remote.to_str().unwrap();
let sym = |name: &str| crate::model::Symbol {
id: crate::model::GlobalSymbolId {
scheme: "t".into(),
package_manager: ".".into(),
package: "r".into(),
version: ".".into(),
descriptor: format!("m/{name}()."),
},
name: name.into(),
kind: crate::model::SymbolKind::Function,
language: crate::model::Language::Python,
repo: crate::model::RepoId("r".into()),
file_path: "m.py".into(),
range: crate::model::Range {
start: crate::model::Position { line: 1, column: 0 },
end: crate::model::Position { line: 2, column: 0 },
},
signature: None,
docstring: None,
fingerprint: None,
};
let graph = format!("{root}/_graph");
crate::index::write_graph_blocking(&graph, &[sym("a")], &[]).unwrap();
assert!(stale(&cache, Duration::from_secs(300)));
assert!(refresh(root, &cache).unwrap(), "first sync copies");
assert!(!stale(&cache, Duration::from_secs(300)));
assert!(
!refresh(root, &cache).unwrap(),
"unchanged remote is not copied again"
);
crate::index::write_graph_blocking(&graph, &[sym("a"), sym("b")], &[]).unwrap();
assert!(
refresh(root, &cache).unwrap(),
"a new remote version is copied"
);
let (symbols, _, records) = read(&cache).unwrap();
assert_eq!(symbols.len(), 2);
assert!(records.is_empty());
std::fs::remove_dir_all(&base).ok();
}
}