powerpack_cache/query.rs
1use std::convert::Infallible;
2use std::fmt::Write as _;
3use std::io;
4use std::time::Duration;
5
6use flagset::{FlagSet, flags};
7use serde_json as json;
8use thiserror::Error;
9
10use crate::{PrevEntry, UpdateFn};
11
12/// Raised when accessing data in the cache.
13#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum QueryError {
16 /// Raised when there is a cache miss.
17 #[error("cache miss")]
18 Miss,
19
20 /// Raised when an I/O error occurs.
21 ///
22 /// This can occur when reading the cache file.
23 #[error("io error")]
24 Io(#[from] io::Error),
25
26 /// Raised when JSON deserialization occurs.
27 ///
28 /// Data is stored in the cache as JSON, so this error is raised when
29 /// deserializing the data fails.
30 ///
31 /// Since the caller provides the type that is stored in the cache, this
32 /// will typically occur if the type changes.
33 #[error("deserialization error")]
34 BadData(#[from] json::Error),
35}
36
37flags! {
38 /// The policy for querying the cache.
39 ///
40 /// Holds various toggles for when to update the cache and when to return
41 /// stale data. This type follows a bitflag pattern, so multiple flags can
42 /// be combined using the `|` operator.
43 ///
44 /// The default policy is [`QueryPolicy::default_set()`], which is
45 /// equivalent to the following example.
46 ///
47 /// # Examples
48 ///
49 /// ```
50 /// # use powerpack_cache::{Query, QueryPolicy};
51 /// let q = Query::new("unique_key").policy(
52 /// QueryPolicy::UpdateBadData
53 /// | QueryPolicy::UpdateChecksumMismatch
54 /// | QueryPolicy::UpdateExpired
55 /// | QueryPolicy::ReturnExpired
56 /// );
57 /// # let q: Query<'_, ()> = q;
58 /// ```
59 pub enum QueryPolicy: u16 {
60 /// Always update the cache when a [`Query::update_fn`] is provided.
61 ///
62 /// This option overrides the other `Update...`flags and will always
63 /// update the cache. If not set then the cache will only be updated if
64 /// the data is bad or stale. Outlined in the below flags.
65 ///
66 /// Generally this should not be set because:
67 /// - It defeats the purpose of a TTL
68 /// - Alfred can spawn many instances of a process in a short period of
69 /// time, e.g. one for each character typed, this could result in many
70 /// unnecessary updates (depending on what type of data you're
71 /// storing).
72 UpdateAlways,
73
74 /// Update the cache if the data is bad (fails to deserialize).
75 ///
76 /// Generally this should be set because if the data is bad then you
77 /// want to correct it in the cache.
78 UpdateBadData,
79
80 /// Update the cache if the checksum is different.
81 ///
82 /// Generally this should be set because the checksum is used to
83 /// determine if the data is still applicable.
84 UpdateChecksumMismatch,
85
86 /// Update the cache if the data is expired.
87 UpdateExpired,
88
89 /// Always return data if it is available.
90 ///
91 /// This option is forward compatible with any other flags that may be
92 /// added in the future for returning data. Right now it is equivalent
93 /// to `ReturnBadDataErr | ReturnChecksumMismatch | ReturnExpired`.
94 ///
95 /// Generally this should not be set.
96 ReturnAlways,
97
98 /// Return the error if the data is bad, [`QueryError::BadData`] which
99 /// contains the deserialization error in the source.
100 ///
101 /// If not set then [`QueryError::Miss`] will be returned.
102 ///
103 /// Whether this should be set depends on whether your code is planning
104 /// on handling the error.
105 ReturnBadDataErr,
106
107 /// Return data even if the checksum is different.
108 ///
109 /// If not set then [`QueryError::Miss`] will be returned.
110 ///
111 /// Generally this should not be set because the checksum is used to
112 /// determine if the data is still applicable.
113 ReturnChecksumMismatch,
114
115 /// Return data if it is expired.
116 ///
117 /// If not set then [`QueryError::Miss`] will be returned.
118 ///
119 /// Generally this should be set because for Alfred workflows it is
120 /// desirable to return *something* even if the data is expired.
121 ReturnExpired,
122 }
123}
124
125impl QueryPolicy {
126 /// Returns the default policy.
127 ///
128 /// The default enables the following flags only.
129 /// - [`QueryPolicy::UpdateBadData`]
130 /// - [`QueryPolicy::UpdateChecksumMismatch`]
131 /// - [`QueryPolicy::UpdateExpired`]
132 /// - [`QueryPolicy::ReturnExpired`]
133 pub fn default_set() -> FlagSet<Self> {
134 QueryPolicy::UpdateBadData
135 | QueryPolicy::UpdateChecksumMismatch
136 | QueryPolicy::UpdateExpired
137 | QueryPolicy::ReturnExpired
138 }
139}
140
141/// Query the cache for data.
142///
143/// A query must be constructed, using the builder pattern and then passed to
144/// [`Cache::query`](crate::Cache::query) to retrieve the data.
145///
146/// The following fields are required when constructing a query:
147///
148/// - `key`: passed to [`Query::new`], this is a unique identifier for the
149/// data in the cache, and is used to determine the name of the cache file.
150///
151/// - type `T`: the type of the data stored in the cache, it must implement
152/// [`serde::Serialize`] and [`serde::Deserialize`].
153///
154/// The following fields are optional:
155///
156/// - `update_fn`: used to update the cache, see [`Query::update_fn`].
157/// - `checksum`: used to determine staleness of the cache, see
158/// [`Query::checksum`].
159/// - `policy`: used to determine when to update the cache and when to return
160/// stale data, see [`Query::policy`].
161/// - `ttl`: the Time To Live (TTL) for the data in the cache, see
162/// [`Query::ttl`].
163/// - `initial_poll`: the duration to wait for the cache to be populated on the
164/// first call, see [`Query::initial_poll`].
165///
166pub struct Query<'a, T, E = Infallible> {
167 pub(crate) key: &'a str,
168 pub(crate) update_fn: Option<UpdateFn<'a, T, E>>,
169 pub(crate) policy: Option<FlagSet<QueryPolicy>>,
170 pub(crate) checksum: Option<String>,
171 pub(crate) ttl: Option<Duration>,
172 pub(crate) initial_poll: Option<Duration>,
173}
174
175impl<'a> Query<'a, (), Infallible> {
176 /// Returns a new cache query.
177 ///
178 /// The key is used to determine the name of the cache file.
179 #[inline]
180 pub fn new(key: &'a str) -> Self {
181 Query {
182 key,
183 update_fn: None,
184 policy: None,
185 checksum: None,
186 ttl: None,
187 initial_poll: None,
188 }
189 }
190
191 /// Set the function to update the cache.
192 ///
193 /// This function is called if the cache needs to be updated.
194 ///
195 /// # 💡 Note
196 ///
197 /// The cache is updated in a separate process to avoid blocking the main
198 /// thread, this means that any errors from the update function will not be
199 /// propagated. Stale data will be returned in the meantime.
200 #[inline]
201 pub fn update_fn<F, T, E>(self, update_fn: F) -> Query<'a, T, E>
202 where
203 F: FnOnce(Option<PrevEntry<T>>) -> Result<T, E> + 'a,
204 {
205 Query {
206 key: self.key,
207 checksum: self.checksum,
208 policy: self.policy,
209 ttl: self.ttl,
210 initial_poll: self.initial_poll,
211 update_fn: Some(Box::new(update_fn)),
212 }
213 }
214}
215
216impl<T, E> Query<'_, T, E> {
217 /// Set the checksum for the cache.
218 ///
219 /// This is used to determine staleness and is used in two places:
220 ///
221 /// - Whether to the cache needs to be updated (in addition to the TTL).
222 /// If the checksum is different to the one stored in the cache then the
223 /// cache might be updated, depending on the [`QueryPolicy`].
224 ///
225 /// - Whether to return stale data. If the checksum is different to the one
226 /// stored in the cache then depending on the [`QueryPolicy`] stale data
227 /// may be returned.
228 ///
229 #[inline]
230 pub fn checksum<C>(mut self, checksum: C) -> Self
231 where
232 C: AsRef<[u8]>,
233 {
234 self.checksum = Some(to_hex(checksum.as_ref()));
235 self
236 }
237
238 /// Set the policy for the cache query.
239 ///
240 /// This is used to determine when updates should occur and stale data is
241 /// allowed to be returned.
242 ///
243 /// Defaults to the cache's policy.
244 #[inline]
245 pub fn policy(mut self, policy: impl Into<FlagSet<QueryPolicy>>) -> Self {
246 self.policy = Some(policy.into());
247 self
248 }
249
250 /// Set the Time To Live (TTL) for the data in the cache.
251 ///
252 /// If the data in the cache is older than this then the cache will be
253 /// automatically refreshed. Stale data will be returned in the meantime.
254 ///
255 /// Defaults to the cache's TTL.
256 #[inline]
257 pub fn ttl(mut self, ttl: Duration) -> Self {
258 self.ttl = Some(ttl);
259 self
260 }
261
262 /// Set the initial poll duration.
263 ///
264 /// This is the duration to wait for the cache to be populated on the first
265 /// call. If the cache is not populated within this duration, a miss error
266 /// will be raised.
267 ///
268 /// Defaults to the cache's initial poll duration.
269 #[inline]
270 pub fn initial_poll(mut self, initial_poll: Duration) -> Self {
271 self.initial_poll = Some(initial_poll);
272 self
273 }
274}
275
276fn to_hex(b: &[u8]) -> String {
277 let mut s = String::with_capacity(b.len() * 2);
278 for byte in b {
279 write!(&mut s, "{:02x}", byte).unwrap();
280 }
281 s
282}