use crate::adapter::net::behavior::fold::NodeId;
use crate::adapter::net::behavior::sensing::{
classify_branch, BranchViability, BranchView, ConsumerLatencyBudget,
};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SensedCandidates {
pub viable: Vec<NodeId>,
pub potential: Vec<NodeId>,
pub non_viable: Vec<NodeId>,
}
impl SensedCandidates {
pub fn selected_provider(&self) -> Option<NodeId> {
self.viable.first().copied()
}
}
pub fn project_sensed_candidates(
branches: &[BranchView],
budget: &ConsumerLatencyBudget,
) -> SensedCandidates {
let mut ranked: Vec<(std::time::Duration, NodeId)> = Vec::new();
let mut delta = SensedCandidates::default();
for branch in branches {
match classify_branch(branch, budget) {
BranchViability::Viable(cost) => ranked.push((cost, branch.provider)),
BranchViability::Potential => delta.potential.push(branch.provider),
BranchViability::NonViable => delta.non_viable.push(branch.provider),
}
}
ranked.sort();
delta.viable = ranked.into_iter().map(|(_, id)| id).collect();
delta.potential.sort_unstable();
delta.non_viable.sort_unstable();
delta
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::adapter::net::behavior::sensing::ProjectedReadiness;
use super::*;
fn ms(v: u64) -> Duration {
Duration::from_millis(v)
}
fn branch(
provider: NodeId,
projection: ProjectedReadiness,
route_ms: u64,
start_ms: u64,
) -> BranchView {
BranchView {
provider,
projection,
estimated_start: Some(ms(start_ms)),
route_estimate: ms(route_ms),
}
}
#[test]
fn classifies_and_ranks_by_the_aggregates_own_economics() {
let budget = ConsumerLatencyBudget {
end_to_end_within: Some(ms(500)),
};
let branches = [
branch(5, ProjectedReadiness::Ready, 200, 100),
branch(9, ProjectedReadiness::Ready, 100, 50),
branch(2, ProjectedReadiness::Ready, 600, 100),
branch(7, ProjectedReadiness::Unknown, 10, 10),
branch(3, ProjectedReadiness::NotReady, 10, 10),
];
let delta = project_sensed_candidates(&branches, &budget);
assert_eq!(delta.viable, vec![9, 5], "ranked by route + start");
assert_eq!(delta.potential, vec![2, 7], "sorted; absence never prunes");
assert_eq!(delta.non_viable, vec![3]);
assert_eq!(delta.selected_provider(), Some(9));
}
#[test]
fn equal_costs_tie_break_on_provider_id_deterministically() {
let budget = ConsumerLatencyBudget::default();
let branches = [
branch(8, ProjectedReadiness::Ready, 100, 0),
branch(4, ProjectedReadiness::Ready, 100, 0),
];
let delta = project_sensed_candidates(&branches, &budget);
assert_eq!(delta.viable, vec![4, 8], "stable id tie-break");
}
#[test]
fn empty_views_yield_an_empty_delta_and_no_selection() {
let delta = project_sensed_candidates(&[], &ConsumerLatencyBudget::default());
assert_eq!(delta, SensedCandidates::default());
assert_eq!(delta.selected_provider(), None);
}
}