use aube_lockfile::LockfileGraph;
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::future::Future;
use std::sync::{Arc, RwLock};
type ActiveIndex = Arc<RwLock<Option<Arc<ChainIndex>>>>;
tokio::task_local! {
static ACTIVE: ActiveIndex;
}
pub async fn scope<F: Future>(future: F) -> F::Output {
ACTIVE.scope(Arc::new(RwLock::new(None)), future).await
}
pub fn scope_current<F: Future>(future: F) -> impl Future<Output = F::Output> {
let active = ACTIVE
.try_with(Arc::clone)
.unwrap_or_else(|_| Arc::new(RwLock::new(None)));
ACTIVE.scope(active, future)
}
#[derive(Debug, Default)]
pub struct ChainIndex {
chains: HashMap<(String, String), Vec<(String, String)>>,
}
impl ChainIndex {
pub fn lookup(&self, name: &str, version: &str) -> Option<&[(String, String)]> {
self.chains
.get(&(name.to_string(), version.to_string()))
.map(Vec::as_slice)
}
pub fn from_graph(graph: &LockfileGraph) -> Self {
let mut chains: HashMap<(String, String), Vec<(String, String)>> = HashMap::new();
let mut queue: VecDeque<(String, Vec<(String, String)>)> = VecDeque::new();
for deps in graph.importers.values() {
for direct in deps {
queue.push_back((direct.dep_path.clone(), Vec::new()));
}
}
while let Some((dep_path, ancestors)) = queue.pop_front() {
let Some(pkg) = graph.packages.get(&dep_path) else {
continue;
};
let alias_key = (pkg.name.clone(), pkg.version.clone());
if chains.contains_key(&alias_key) {
continue;
}
chains.insert(alias_key, ancestors.clone());
if let Some(real) = &pkg.alias_of {
let real_key = (real.clone(), pkg.version.clone());
chains.entry(real_key).or_insert_with(|| ancestors.clone());
}
let mut child_ancestors = ancestors;
child_ancestors.push((pkg.name.clone(), pkg.version.clone()));
push_children(&mut queue, &pkg.dependencies, &child_ancestors);
push_children(&mut queue, &pkg.optional_dependencies, &child_ancestors);
}
Self { chains }
}
}
fn push_children(
queue: &mut VecDeque<(String, Vec<(String, String)>)>,
children: &BTreeMap<String, String>,
ancestors: &[(String, String)],
) {
for (child_name, child_tail) in children {
let child_dep_path = format!("{child_name}@{child_tail}");
queue.push_back((child_dep_path, ancestors.to_vec()));
}
}
pub fn format_chain(ancestors: &[(String, String)], leaf_name: &str, leaf_version: &str) -> String {
if ancestors.is_empty() {
return String::new();
}
let mut s = String::from("chain: ");
for (i, (n, v)) in ancestors.iter().enumerate() {
if i > 0 {
s.push_str(" > ");
}
s.push_str(&format!("{n}@{v}"));
}
s.push_str(&format!(" > {leaf_name}@{leaf_version}"));
s
}
pub fn set_active(graph: &LockfileGraph) {
let idx = Arc::new(ChainIndex::from_graph(graph));
let _ = ACTIVE.try_with(|active| match active.write() {
Ok(mut slot) => *slot = Some(idx),
Err(poisoned) => *poisoned.into_inner() = Some(idx),
});
}
pub fn format_chain_for(name: &str, version: &str) -> String {
ACTIVE
.try_with(|active| {
let guard = match active.read() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
match guard.as_ref().and_then(|idx| idx.lookup(name, version)) {
Some(chain) if !chain.is_empty() => {
format!("\n{}", format_chain(chain, name, version))
}
_ => String::new(),
}
})
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use aube_lockfile::{DirectDep, LockedPackage};
fn pkg(name: &str, version: &str, deps: &[(&str, &str)]) -> (String, LockedPackage) {
let dep_path = format!("{name}@{version}");
let dependencies: BTreeMap<String, String> = deps
.iter()
.map(|(n, v)| (n.to_string(), v.to_string()))
.collect();
(
dep_path.clone(),
LockedPackage {
name: name.to_string(),
version: version.to_string(),
dep_path,
dependencies,
..Default::default()
},
)
}
fn direct(name: &str, version: &str) -> DirectDep {
DirectDep {
name: name.to_string(),
dep_path: format!("{name}@{version}"),
dep_type: aube_lockfile::DepType::Production,
specifier: None,
}
}
#[test]
fn shortest_chain_wins() {
let mut graph = LockfileGraph::default();
graph
.importers
.insert(".".to_string(), vec![direct("a", "1")]);
graph.packages.extend([
pkg("a", "1", &[("b", "1"), ("c", "1")]),
pkg("b", "1", &[("d", "1")]),
pkg("c", "1", &[]),
pkg("d", "1", &[]),
]);
let idx = ChainIndex::from_graph(&graph);
assert_eq!(idx.lookup("a", "1"), Some(&[][..]));
assert_eq!(
idx.lookup("b", "1"),
Some(&[("a".to_string(), "1".to_string())][..])
);
assert_eq!(
idx.lookup("d", "1"),
Some(
&[
("a".to_string(), "1".to_string()),
("b".to_string(), "1".to_string())
][..]
)
);
}
#[test]
fn format_chain_renders_arrow_path() {
let chain = vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string()),
];
assert_eq!(
format_chain(&chain, "leaf", "3"),
"chain: a@1 > b@2 > leaf@3"
);
}
#[test]
fn format_chain_empty_returns_empty() {
assert_eq!(format_chain(&[], "leaf", "3"), "");
}
#[test]
fn aliased_packages_resolve_under_both_alias_and_real_name() {
let mut graph = LockfileGraph::default();
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "h3-v2".to_string(),
dep_path: "h3-v2@2.0.0".to_string(),
dep_type: aube_lockfile::DepType::Production,
specifier: None,
}],
);
graph.packages.insert(
"h3-v2@2.0.0".to_string(),
LockedPackage {
name: "h3-v2".to_string(),
version: "2.0.0".to_string(),
dep_path: "h3-v2@2.0.0".to_string(),
alias_of: Some("h3".to_string()),
..Default::default()
},
);
let idx = ChainIndex::from_graph(&graph);
assert_eq!(idx.lookup("h3-v2", "2.0.0"), Some(&[][..]));
assert_eq!(idx.lookup("h3", "2.0.0"), Some(&[][..]));
}
fn graph_with_ancestor(ancestor: &str) -> LockfileGraph {
let mut graph = LockfileGraph::default();
graph
.importers
.insert(".".to_string(), vec![direct(ancestor, "1")]);
graph
.packages
.extend([pkg(ancestor, "1", &[("leaf", "1")]), pkg("leaf", "1", &[])]);
graph
}
#[tokio::test]
async fn parallel_scopes_keep_their_own_chain_index() {
let barrier = Arc::new(tokio::sync::Barrier::new(2));
let first_barrier = Arc::clone(&barrier);
let second_barrier = Arc::clone(&barrier);
let first = scope(async move {
set_active(&graph_with_ancestor("first"));
first_barrier.wait().await;
format_chain_for("leaf", "1")
});
let second = scope(async move {
set_active(&graph_with_ancestor("second"));
second_barrier.wait().await;
format_chain_for("leaf", "1")
});
let (first, second) = tokio::join!(first, second);
assert_eq!(first, "\nchain: first@1 > leaf@1");
assert_eq!(second, "\nchain: second@1 > leaf@1");
}
#[tokio::test]
async fn scope_current_propagates_chain_index_to_spawned_tasks() {
let chain = scope(async {
set_active(&graph_with_ancestor("parent"));
tokio::spawn(scope_current(async { format_chain_for("leaf", "1") }))
.await
.unwrap()
})
.await;
assert_eq!(chain, "\nchain: parent@1 > leaf@1");
}
}