use tokio::sync::mpsc;
use crate::app::runtime::handlers::common::{HandlerConfig, handle_result};
use crate::logic::add_to_install_list;
use crate::state::{AppState, PackageItem};
pub fn handle_add_to_install_list(
app: &mut AppState,
item: PackageItem,
deps_req_tx: &mpsc::UnboundedSender<(Vec<PackageItem>, crate::state::modal::PreflightAction)>,
files_req_tx: &mpsc::UnboundedSender<(Vec<PackageItem>, crate::state::modal::PreflightAction)>,
services_req_tx: &mpsc::UnboundedSender<(
Vec<PackageItem>,
crate::state::modal::PreflightAction,
)>,
sandbox_req_tx: &mpsc::UnboundedSender<Vec<PackageItem>>,
) {
add_to_install_list(app, item);
if !app.install_list.is_empty() {
app.deps_resolving = true;
let _ = deps_req_tx.send((
app.install_list.clone(),
crate::state::modal::PreflightAction::Install,
));
app.files_resolving = true;
let _ = files_req_tx.send((
app.install_list.clone(),
crate::state::modal::PreflightAction::Install,
));
app.services_resolving = true;
let _ = services_req_tx.send((
app.install_list.clone(),
crate::state::modal::PreflightAction::Install,
));
app.sandbox_resolving = true;
let _ = sandbox_req_tx.send(app.install_list.clone());
}
}
struct DependencyHandlerConfig;
impl HandlerConfig for DependencyHandlerConfig {
type Result = crate::state::modal::DependencyInfo;
fn get_resolving(&self, app: &AppState) -> bool {
app.deps_resolving
}
fn set_resolving(&self, app: &mut AppState, value: bool) {
app.deps_resolving = value; }
fn get_preflight_resolving(&self, app: &AppState) -> bool {
app.preflight_deps_resolving
}
fn set_preflight_resolving(&self, app: &mut AppState, value: bool) {
app.preflight_deps_resolving = value;
}
fn stage_name(&self) -> &'static str {
"dependencies"
}
fn update_cache(&self, app: &mut AppState, results: &[Self::Result]) {
app.install_list_deps = results.to_vec();
}
fn set_cache_dirty(&self, app: &mut AppState) {
app.deps_cache_dirty = true;
}
fn clear_preflight_items(&self, app: &mut AppState) {
app.preflight_deps_items = None;
}
fn sync_to_modal(&self, app: &mut AppState, results: &[Self::Result], was_preflight: bool) {
if let crate::state::Modal::Preflight {
items,
action,
dependency_info,
..
} = &mut app.modal
{
tracing::info!(
"[Runtime] sync_to_modal: action={:?}, results={}, items={}, was_preflight={}",
action,
results.len(),
items.len(),
was_preflight
);
let item_names: std::collections::HashSet<String> =
items.iter().map(|i| i.name.clone()).collect();
for (i, dep) in results.iter().take(3).enumerate() {
tracing::info!(
"[Runtime] sync_to_modal: result[{}] name={}, required_by={:?}",
i,
dep.name,
dep.required_by
);
}
let filtered_deps: Vec<_> = results
.iter()
.filter(|dep| {
let matches = dep
.required_by
.iter()
.any(|req_by| item_names.contains(req_by));
if !matches && results.len() <= 10 {
tracing::debug!(
"[Runtime] sync_to_modal: dep {} required_by={:?} doesn't match items={:?}",
dep.name,
dep.required_by,
item_names
);
}
matches
})
.cloned()
.collect();
let old_deps_len = dependency_info.len();
tracing::info!(
"[Runtime] sync_to_modal: filtered {} deps from {} results (items={:?})",
filtered_deps.len(),
results.len(),
item_names
);
if filtered_deps.is_empty() {
tracing::info!(
"[Runtime] No matching dependencies to sync (results={}, items={:?})",
results.len(),
item_names
);
if matches!(action, crate::state::PreflightAction::Remove) {
tracing::info!(
"[Runtime] Remove action: setting dependency_info to empty (no reverse deps)"
);
*dependency_info = Vec::new();
}
} else {
tracing::info!(
"[Runtime] Syncing {} dependencies to preflight modal (was_preflight={}, modal had {} before)",
filtered_deps.len(),
was_preflight,
old_deps_len
);
*dependency_info = filtered_deps;
tracing::info!(
"[Runtime] Modal dependency_info now has {} entries (was {})",
dependency_info.len(),
old_deps_len
);
}
} else {
tracing::debug!("[Runtime] sync_to_modal: Modal is not Preflight, skipping sync");
}
}
fn log_flag_clear(&self, app: &AppState, was_preflight: bool, cancelled: bool) {
tracing::debug!(
"[Runtime] handle_dependency_result: Clearing flags - was_preflight={}, deps_resolving={}, preflight_deps_resolving={}, cancelled={}",
was_preflight,
self.get_resolving(app),
app.preflight_deps_resolving,
cancelled
);
}
fn is_resolution_complete(&self, app: &AppState, results: &[Self::Result]) -> bool {
if let crate::state::Modal::Preflight { items, action, .. } = &app.modal {
let item_names: std::collections::HashSet<String> =
items.iter().map(|i| i.name.clone()).collect();
if item_names.is_empty() {
return true;
}
if matches!(action, crate::state::PreflightAction::Remove) {
tracing::debug!(
"[Runtime] handle_dependency_result: Remove action - resolution complete with {} results",
results.len()
);
return true;
}
let result_packages: std::collections::HashSet<String> = results
.iter()
.flat_map(|d| d.required_by.iter().cloned())
.collect();
let cache_packages: std::collections::HashSet<String> = app
.install_list_deps
.iter()
.flat_map(|d| d.required_by.iter().cloned())
.collect();
let all_processed = item_names
.iter()
.all(|name| result_packages.contains(name) || cache_packages.contains(name));
if !all_processed {
let missing: Vec<String> = item_names
.iter()
.filter(|name| {
!result_packages.contains(*name) && !cache_packages.contains(*name)
})
.cloned()
.collect();
tracing::debug!(
"[Runtime] handle_dependency_result: Resolution incomplete - missing deps for: {:?}",
missing
);
}
return all_processed;
}
if let Some((ref install_items, ref action)) = app.preflight_deps_items {
let item_names: std::collections::HashSet<String> =
install_items.iter().map(|i| i.name.clone()).collect();
if item_names.is_empty() {
return true;
}
if matches!(action, crate::state::PreflightAction::Remove) {
tracing::debug!(
"[Runtime] handle_dependency_result: Remove action (no modal) - resolution complete with {} results",
results.len()
);
return true;
}
let result_packages: std::collections::HashSet<String> = results
.iter()
.flat_map(|d| d.required_by.iter().cloned())
.collect();
let cache_packages: std::collections::HashSet<String> = app
.install_list_deps
.iter()
.flat_map(|d| d.required_by.iter().cloned())
.collect();
let all_processed = item_names
.iter()
.all(|name| result_packages.contains(name) || cache_packages.contains(name));
if !all_processed {
let missing: Vec<String> = item_names
.iter()
.filter(|name| {
!result_packages.contains(*name) && !cache_packages.contains(*name)
})
.cloned()
.collect();
tracing::debug!(
"[Runtime] handle_dependency_result: Resolution incomplete - missing deps for: {:?}",
missing
);
}
return all_processed;
}
true
}
}
pub fn handle_dependency_result(
app: &mut AppState,
deps: &[crate::state::modal::DependencyInfo],
tick_tx: &mpsc::UnboundedSender<()>,
) {
handle_result(app, deps, tick_tx, &DependencyHandlerConfig);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::Source;
fn new_app() -> AppState {
AppState::default()
}
#[test]
fn handle_add_to_install_list_adds_and_triggers_resolution() {
let mut app = new_app();
app.install_list.clear();
let (deps_tx, mut deps_rx) =
mpsc::unbounded_channel::<(Vec<PackageItem>, crate::state::modal::PreflightAction)>();
let (files_tx, mut files_rx) = mpsc::unbounded_channel();
let (services_tx, mut services_rx) = mpsc::unbounded_channel();
let (sandbox_tx, mut sandbox_rx) = mpsc::unbounded_channel();
let item = PackageItem {
name: "test-package".to_string(),
version: "1.0.0".to_string(),
description: "Test".to_string(),
source: Source::Aur,
popularity: None,
out_of_date: None,
orphaned: false,
};
handle_add_to_install_list(
&mut app,
item,
&deps_tx,
&files_tx,
&services_tx,
&sandbox_tx,
);
assert_eq!(app.install_list.len(), 1);
assert_eq!(app.install_list[0].name, "test-package");
assert!(app.deps_resolving);
assert!(app.files_resolving);
assert!(app.services_resolving);
assert!(app.sandbox_resolving);
let (items, action) = deps_rx.try_recv().expect("deps request should be sent");
assert_eq!(items.len(), 1);
assert!(matches!(
action,
crate::state::modal::PreflightAction::Install
));
assert!(files_rx.try_recv().is_ok());
assert!(services_rx.try_recv().is_ok());
assert!(sandbox_rx.try_recv().is_ok());
}
#[test]
fn handle_dependency_result_updates_cache() {
let mut app = new_app();
app.deps_resolving = true;
app.preflight_deps_resolving = false;
app.preflight_cancelled
.store(false, std::sync::atomic::Ordering::Relaxed);
let (tick_tx, _tick_rx) = mpsc::unbounded_channel();
let deps = vec![crate::state::modal::DependencyInfo {
name: "dep-package".to_string(),
version: "1.0.0".to_string(),
status: crate::state::modal::DependencyStatus::ToInstall,
source: crate::state::modal::DependencySource::Official {
repo: "extra".to_string(),
},
required_by: vec!["test-package".to_string()],
depends_on: Vec::new(),
is_core: false,
is_system: false,
}];
handle_dependency_result(&mut app, &deps, &tick_tx);
assert_eq!(app.install_list_deps.len(), 1);
assert!(!app.deps_resolving);
assert!(!app.preflight_deps_resolving);
assert!(app.deps_cache_dirty);
}
#[test]
fn handle_dependency_result_respects_cancellation() {
let mut app = new_app();
app.preflight_deps_resolving = true;
app.preflight_cancelled
.store(true, std::sync::atomic::Ordering::Relaxed);
app.install_list_deps = vec![];
let (tick_tx, _tick_rx) = mpsc::unbounded_channel();
let deps = vec![crate::state::modal::DependencyInfo {
name: "dep-package".to_string(),
version: "1.0.0".to_string(),
status: crate::state::modal::DependencyStatus::ToInstall,
source: crate::state::modal::DependencySource::Official {
repo: "extra".to_string(),
},
required_by: vec!["test-package".to_string()],
depends_on: Vec::new(),
is_core: false,
is_system: false,
}];
handle_dependency_result(&mut app, &deps, &tick_tx);
assert_eq!(app.install_list_deps.len(), 0);
assert!(!app.deps_resolving);
assert!(!app.preflight_deps_resolving);
}
}