pacsea/logic/
prefetch.rs

1//! Proactive detail fetching for packages near the current selection.
2
3use tokio::sync::mpsc;
4
5use crate::state::{AppState, PackageItem};
6
7/// What: Prefetch details for items near the current selection (alternating above/below).
8///
9/// Inputs:
10/// - `app`: Mutable application state (`results`, `selected`, `details_cache`)
11/// - `details_tx`: Channel to enqueue detail requests
12///
13/// Output:
14/// - Enqueues requests for allowed, uncached neighbors within a fixed radius; no return value.
15///
16/// Details:
17/// - Respects `logic::is_allowed` and skips names present in the cache; designed to be cheap.
18pub 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    /// What: Ensure prefetching emits no requests when results are empty.
82    ///
83    /// Inputs:
84    /// - Application state with zero search results.
85    ///
86    /// Output:
87    /// - No messages received on the details channel within the timeout window.
88    ///
89    /// Details:
90    /// - Uses a short timeout to confirm no unexpected sends occur during the async loop.
91    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    /// What: Verify prefetch honours allowed gating and avoids cached entries.
106    ///
107    /// Inputs:
108    /// - Results list of three packages with varying allowed states and cache contents.
109    ///
110    /// Output:
111    /// - No requests when only the selected item is allowed; afterwards only uncached, allowed neighbor is dispatched.
112    ///
113    /// Details:
114    /// - Toggles `set_allowed_only_selected` and `set_allowed_ring`, updating the cache between passes to target specific neighbours.
115    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        // Disallow b/c except selected, and cache one neighbor
127        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        // With only-selected allowed, neighbors shouldn't be sent
138        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        // Now allow ring and clear cache for b, keep c cached
145        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        // Expect only 'a' (above neighbor) to be sent; 'c' is cached
156        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}