1use std::collections::HashMap;
35use std::sync::{Arc, Mutex};
36use std::time::{Duration, Instant};
37
38pub const DEFAULT_TTL: Duration = Duration::from_secs(5 * 60);
40
41#[derive(Default)]
46pub struct PaginationCache<T, M = ()> {
47 map: Mutex<HashMap<String, Arc<QueryLock<T, M>>>>,
48}
49
50impl<T, M> PaginationCache<T, M> {
51 pub fn new() -> Self {
53 Self {
54 map: Mutex::new(HashMap::new()),
55 }
56 }
57
58 #[expect(
61 clippy::unwrap_used,
62 reason = "Mutex poisoning indicates a prior panic. Fail fast for pagination cache map."
63 )]
64 fn lock_map(&self) -> std::sync::MutexGuard<'_, HashMap<String, Arc<QueryLock<T, M>>>> {
65 self.map.lock().unwrap()
66 }
67
68 pub fn remove_if_same(&self, key: &str, candidate: &Arc<QueryLock<T, M>>) {
73 let mut m = self.lock_map();
74 if let Some(existing) = m.get(key)
75 && Arc::ptr_eq(existing, candidate)
76 {
77 m.remove(key);
78 }
79 }
80}
81
82impl<T, M: Default> PaginationCache<T, M> {
83 pub fn get_or_create(&self, key: &str) -> Arc<QueryLock<T, M>> {
88 let mut m = self.lock_map();
89 m.entry(key.to_string())
90 .or_insert_with(|| Arc::new(QueryLock::new()))
91 .clone()
92 }
93
94 pub fn sweep_expired(&self) {
99 let entries: Vec<(String, Arc<QueryLock<T, M>>)> = {
100 let m = self.lock_map();
101 m.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
102 };
103
104 for (k, lk) in entries {
105 let expired = { lk.lock_state().is_expired() };
106 if expired {
107 let mut m = self.lock_map();
108 if let Some(existing) = m.get(&k)
109 && Arc::ptr_eq(existing, &lk)
110 {
111 m.remove(&k);
112 }
113 }
114 }
115 }
116}
117
118pub struct QueryLock<T, M = ()> {
120 pub state: Mutex<QueryState<T, M>>,
121}
122
123impl<T, M> QueryLock<T, M> {
124 #[expect(
127 clippy::unwrap_used,
128 reason = "Mutex poisoning indicates a prior panic. Fail fast to avoid \
129 inconsistent pagination state."
130 )]
131 pub fn lock_state(&self) -> std::sync::MutexGuard<'_, QueryState<T, M>> {
132 self.state.lock().unwrap()
133 }
134}
135
136impl<T, M: Default> QueryLock<T, M> {
137 pub fn new() -> Self {
139 Self {
140 state: Mutex::new(QueryState::with_ttl(DEFAULT_TTL)),
141 }
142 }
143}
144
145impl<T, M: Default> Default for QueryLock<T, M> {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151pub struct QueryState<T, M = ()> {
153 pub results: Vec<T>,
155 pub meta: M,
157 pub next_offset: usize,
159 pub page_size: usize,
161 pub created_at: Instant,
163 ttl: Duration,
165}
166
167impl<T> QueryState<T, ()> {
168 pub fn empty() -> Self {
170 Self {
171 results: Vec::new(),
172 meta: (),
173 next_offset: 0,
174 page_size: 0,
175 created_at: Instant::now(),
176 ttl: DEFAULT_TTL,
177 }
178 }
179}
180
181impl<T, M: Default> QueryState<T, M> {
182 pub fn with_ttl(ttl: Duration) -> Self {
184 Self {
185 results: Vec::new(),
186 meta: M::default(),
187 next_offset: 0,
188 page_size: 0,
189 created_at: Instant::now(),
190 ttl,
191 }
192 }
193
194 pub fn reset(&mut self, entries: Vec<T>, meta: M, page_size: usize) {
196 self.results = entries;
197 self.meta = meta;
198 self.next_offset = 0;
199 self.page_size = page_size;
200 self.created_at = Instant::now();
201 }
202
203 pub fn is_expired(&self) -> bool {
205 self.created_at.elapsed() >= self.ttl
206 }
207
208 pub fn is_empty(&self) -> bool {
210 self.results.is_empty() && self.page_size == 0
211 }
212}
213
214pub fn paginate_slice<T: Clone>(entries: &[T], offset: usize, page_size: usize) -> (Vec<T>, bool) {
226 if offset >= entries.len() {
227 return (vec![], false);
228 }
229 let end = (offset + page_size).min(entries.len());
230 let has_more = end < entries.len();
231 (entries[offset..end].to_vec(), has_more)
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn paginate_slice_first_page() {
240 let items: Vec<i32> = (0..25).collect();
241 let (page, has_more) = paginate_slice(&items, 0, 10);
242 assert_eq!(page.len(), 10);
243 assert!(has_more);
244 assert_eq!(page[0], 0);
245 assert_eq!(page[9], 9);
246 }
247
248 #[test]
249 fn paginate_slice_second_page() {
250 let items: Vec<i32> = (0..25).collect();
251 let (page, has_more) = paginate_slice(&items, 10, 10);
252 assert_eq!(page.len(), 10);
253 assert!(has_more);
254 assert_eq!(page[0], 10);
255 assert_eq!(page[9], 19);
256 }
257
258 #[test]
259 fn paginate_slice_last_page() {
260 let items: Vec<i32> = (0..25).collect();
261 let (page, has_more) = paginate_slice(&items, 20, 10);
262 assert_eq!(page.len(), 5);
263 assert!(!has_more);
264 assert_eq!(page[0], 20);
265 assert_eq!(page[4], 24);
266 }
267
268 #[test]
269 fn paginate_slice_empty_at_end() {
270 let items: Vec<i32> = (0..10).collect();
271 let (page, has_more) = paginate_slice(&items, 10, 10);
272 assert!(page.is_empty());
273 assert!(!has_more);
274 }
275
276 #[test]
277 fn paginate_slice_empty_input() {
278 let items: Vec<i32> = vec![];
279 let (page, has_more) = paginate_slice(&items, 0, 10);
280 assert!(page.is_empty());
281 assert!(!has_more);
282 }
283
284 #[test]
285 fn query_state_empty_detection() {
286 let state: QueryState<i32> = QueryState::empty();
287 assert!(state.is_empty());
288 assert!(!state.is_expired());
289 }
290
291 #[test]
292 fn query_state_reset() {
293 let mut state: QueryState<i32> = QueryState::empty();
294 assert!(state.is_empty());
295
296 state.reset(vec![1, 2, 3], (), 10);
297 assert!(!state.is_empty());
298 assert_eq!(state.results.len(), 3);
299 assert_eq!(state.page_size, 10);
300 assert_eq!(state.next_offset, 0);
301 }
302
303 #[test]
304 fn query_state_with_meta() {
305 let mut state: QueryState<i32, Vec<String>> = QueryState::with_ttl(DEFAULT_TTL);
306 state.reset(vec![1, 2], vec!["warning".into()], 10);
307 assert_eq!(state.meta.len(), 1);
308 assert_eq!(state.meta[0], "warning");
309 }
310
311 #[test]
312 fn pagination_cache_get_or_create() {
313 let cache: PaginationCache<i32> = PaginationCache::new();
314
315 let lock1 = cache.get_or_create("key1");
317
318 let lock2 = cache.get_or_create("key1");
320 assert!(Arc::ptr_eq(&lock1, &lock2));
321
322 let lock3 = cache.get_or_create("key2");
324 assert!(!Arc::ptr_eq(&lock1, &lock3));
325 }
326
327 #[test]
328 fn pagination_cache_remove_if_same() {
329 let cache: PaginationCache<i32> = PaginationCache::new();
330
331 let lock1 = cache.get_or_create("key1");
332
333 cache.remove_if_same("key1", &lock1);
335
336 let lock2 = cache.get_or_create("key1");
338 assert!(!Arc::ptr_eq(&lock1, &lock2));
339 }
340
341 #[test]
342 fn pagination_cache_remove_if_same_ignores_mismatch() {
343 let cache: PaginationCache<i32> = PaginationCache::new();
344
345 let lock1 = cache.get_or_create("key1");
346
347 let different_lock = Arc::new(QueryLock::<i32>::new());
349
350 cache.remove_if_same("key1", &different_lock);
352
353 let lock2 = cache.get_or_create("key1");
355 assert!(Arc::ptr_eq(&lock1, &lock2));
356 }
357
358 #[test]
359 fn sweep_expired_removes_expired_entries() {
360 let cache: PaginationCache<i32> = PaginationCache::new();
361
362 let lock = cache.get_or_create("key1");
364
365 {
367 let mut st = lock.state.lock().unwrap();
368 st.created_at = Instant::now() - Duration::from_secs(6 * 60);
369 }
370
371 cache.sweep_expired();
373
374 let lock2 = cache.get_or_create("key1");
376 assert!(!Arc::ptr_eq(&lock, &lock2));
377 }
378
379 #[test]
380 fn sweep_expired_keeps_fresh_entries() {
381 let cache: PaginationCache<i32> = PaginationCache::new();
382
383 let lock1 = cache.get_or_create("key1");
385
386 cache.sweep_expired();
388
389 let lock2 = cache.get_or_create("key1");
391 assert!(Arc::ptr_eq(&lock1, &lock2));
392 }
393}