Skip to main content

arch_toolkit/index/
refresh.rs

1//! Cancellable background index-refresh handle.
2
3use std::future::Future;
4
5use crate::error::{ArchToolkitError, Result};
6use crate::types::index::OfficialIndex;
7
8/// What: Represent one caller-supplied background official-index refresh.
9///
10/// Inputs:
11/// - Created by [`spawn_index_refresh`] from a future that produces an
12///   [`OfficialIndex`] or a structured toolkit error.
13///
14/// Output:
15/// - A handle that can cancel pending async work and explicitly await its
16///   result/error delivery.
17///
18/// Details:
19/// - This handle owns no global index and performs no system mutation.
20/// - Cancellation aborts the async task; cooperative futures such as caller
21///   HTTP requests are dropped promptly, while caller code still owns any
22///   external resource semantics.
23#[derive(Debug)]
24pub struct IndexRefreshHandle {
25    /// Tokio task delivering the caller-supplied refresh result.
26    task: tokio::task::JoinHandle<Result<OfficialIndex>>,
27}
28
29/// What: Start a caller-supplied async index refresh in the background.
30///
31/// Inputs:
32/// - `refresh`: Sendable `'static` future returning a refreshed index or a
33///   structured error.
34///
35/// Output:
36/// - [`IndexRefreshHandle`] for cancellation and explicit result delivery.
37///
38/// Details:
39/// - The API intentionally accepts a future rather than hiding `pacman` or
40///   network policy. Callers can use a local fetch, a caller-client HTTP
41///   fetcher, or an in-memory fixture with the same cancellation contract.
42/// - Dropping the handle detaches the task; call [`IndexRefreshHandle::cancel`]
43///   when a caller no longer wants the refresh to continue.
44pub fn spawn_index_refresh<F>(refresh: F) -> IndexRefreshHandle
45where
46    F: Future<Output = Result<OfficialIndex>> + Send + 'static,
47{
48    IndexRefreshHandle {
49        task: tokio::spawn(refresh),
50    }
51}
52
53impl IndexRefreshHandle {
54    /// What: Request cancellation of a pending background refresh.
55    ///
56    /// Inputs: None.
57    ///
58    /// Output:
59    /// - Requests task abortion; [`Self::wait`] reports a structured cancelled
60    ///   error if the task did not complete first.
61    ///
62    /// Details:
63    /// - Cancellation is idempotent and does not block the calling thread.
64    /// - It is meaningful for async futures; callers should not wrap a
65    ///   non-cancellable `spawn_blocking` operation in this API and expect it
66    ///   to stop after it has started.
67    pub fn cancel(&self) {
68        self.task.abort();
69    }
70
71    /// What: Check whether the refresh task has completed.
72    ///
73    /// Inputs: None.
74    ///
75    /// Output:
76    /// - `true` after successful, failed, panicked, or cancelled completion.
77    ///
78    /// Details:
79    /// - This is an observation only; use [`Self::wait`] for explicit result
80    ///   or error delivery.
81    #[must_use]
82    pub fn is_finished(&self) -> bool {
83        self.task.is_finished()
84    }
85
86    /// What: Await the background refresh result and deliver task failures explicitly.
87    ///
88    /// Inputs:
89    /// - `self`: Consumes the handle and awaits the owned task.
90    ///
91    /// Output:
92    /// - Refreshed [`OfficialIndex`] or the refresh/task/cancellation error.
93    ///
94    /// Details:
95    /// - A cancelled task maps to an actionable parse error instead of silently
96    ///   returning an empty index. Panics and runtime failures also remain
97    ///   visible to the caller.
98    ///
99    /// # Errors
100    ///
101    /// Returns the caller future's error or a descriptive task failure mapped to
102    /// [`ArchToolkitError::Parse`].
103    pub async fn wait(self) -> Result<OfficialIndex> {
104        self.task
105            .await
106            .map_err(|error| map_refresh_join_error(&error))?
107    }
108}
109
110/// What: Translate a Tokio task join error into a public toolkit error.
111///
112/// Inputs:
113/// - `error`: Join failure observed while awaiting a refresh task.
114///
115/// Output:
116/// - Actionable cancellation or task-failure error.
117///
118/// Details:
119/// - Keeps cancellation distinguishable from a caller fetch error while
120///   avoiding exposure of Tokio error types in the public return contract.
121fn map_refresh_join_error(error: &tokio::task::JoinError) -> ArchToolkitError {
122    if error.is_cancelled() {
123        return ArchToolkitError::Parse("background index refresh was cancelled".to_string());
124    }
125    ArchToolkitError::Parse(format!("background index refresh task failed: {error}"))
126}
127
128#[cfg(test)]
129mod tests {
130    use std::time::Duration;
131
132    use crate::error::Result;
133    use crate::types::index::OfficialIndex;
134
135    use super::spawn_index_refresh;
136
137    #[tokio::test]
138    /// What: Verify a background refresh delivers its successful index explicitly.
139    ///
140    /// Inputs:
141    /// - A fixture-only async future returning an empty official index.
142    ///
143    /// Output:
144    /// - The same completed index from [`IndexRefreshHandle::wait`].
145    ///
146    /// Details:
147    /// - Demonstrates that the API returns a handle rather than hidden global
148    ///   state or an unobservable detached task.
149    async fn refresh_delivers_successful_result() {
150        let handle =
151            spawn_index_refresh(async { Result::<OfficialIndex>::Ok(OfficialIndex::default()) });
152        let index = handle.wait().await.expect("refresh result");
153        assert!(index.pkgs.is_empty());
154    }
155
156    #[tokio::test]
157    /// What: Verify cancellation produces an explicit error on wait.
158    ///
159    /// Inputs:
160    /// - A pending fixture-only async refresh future.
161    ///
162    /// Output:
163    /// - A cancellation error instead of a false successful empty index.
164    ///
165    /// Details:
166    /// - Uses Tokio time only; no system command or remote endpoint is touched.
167    async fn refresh_cancellation_is_explicit() {
168        let handle = spawn_index_refresh(async {
169            tokio::time::sleep(Duration::from_mins(1)).await;
170            Result::<OfficialIndex>::Ok(OfficialIndex::default())
171        });
172        handle.cancel();
173        let error = handle.wait().await.expect_err("cancelled refresh error");
174        assert!(error.to_string().contains("cancelled"));
175    }
176}