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