1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//! Proactive detail fetching for packages near the current selection.
use tokio::sync::mpsc;
use crate::state::{AppState, PackageItem};
/// What: Prefetch details for items near the current selection (alternating above/below).
///
/// Inputs:
/// - `app`: Mutable application state (`results`, `selected`, `details_cache`)
/// - `details_tx`: Channel to enqueue detail requests
///
/// Output:
/// - Enqueues requests for allowed, uncached neighbors within a fixed radius; no return value.
///
/// Details:
/// - Respects `logic::is_allowed` and skips names present in the cache; designed to be cheap.
pub fn ring_prefetch_from_selected(
app: &mut AppState,
details_tx: &mpsc::UnboundedSender<PackageItem>,
) {
let len_u = app.results.len();
if len_u == 0 {
return;
}
let max_radius: usize = 30;
let mut step: usize = 1;
loop {
let progressed_up = if let Some(i) = app.selected.checked_sub(step) {
if let Some(it) = app.results.get(i).cloned()
&& crate::logic::is_allowed(&it.name)
&& !app.details_cache.contains_key(&it.name)
{
let _ = details_tx.send(it);
}
true
} else {
false
};
let below = app.selected + step;
let progressed_down = if below < len_u {
if let Some(it) = app.results.get(below).cloned()
&& crate::logic::is_allowed(&it.name)
&& !app.details_cache.contains_key(&it.name)
{
let _ = details_tx.send(it);
}
true
} else {
false
};
let progressed = progressed_up || progressed_down;
if step >= max_radius || !progressed {
break;
}
step += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn item_official(name: &str, repo: &str) -> PackageItem {
PackageItem {
name: name.to_string(),
version: "1.0".to_string(),
description: format!("{name} desc"),
source: crate::state::Source::Official {
repo: repo.to_string(),
arch: "x86_64".to_string(),
},
popularity: None,
out_of_date: None,
orphaned: false,
}
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
/// What: Ensure prefetching emits no requests when results are empty.
///
/// Inputs:
/// - Application state with zero search results.
///
/// Output:
/// - No messages received on the details channel within the timeout window.
///
/// Details:
/// - Uses a short timeout to confirm no unexpected sends occur during the async loop.
async fn prefetch_noop_on_empty_results() {
let _guard = crate::global_test_mutex_lock();
let mut app = AppState::default();
let (tx, mut rx) = mpsc::unbounded_channel();
ring_prefetch_from_selected(&mut app, &tx);
let none = tokio::time::timeout(std::time::Duration::from_millis(30), rx.recv())
.await
.ok()
.flatten();
assert!(none.is_none());
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
/// What: Verify prefetch honours allowed gating and avoids cached entries.
///
/// Inputs:
/// - Results list of three packages with varying allowed states and cache contents.
///
/// Output:
/// - No requests when only the selected item is allowed; afterwards only uncached, allowed neighbor is dispatched.
///
/// Details:
/// - Toggles `set_allowed_only_selected` and `set_allowed_ring`, updating the cache between passes to target specific neighbours.
async fn prefetch_respects_allowed_and_cache() {
let _guard = crate::global_test_mutex_lock();
let mut app = AppState {
results: vec![
item_official("a", "core"),
item_official("b", "extra"),
item_official("c", "extra"),
],
selected: 1,
..Default::default()
};
// Disallow b/c except selected, and cache one neighbor
crate::logic::set_allowed_only_selected(&app);
app.details_cache.insert(
"c".into(),
crate::state::PackageDetails {
name: "c".into(),
..Default::default()
},
);
let (tx, mut rx) = mpsc::unbounded_channel();
ring_prefetch_from_selected(&mut app, &tx);
// With only-selected allowed, neighbors shouldn't be sent
let none = tokio::time::timeout(std::time::Duration::from_millis(60), rx.recv())
.await
.ok()
.flatten();
assert!(none.is_none());
// Now allow ring and clear cache for b, keep c cached
app.details_cache.clear();
app.details_cache.insert(
"c".into(),
crate::state::PackageDetails {
name: "c".into(),
..Default::default()
},
);
crate::logic::set_allowed_ring(&app, 1);
ring_prefetch_from_selected(&mut app, &tx);
// Expect only 'a' (above neighbor) to be sent; 'c' is cached
let sent = tokio::time::timeout(std::time::Duration::from_millis(200), rx.recv())
.await
.ok()
.flatten()
.expect("one sent");
assert_eq!(sent.name, "a");
let none2 = tokio::time::timeout(std::time::Duration::from_millis(60), rx.recv())
.await
.ok()
.flatten();
assert!(none2.is_none());
}
}