1use std::collections::HashMap;
53use std::sync::Arc;
54use std::sync::Mutex;
55use std::time::Duration;
56use std::time::Instant;
57
58pub const DEFAULT_TTL: Duration = Duration::from_secs(5 * 60);
60
61#[derive(Default)]
66pub struct PaginationCache<T, M = ()> {
67 map: Mutex<HashMap<String, Arc<QueryLock<T, M>>>>,
68}
69
70impl<T, M> PaginationCache<T, M> {
71 pub fn new() -> Self {
73 Self {
74 map: Mutex::new(HashMap::new()),
75 }
76 }
77
78 #[expect(
81 clippy::unwrap_used,
82 reason = "Mutex poisoning indicates a prior panic. Fail fast for pagination cache map."
83 )]
84 fn lock_map(&self) -> std::sync::MutexGuard<'_, HashMap<String, Arc<QueryLock<T, M>>>> {
85 self.map.lock().unwrap()
86 }
87
88 pub fn remove_if_same(&self, key: &str, candidate: &Arc<QueryLock<T, M>>) {
93 let mut m = self.lock_map();
94 if let Some(existing) = m.get(key)
95 && Arc::ptr_eq(existing, candidate)
96 {
97 m.remove(key);
98 }
99 }
100}
101
102impl<T, M: Default> PaginationCache<T, M> {
103 pub fn get_or_create(&self, key: &str) -> Arc<QueryLock<T, M>> {
108 let mut m = self.lock_map();
109 let arc = m
110 .entry(key.to_string())
111 .or_insert_with(|| Arc::new(QueryLock::new()));
112 Arc::clone(arc)
113 }
114
115 pub fn sweep_expired(&self) {
120 let entries: Vec<(String, Arc<QueryLock<T, M>>)> = {
121 let m = self.lock_map();
122 m.iter().map(|(k, v)| (k.clone(), Arc::clone(v))).collect()
123 };
124
125 for (k, lk) in entries {
126 let expired = { lk.lock_state().is_expired() };
127 if expired {
128 let mut m = self.lock_map();
129 if let Some(existing) = m.get(&k)
130 && Arc::ptr_eq(existing, &lk)
131 {
132 m.remove(&k);
133 }
134 }
135 }
136 }
137}
138
139pub struct QueryLock<T, M = ()> {
141 pub state: Mutex<QueryState<T, M>>,
142}
143
144impl<T, M> QueryLock<T, M> {
145 #[expect(
148 clippy::unwrap_used,
149 reason = "Mutex poisoning indicates a prior panic. Fail fast to avoid \
150 inconsistent pagination state."
151 )]
152 pub fn lock_state(&self) -> std::sync::MutexGuard<'_, QueryState<T, M>> {
153 self.state.lock().unwrap()
154 }
155}
156
157impl<T, M: Default> QueryLock<T, M> {
158 pub fn new() -> Self {
160 Self {
161 state: Mutex::new(QueryState::with_ttl(DEFAULT_TTL)),
162 }
163 }
164}
165
166impl<T, M: Default> Default for QueryLock<T, M> {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172pub struct QueryState<T, M = ()> {
174 pub results: Vec<T>,
176 pub meta: M,
178 pub next_offset: usize,
180 pub page_size: usize,
182 pub created_at: Instant,
184 ttl: Duration,
186}
187
188impl<T> QueryState<T, ()> {
189 pub fn empty() -> Self {
191 Self {
192 results: Vec::new(),
193 meta: (),
194 next_offset: 0,
195 page_size: 0,
196 created_at: Instant::now(),
197 ttl: DEFAULT_TTL,
198 }
199 }
200}
201
202impl<T, M: Default> QueryState<T, M> {
203 pub fn with_ttl(ttl: Duration) -> Self {
205 Self {
206 results: Vec::new(),
207 meta: M::default(),
208 next_offset: 0,
209 page_size: 0,
210 created_at: Instant::now(),
211 ttl,
212 }
213 }
214
215 pub fn reset(&mut self, entries: Vec<T>, meta: M, page_size: usize) {
217 self.results = entries;
218 self.meta = meta;
219 self.next_offset = 0;
220 self.page_size = page_size;
221 self.created_at = Instant::now();
222 }
223
224 pub fn is_expired(&self) -> bool {
226 self.created_at.elapsed() >= self.ttl
227 }
228
229 pub fn is_empty(&self) -> bool {
231 self.results.is_empty() && self.page_size == 0
232 }
233}
234
235pub fn paginate_slice<T: Clone>(entries: &[T], offset: usize, page_size: usize) -> (Vec<T>, bool) {
247 if offset >= entries.len() {
248 return (vec![], false);
249 }
250 let end = (offset + page_size).min(entries.len());
251 let has_more = end < entries.len();
252 (entries[offset..end].to_vec(), has_more)
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn paginate_slice_first_page() {
261 let items: Vec<i32> = (0..25).collect();
262 let (page, has_more) = paginate_slice(&items, 0, 10);
263 assert_eq!(page.len(), 10);
264 assert!(has_more);
265 assert_eq!(page[0], 0);
266 assert_eq!(page[9], 9);
267 }
268
269 #[test]
270 fn paginate_slice_second_page() {
271 let items: Vec<i32> = (0..25).collect();
272 let (page, has_more) = paginate_slice(&items, 10, 10);
273 assert_eq!(page.len(), 10);
274 assert!(has_more);
275 assert_eq!(page[0], 10);
276 assert_eq!(page[9], 19);
277 }
278
279 #[test]
280 fn paginate_slice_last_page() {
281 let items: Vec<i32> = (0..25).collect();
282 let (page, has_more) = paginate_slice(&items, 20, 10);
283 assert_eq!(page.len(), 5);
284 assert!(!has_more);
285 assert_eq!(page[0], 20);
286 assert_eq!(page[4], 24);
287 }
288
289 #[test]
290 fn paginate_slice_empty_at_end() {
291 let items: Vec<i32> = (0..10).collect();
292 let (page, has_more) = paginate_slice(&items, 10, 10);
293 assert!(page.is_empty());
294 assert!(!has_more);
295 }
296
297 #[test]
298 fn paginate_slice_empty_input() {
299 let items: Vec<i32> = vec![];
300 let (page, has_more) = paginate_slice(&items, 0, 10);
301 assert!(page.is_empty());
302 assert!(!has_more);
303 }
304
305 #[test]
306 fn query_state_empty_detection() {
307 let state: QueryState<i32> = QueryState::empty();
308 assert!(state.is_empty());
309 assert!(!state.is_expired());
310 }
311
312 #[test]
313 fn query_state_reset() {
314 let mut state: QueryState<i32> = QueryState::empty();
315 assert!(state.is_empty());
316
317 state.reset(vec![1, 2, 3], (), 10);
318 assert!(!state.is_empty());
319 assert_eq!(state.results.len(), 3);
320 assert_eq!(state.page_size, 10);
321 assert_eq!(state.next_offset, 0);
322 }
323
324 #[test]
325 fn query_state_with_meta() {
326 let mut state: QueryState<i32, Vec<String>> = QueryState::with_ttl(DEFAULT_TTL);
327 state.reset(vec![1, 2], vec!["warning".into()], 10);
328 assert_eq!(state.meta.len(), 1);
329 assert_eq!(state.meta[0], "warning");
330 }
331
332 #[test]
333 fn pagination_cache_get_or_create() {
334 let cache: PaginationCache<i32> = PaginationCache::new();
335
336 let lock1 = cache.get_or_create("key1");
338
339 let lock2 = cache.get_or_create("key1");
341 assert!(Arc::ptr_eq(&lock1, &lock2));
342
343 let lock3 = cache.get_or_create("key2");
345 assert!(!Arc::ptr_eq(&lock1, &lock3));
346 }
347
348 #[test]
349 fn pagination_cache_remove_if_same() {
350 let cache: PaginationCache<i32> = PaginationCache::new();
351
352 let lock1 = cache.get_or_create("key1");
353
354 cache.remove_if_same("key1", &lock1);
356
357 let lock2 = cache.get_or_create("key1");
359 assert!(!Arc::ptr_eq(&lock1, &lock2));
360 }
361
362 #[test]
363 fn pagination_cache_remove_if_same_ignores_mismatch() {
364 let cache: PaginationCache<i32> = PaginationCache::new();
365
366 let lock1 = cache.get_or_create("key1");
367
368 let different_lock = Arc::new(QueryLock::<i32>::new());
370
371 cache.remove_if_same("key1", &different_lock);
373
374 let lock2 = cache.get_or_create("key1");
376 assert!(Arc::ptr_eq(&lock1, &lock2));
377 }
378
379 #[test]
380 fn sweep_expired_removes_expired_entries() {
381 let cache: PaginationCache<i32> = PaginationCache::new();
382
383 let lock = cache.get_or_create("key1");
385
386 {
388 let mut st = lock.state.lock().unwrap();
389 st.created_at = Instant::now()
390 .checked_sub(Duration::from_secs(6 * 60))
391 .unwrap();
392 }
393
394 cache.sweep_expired();
396
397 let lock2 = cache.get_or_create("key1");
399 assert!(!Arc::ptr_eq(&lock, &lock2));
400 }
401
402 #[test]
403 fn sweep_expired_keeps_fresh_entries() {
404 let cache: PaginationCache<i32> = PaginationCache::new();
405
406 let lock1 = cache.get_or_create("key1");
408
409 cache.sweep_expired();
411
412 let lock2 = cache.get_or_create("key1");
414 assert!(Arc::ptr_eq(&lock1, &lock2));
415 }
416}