1use tokio::sync::mpsc;
4
5use crate::state::{AppState, PackageItem};
6
7pub fn ring_prefetch_from_selected(
19 app: &mut AppState,
20 details_tx: &mpsc::UnboundedSender<PackageItem>,
21) {
22 let len_u = app.results.len();
23 if len_u == 0 {
24 return;
25 }
26 let max_radius: usize = 30;
27 let mut step: usize = 1;
28 loop {
29 let progressed_up = if let Some(i) = app.selected.checked_sub(step) {
30 if let Some(it) = app.results.get(i).cloned()
31 && crate::logic::is_allowed(&it.name)
32 && !app.details_cache.contains_key(&it.name)
33 {
34 let _ = details_tx.send(it);
35 }
36 true
37 } else {
38 false
39 };
40 let below = app.selected + step;
41 let progressed_down = if below < len_u {
42 if let Some(it) = app.results.get(below).cloned()
43 && crate::logic::is_allowed(&it.name)
44 && !app.details_cache.contains_key(&it.name)
45 {
46 let _ = details_tx.send(it);
47 }
48 true
49 } else {
50 false
51 };
52 let progressed = progressed_up || progressed_down;
53 if step >= max_radius || !progressed {
54 break;
55 }
56 step += 1;
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 fn item_official(name: &str, repo: &str) -> PackageItem {
65 PackageItem {
66 name: name.to_string(),
67 version: "1.0".to_string(),
68 description: format!("{name} desc"),
69 source: crate::state::Source::Official {
70 repo: repo.to_string(),
71 arch: "x86_64".to_string(),
72 },
73 popularity: None,
74 out_of_date: None,
75 orphaned: false,
76 }
77 }
78
79 #[tokio::test]
80 #[allow(clippy::await_holding_lock)]
81 async fn prefetch_noop_on_empty_results() {
92 let _guard = crate::global_test_mutex_lock();
93 let mut app = AppState::default();
94 let (tx, mut rx) = mpsc::unbounded_channel();
95 ring_prefetch_from_selected(&mut app, &tx);
96 let none = tokio::time::timeout(std::time::Duration::from_millis(30), rx.recv())
97 .await
98 .ok()
99 .flatten();
100 assert!(none.is_none());
101 }
102
103 #[tokio::test]
104 #[allow(clippy::await_holding_lock)]
105 async fn prefetch_respects_allowed_and_cache() {
116 let _guard = crate::global_test_mutex_lock();
117 let mut app = AppState {
118 results: vec![
119 item_official("a", "core"),
120 item_official("b", "extra"),
121 item_official("c", "extra"),
122 ],
123 selected: 1,
124 ..Default::default()
125 };
126 crate::logic::set_allowed_only_selected(&app);
128 app.details_cache.insert(
129 "c".into(),
130 crate::state::PackageDetails {
131 name: "c".into(),
132 ..Default::default()
133 },
134 );
135 let (tx, mut rx) = mpsc::unbounded_channel();
136 ring_prefetch_from_selected(&mut app, &tx);
137 let none = tokio::time::timeout(std::time::Duration::from_millis(60), rx.recv())
139 .await
140 .ok()
141 .flatten();
142 assert!(none.is_none());
143
144 app.details_cache.clear();
146 app.details_cache.insert(
147 "c".into(),
148 crate::state::PackageDetails {
149 name: "c".into(),
150 ..Default::default()
151 },
152 );
153 crate::logic::set_allowed_ring(&app, 1);
154 ring_prefetch_from_selected(&mut app, &tx);
155 let sent = tokio::time::timeout(std::time::Duration::from_millis(200), rx.recv())
157 .await
158 .ok()
159 .flatten()
160 .expect("one sent");
161 assert_eq!(sent.name, "a");
162 let none2 = tokio::time::timeout(std::time::Duration::from_millis(60), rx.recv())
163 .await
164 .ok()
165 .flatten();
166 assert!(none2.is_none());
167 }
168}