use std::collections::HashSet;
use tui_pane::RunningTracker;
use tui_pane::ToastId;
use super::NetworkRunningToasts;
use super::StartupNetworkPending;
use super::StartupNetworkReadiness;
use super::StartupNetworkReady;
use super::network_stage::NetworkRunningTrackers;
use super::network_stage::StartupServiceExit;
use super::network_stage::SteadyStateNetworkToasts;
use crate::ci::OwnerRepo;
use crate::http::GitHubRateLimit;
use crate::http::HttpClient;
use crate::http::ServiceKind;
use crate::scan;
use crate::scan::RepoCache;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum AvailabilityStatus {
#[default]
Reachable,
Unreachable,
RateLimited,
Unauthenticated,
NotInstalled,
}
impl AvailabilityStatus {
pub const fn is_available(self) -> bool { matches!(self, Self::Reachable) }
pub const fn is_unauthenticated(self) -> bool {
matches!(self, Self::Unauthenticated | Self::NotInstalled)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RecoveryOutcome {
NoTransition,
Silent,
WithToast(ToastId),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ServiceStatus {
#[default]
Available,
Unreachable,
}
pub struct ServiceAvailability {
status: AvailabilityStatus,
retry_active: bool,
unavailable_toast: Option<ToastId>,
}
impl ServiceAvailability {
pub const fn new() -> Self {
Self {
status: AvailabilityStatus::Reachable,
retry_active: false,
unavailable_toast: None,
}
}
pub const fn status(&self) -> AvailabilityStatus { self.status }
#[cfg(test)]
pub const fn is_unavailable(&self) -> bool { !self.status.is_available() }
pub const fn mark_reachable(&mut self) -> RecoveryOutcome {
let was_unavailable = !matches!(self.status, AvailabilityStatus::Reachable);
self.status = AvailabilityStatus::Reachable;
if !was_unavailable {
return RecoveryOutcome::NoTransition;
}
self.retry_active = false;
match self.unavailable_toast.take() {
Some(id) => RecoveryOutcome::WithToast(id),
None => RecoveryOutcome::Silent,
}
}
pub const fn mark_unreachable(&mut self) -> bool {
self.status = AvailabilityStatus::Unreachable;
let newly_active = !self.retry_active;
self.retry_active = true;
newly_active
}
pub const fn mark_rate_limited(&mut self) -> bool {
self.status = AvailabilityStatus::RateLimited;
let newly_active = !self.retry_active;
self.retry_active = true;
newly_active
}
pub const fn mark_unauthenticated(&mut self) {
self.status = AvailabilityStatus::Unauthenticated;
}
pub const fn mark_not_installed(&mut self) { self.status = AvailabilityStatus::NotInstalled; }
pub const fn toast_id(&self) -> Option<ToastId> { self.unavailable_toast }
pub const fn set_toast(&mut self, id: ToastId) { self.unavailable_toast = Some(id); }
pub const fn mark_recovered(&mut self) -> RecoveryOutcome { self.mark_reachable() }
}
pub struct Github {
pub availability: ServiceAvailability,
pub fetch_cache: RepoCache,
repo_fetch_in_flight: HashSet<OwnerRepo>,
pr_check_polls: HashSet<(OwnerRepo, u32)>,
}
impl Github {
fn new() -> Self {
Self {
availability: ServiceAvailability::new(),
fetch_cache: scan::new_repo_cache(),
repo_fetch_in_flight: HashSet::new(),
pr_check_polls: HashSet::new(),
}
}
pub const fn repo_fetch_in_flight_mut(&mut self) -> &mut HashSet<OwnerRepo> {
&mut self.repo_fetch_in_flight
}
pub fn contains_in_flight(&self, repo: &OwnerRepo) -> bool {
self.repo_fetch_in_flight.contains(repo)
}
pub fn has_repo_fetch_in_flight(&self) -> bool { !self.repo_fetch_in_flight.is_empty() }
pub fn insert_pr_check_poll(&mut self, repo: OwnerRepo, number: u32) -> bool {
self.pr_check_polls.insert((repo, number))
}
pub fn remove_pr_check_poll(&mut self, repo: &OwnerRepo, number: u32) -> bool {
self.pr_check_polls.remove(&(repo.clone(), number))
}
pub fn pr_check_poll_numbers(&self, repo: &OwnerRepo) -> HashSet<u32> {
self.pr_check_polls
.iter()
.filter_map(|(poll_repo, number)| (poll_repo == repo).then_some(*number))
.collect()
}
pub fn has_pr_check_polls(&self) -> bool { !self.pr_check_polls.is_empty() }
pub fn retain_pr_check_polls_for_repo(
&mut self,
repo: &OwnerRepo,
active_numbers: &HashSet<u32>,
) -> bool {
let before = self.pr_check_polls.len();
self.pr_check_polls
.retain(|(poll_repo, number)| poll_repo != repo || active_numbers.contains(number));
before != self.pr_check_polls.len()
}
fn clear_for_tree_change(&mut self) {
self.fetch_cache = scan::new_repo_cache();
self.repo_fetch_in_flight.clear();
self.pr_check_polls.clear();
}
}
pub struct CratesIo {
pub availability: ServiceAvailability,
}
impl CratesIo {
const fn new() -> Self {
Self {
availability: ServiceAvailability::new(),
}
}
}
enum NetworkToastStage {
StartupOwned(NetworkRunningTrackers),
SteadyState(SteadyStateNetworkToasts),
}
pub struct Net {
pub http_client: HttpClient,
pub github: Github,
pub crates_io: CratesIo,
toast_stage: NetworkToastStage,
}
impl Net {
pub fn new(http_client: HttpClient) -> Self {
Self {
http_client,
github: Github::new(),
crates_io: CratesIo::new(),
toast_stage: NetworkToastStage::StartupOwned(NetworkRunningTrackers::default()),
}
}
pub const fn network_toasts(&self) -> Option<&NetworkRunningToasts> {
match &self.toast_stage {
NetworkToastStage::SteadyState(stage) => Some(&stage.toasts),
NetworkToastStage::StartupOwned(_) => None,
}
}
pub const fn network_toasts_mut(&mut self) -> Option<&mut NetworkRunningToasts> {
match &mut self.toast_stage {
NetworkToastStage::SteadyState(stage) => Some(&mut stage.toasts),
NetworkToastStage::StartupOwned(_) => None,
}
}
pub const fn github_running(&self) -> &RunningTracker<OwnerRepo> {
match &self.toast_stage {
NetworkToastStage::StartupOwned(trackers) => &trackers.github,
NetworkToastStage::SteadyState(stage) => &stage.running.github,
}
}
pub const fn github_running_mut(&mut self) -> &mut RunningTracker<OwnerRepo> {
match &mut self.toast_stage {
NetworkToastStage::StartupOwned(trackers) => &mut trackers.github,
NetworkToastStage::SteadyState(stage) => &mut stage.running.github,
}
}
pub const fn crates_io_running(&self) -> &RunningTracker<String> {
match &self.toast_stage {
NetworkToastStage::StartupOwned(trackers) => &trackers.crates_io,
NetworkToastStage::SteadyState(stage) => &stage.running.crates_io,
}
}
pub const fn crates_io_running_mut(&mut self) -> &mut RunningTracker<String> {
match &mut self.toast_stage {
NetworkToastStage::StartupOwned(trackers) => &mut trackers.crates_io,
NetworkToastStage::SteadyState(stage) => &mut stage.running.crates_io,
}
}
pub fn startup_github_running_repos(&self) -> Vec<OwnerRepo> {
match &self.toast_stage {
NetworkToastStage::StartupOwned(trackers) => {
trackers.github.running.keys().cloned().collect()
},
NetworkToastStage::SteadyState(_) => Vec::new(),
}
}
pub fn startup_crates_io_running_names(&self) -> Vec<String> {
match &self.toast_stage {
NetworkToastStage::StartupOwned(trackers) => {
trackers.crates_io.running.keys().cloned().collect()
},
NetworkToastStage::SteadyState(_) => Vec::new(),
}
}
pub fn startup_network_readiness(
&self,
github_failed: bool,
crates_io_failed: bool,
) -> StartupNetworkReadiness {
let NetworkToastStage::StartupOwned(trackers) = &self.toast_stage else {
return StartupNetworkReadiness::Ready(StartupNetworkReady {
github: StartupServiceExit::Drained,
crates_io: StartupServiceExit::Drained,
});
};
let exits = match (
service_exit(trackers.github.running.len(), github_failed),
service_exit(trackers.crates_io.running.len(), crates_io_failed),
) {
(Ok(github), Ok(crates_io)) => (github, crates_io),
(github, crates_io) => {
return StartupNetworkReadiness::Pending(StartupNetworkPending {
github: github.err().unwrap_or_default(),
crates_io: crates_io.err().unwrap_or_default(),
});
},
};
StartupNetworkReadiness::Ready(StartupNetworkReady {
github: exits.0,
crates_io: exits.1,
})
}
pub fn begin_steady_state_network_toasts(&mut self, ready: &StartupNetworkReady) {
let NetworkToastStage::StartupOwned(trackers) = &mut self.toast_stage else {
return;
};
if matches!(ready.github, StartupServiceExit::Abandoned) {
trackers.github.clear();
}
if matches!(ready.crates_io, StartupServiceExit::Abandoned) {
trackers.crates_io.clear();
}
let running = std::mem::take(trackers);
self.toast_stage = NetworkToastStage::SteadyState(SteadyStateNetworkToasts {
running,
toasts: NetworkRunningToasts::default(),
});
}
pub fn set_network_toasts_startup_owned(&mut self) {
let trackers = match std::mem::replace(
&mut self.toast_stage,
NetworkToastStage::StartupOwned(NetworkRunningTrackers::default()),
) {
NetworkToastStage::StartupOwned(trackers) => trackers,
NetworkToastStage::SteadyState(stage) => stage.running,
};
self.toast_stage = NetworkToastStage::StartupOwned(trackers);
}
pub fn http_client(&self) -> HttpClient { self.http_client.clone() }
pub fn rate_limit(&self) -> GitHubRateLimit { self.http_client.rate_limit() }
pub fn set_force_github_rate_limit(&self, on: bool) {
self.http_client.set_force_github_rate_limit(on);
}
pub const fn github_status(&self) -> AvailabilityStatus { self.github.availability.status() }
pub fn clear_for_tree_change(&mut self) {
self.github.clear_for_tree_change();
self.github_running_mut().clear();
}
pub const fn availability_for(&mut self, service: ServiceKind) -> &mut ServiceAvailability {
match service {
ServiceKind::GitHub => &mut self.github.availability,
ServiceKind::CratesIo => &mut self.crates_io.availability,
}
}
pub fn spawn_rate_limit_prime(&self) {
let client = self.http_client();
std::thread::spawn(move || {
let (rate_limit, _signal) = client.fetch_rate_limit();
if rate_limit.is_some() {
tracing::info!("rate_limit_prime_ok");
} else {
tracing::info!("rate_limit_prime_failed");
}
});
}
}
const fn service_exit(running: usize, failed: bool) -> Result<StartupServiceExit, usize> {
match (running, failed) {
(0, _) => Ok(StartupServiceExit::Drained),
(_, true) => Ok(StartupServiceExit::Abandoned),
(running, false) => Err(running),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mark_unauthenticated_sets_status_and_reports_unavailable() {
let mut avail = ServiceAvailability::new();
avail.mark_unauthenticated();
assert_eq!(avail.status(), AvailabilityStatus::Unauthenticated);
assert!(avail.status().is_unauthenticated());
assert!(!avail.status().is_available());
}
#[test]
fn mark_not_installed_sets_status_and_reports_unavailable() {
let mut avail = ServiceAvailability::new();
avail.mark_not_installed();
assert_eq!(avail.status(), AvailabilityStatus::NotInstalled);
assert!(avail.status().is_unauthenticated());
assert!(!avail.status().is_available());
}
}