use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock, RwLock};
use crate::graph_cache::cache;
use crate::graph_cache::delta::{apply_delta_to_calls, apply_delta_to_deps, refresh_records};
use crate::graph_cache::UnifiedGraph;
use crate::search::cache::{compute_delta, Delta, FileRecord};
#[derive(Clone)]
struct Entry {
graph: Arc<UnifiedGraph>,
records: Arc<Vec<FileRecord>>,
}
type Registry = RwLock<HashMap<PathBuf, Entry>>;
static REGISTRY: OnceLock<Registry> = OnceLock::new();
fn registry() -> &'static Registry {
REGISTRY.get_or_init(|| RwLock::new(HashMap::new()))
}
fn store(key: PathBuf, graph: Arc<UnifiedGraph>, records: Arc<Vec<FileRecord>>) {
registry().write().unwrap().insert(key, Entry { graph, records });
}
pub fn get_or_init(root: &Path) -> std::io::Result<Arc<UnifiedGraph>> {
let key = root_for(root);
let cached = registry().read().unwrap().get(&key).cloned();
if let Some(entry) = cached {
let delta = compute_delta(root, root, entry.records.as_slice());
if !delta.requires_rebuild() {
return Ok(entry.graph);
}
}
static LOAD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = LOAD_LOCK.lock().unwrap();
let cached = registry().read().unwrap().get(&key).cloned();
if let Some(entry) = cached {
let delta = compute_delta(root, root, entry.records.as_slice());
if !delta.requires_rebuild() {
return Ok(entry.graph);
}
if let Some((graph, records)) =
patch_in_memory(&entry.graph, entry.records.as_slice(), root, &key, &delta)
{
let arc = Arc::new(graph);
store(key, arc.clone(), records);
return Ok(arc);
}
}
let (graph, records) = cache::load_or_build_with_records(&key, false)?;
let arc = Arc::new(graph);
store(key, arc.clone(), Arc::new(records));
Ok(arc)
}
fn patch_in_memory(
graph: &UnifiedGraph,
prev_records: &[FileRecord],
root: &Path,
key: &Path,
delta: &Delta,
) -> Option<(UnifiedGraph, Arc<Vec<FileRecord>>)> {
let mut g = graph.clone();
if apply_delta_to_deps(&mut g.deps, root, delta).is_err() {
return None;
}
if let Some(calls) = g.calls.as_mut() {
apply_delta_to_calls(calls, &g.deps, root, delta);
}
let new_records = refresh_records(prev_records.to_vec(), root, delta);
let _ = cache::save(key, &g, &new_records);
Some((g, Arc::new(new_records)))
}
pub fn rebuild(root: &Path) -> std::io::Result<Arc<UnifiedGraph>> {
let key = root_for(root);
let (graph, records) = cache::load_or_build_with_records(&key, true)?;
let arc = Arc::new(graph);
store(key, arc.clone(), Arc::new(records));
Ok(arc)
}
pub fn promote_calls<F>(root: &Path, build: F) -> std::io::Result<Arc<UnifiedGraph>>
where
F: FnOnce(&UnifiedGraph) -> crate::calls::graph::CallGraph,
{
let current = get_or_init(root)?;
if current.calls.is_some() {
return Ok(current);
}
let calls = build(¤t);
let promoted = Arc::new(UnifiedGraph {
deps: current.deps.clone(),
calls: Some(calls),
});
let key = root_for(root);
let records = registry()
.read()
.unwrap()
.get(&key)
.map(|e| e.records.clone())
.unwrap_or_else(|| Arc::new(cache::collect_file_records(&key).unwrap_or_default()));
store(key.clone(), promoted.clone(), records.clone());
let _ = cache::save(&key, &promoted, records.as_slice());
Ok(promoted)
}
#[allow(dead_code)]
pub fn forget(root: &Path) {
let key = root_for(root);
registry().write().unwrap().remove(&key);
}
pub fn root_for(root: &Path) -> PathBuf {
root.canonicalize().unwrap_or_else(|_| root.to_path_buf())
}
#[allow(dead_code)]
pub fn with_root<R, F: FnOnce(&UnifiedGraph) -> R>(root: &Path, f: F) -> std::io::Result<R> {
let g = get_or_init(root)?;
Ok(f(&g))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn write(p: &Path, body: &str) {
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(p, body).unwrap();
}
#[test]
fn get_or_init_revalidates_on_change() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
write(
&root.join("Cargo.toml"),
"[package]\nname = \"reval\"\nversion = \"0.0.0\"\nedition = \"2021\"\n",
);
write(&root.join("src/a.rs"), "pub fn a() {}\n");
forget(root);
let first = get_or_init(root).expect("cold build");
let second = get_or_init(root).expect("warm reuse");
assert!(
Arc::ptr_eq(&first, &second),
"unchanged tree must reuse the cached Arc"
);
let files_before = first.deps.forward.len();
write(&root.join("src/b.rs"), "pub fn b() {}\n");
let third = get_or_init(root).expect("revalidate after add");
assert!(
!Arc::ptr_eq(&first, &third),
"added file must trigger a reload into a fresh Arc"
);
assert!(
third.deps.forward.len() > files_before,
"reloaded graph must include the added file"
);
forget(root);
}
}