1use std::collections::HashMap;
2use std::time::{Duration as StdDuration, Instant};
3
4use dashmap::DashMap;
5use serde_json::Value;
6
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct CacheOptions {
10 pub expires_in: Option<StdDuration>,
12 pub version: Option<String>,
14}
15
16#[derive(Debug, Clone)]
18pub struct CacheEntry {
19 pub value: Value,
21 pub version: Option<String>,
23 pub expires_at: Option<Instant>,
25 pub created_at: Instant,
27}
28
29impl CacheEntry {
30 fn is_expired(&self, now: Instant) -> bool {
31 self.expires_at.is_some_and(|expires_at| now >= expires_at)
32 }
33
34 fn version_matches(&self, requested: Option<&str>) -> bool {
35 match (self.version.as_deref(), requested) {
36 (Some(stored), Some(requested)) => stored == requested,
37 _ => true,
38 }
39 }
40}
41
42pub trait CacheStore: Send + Sync {
44 fn read(&self, key: &str) -> Option<Value>;
46
47 fn write(&self, key: &str, value: Value, options: CacheOptions);
49
50 fn delete(&self, key: &str) -> bool;
52
53 fn exist(&self, key: &str) -> bool;
55
56 fn fetch(&self, key: &str, options: CacheOptions, f: impl FnOnce() -> Value) -> Value;
58
59 fn increment(&self, key: &str, amount: i64) -> Option<i64>;
61
62 fn decrement(&self, key: &str, amount: i64) -> Option<i64>;
64
65 fn clear(&self);
67
68 fn read_multi(&self, keys: &[&str]) -> HashMap<String, Value>;
70
71 fn write_multi(&self, entries: HashMap<String, Value>, options: CacheOptions);
73}
74
75#[derive(Debug, Default)]
77pub struct MemoryStore {
78 entries: DashMap<String, CacheEntry>,
79}
80
81impl MemoryStore {
82 #[must_use]
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn read_with_options(&self, key: &str, options: &CacheOptions) -> Option<Value> {
90 self.read_entry(key, options)
91 .map(|entry| entry.value.clone())
92 }
93
94 pub fn exist_with_options(&self, key: &str, options: &CacheOptions) -> bool {
96 self.read_entry(key, options).is_some()
97 }
98
99 fn read_entry(&self, key: &str, options: &CacheOptions) -> Option<CacheEntry> {
100 let now = Instant::now();
101 let entry = self.entries.get(key)?.clone();
102
103 if entry.is_expired(now) {
104 self.entries.remove(key);
105 return None;
106 }
107
108 if !entry.version_matches(options.version.as_deref()) {
109 return None;
110 }
111
112 Some(entry)
113 }
114
115 fn make_entry(value: Value, options: CacheOptions) -> CacheEntry {
116 let created_at = Instant::now();
117 let expires_at = options
118 .expires_in
119 .and_then(|duration| created_at.checked_add(duration));
120
121 CacheEntry {
122 value,
123 version: options.version,
124 expires_at,
125 created_at,
126 }
127 }
128
129 fn adjust_counter(&self, key: &str, delta: i64) -> Option<i64> {
130 let now = Instant::now();
131 let mut entry = self.entries.get_mut(key)?;
132
133 if entry.is_expired(now) {
134 drop(entry);
135 self.entries.remove(key);
136 return None;
137 }
138
139 let current = entry.value.as_i64()?;
140 let updated = current.checked_add(delta)?;
141 entry.value = Value::from(updated);
142 Some(updated)
143 }
144}
145
146impl CacheStore for MemoryStore {
147 fn read(&self, key: &str) -> Option<Value> {
148 self.read_with_options(key, &CacheOptions::default())
149 }
150
151 fn write(&self, key: &str, value: Value, options: CacheOptions) {
152 self.entries
153 .insert(key.to_owned(), Self::make_entry(value, options));
154 }
155
156 fn delete(&self, key: &str) -> bool {
157 self.entries.remove(key).is_some()
158 }
159
160 fn exist(&self, key: &str) -> bool {
161 self.exist_with_options(key, &CacheOptions::default())
162 }
163
164 fn fetch(&self, key: &str, options: CacheOptions, f: impl FnOnce() -> Value) -> Value {
165 if let Some(value) = self.read_with_options(key, &options) {
166 return value;
167 }
168
169 let value = f();
170 self.write(key, value.clone(), options);
171 value
172 }
173
174 fn increment(&self, key: &str, amount: i64) -> Option<i64> {
175 self.adjust_counter(key, amount)
176 }
177
178 fn decrement(&self, key: &str, amount: i64) -> Option<i64> {
179 self.adjust_counter(key, amount.checked_neg()?)
180 }
181
182 fn clear(&self) {
183 self.entries.clear();
184 }
185
186 fn read_multi(&self, keys: &[&str]) -> HashMap<String, Value> {
187 keys.iter()
188 .filter_map(|key| self.read(key).map(|value| ((*key).to_owned(), value)))
189 .collect()
190 }
191
192 fn write_multi(&self, entries: HashMap<String, Value>, options: CacheOptions) {
193 for (key, value) in entries {
194 self.write(&key, value, options.clone());
195 }
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use std::sync::Arc;
202 use std::sync::atomic::{AtomicUsize, Ordering};
203 use std::thread;
204 use std::time::Duration as StdDuration;
205
206 use super::*;
207
208 #[test]
209 fn read_write_round_trip() {
210 let store = MemoryStore::new();
211 store.write("city", Value::from("Duckburg"), CacheOptions::default());
212
213 assert_eq!(store.read("city"), Some(Value::from("Duckburg")));
214 }
215
216 #[test]
217 fn read_missing_returns_none() {
218 let store = MemoryStore::new();
219
220 assert_eq!(store.read("missing"), None);
221 }
222
223 #[test]
224 fn expired_entries_are_missed_and_removed() {
225 let store = MemoryStore::new();
226 store.write(
227 "token",
228 Value::from("abc"),
229 CacheOptions {
230 expires_in: Some(StdDuration::from_millis(20)),
231 version: None,
232 },
233 );
234
235 thread::sleep(StdDuration::from_millis(40));
236
237 assert_eq!(store.read("token"), None);
238 assert!(!store.entries.contains_key("token"));
239 }
240
241 #[test]
242 fn fetch_computes_on_miss_and_caches_value() {
243 let store = MemoryStore::new();
244 let calls = AtomicUsize::new(0);
245
246 let first = store.fetch("answer", CacheOptions::default(), || {
247 calls.fetch_add(1, Ordering::SeqCst);
248 Value::from(42)
249 });
250 let second = store.fetch("answer", CacheOptions::default(), || {
251 calls.fetch_add(1, Ordering::SeqCst);
252 Value::from(100)
253 });
254
255 assert_eq!(first, Value::from(42));
256 assert_eq!(second, Value::from(42));
257 assert_eq!(calls.load(Ordering::SeqCst), 1);
258 }
259
260 #[test]
261 fn fetch_recomputes_when_version_mismatches() {
262 let store = MemoryStore::new();
263 store.write(
264 "user",
265 Value::from("v1"),
266 CacheOptions {
267 expires_in: None,
268 version: Some("1".to_owned()),
269 },
270 );
271
272 let value = store.fetch(
273 "user",
274 CacheOptions {
275 expires_in: None,
276 version: Some("2".to_owned()),
277 },
278 || Value::from("v2"),
279 );
280
281 assert_eq!(value, Value::from("v2"));
282 assert_eq!(
283 store.read_with_options(
284 "user",
285 &CacheOptions {
286 expires_in: None,
287 version: Some("2".to_owned()),
288 },
289 ),
290 Some(Value::from("v2"))
291 );
292 }
293
294 #[test]
295 fn delete_removes_entry() {
296 let store = MemoryStore::new();
297 store.write("city", Value::from("Duckburg"), CacheOptions::default());
298
299 assert!(store.delete("city"));
300 assert_eq!(store.read("city"), None);
301 assert!(!store.delete("city"));
302 }
303
304 #[test]
305 fn exist_checks_presence() {
306 let store = MemoryStore::new();
307 store.write("present", Value::from(true), CacheOptions::default());
308
309 assert!(store.exist("present"));
310 assert!(!store.exist("missing"));
311 }
312
313 #[test]
314 fn version_mismatch_returns_none() {
315 let store = MemoryStore::new();
316 store.write(
317 "article",
318 Value::from("body"),
319 CacheOptions {
320 expires_in: None,
321 version: Some("a".to_owned()),
322 },
323 );
324
325 assert_eq!(
326 store.read_with_options(
327 "article",
328 &CacheOptions {
329 expires_in: None,
330 version: Some("b".to_owned()),
331 },
332 ),
333 None
334 );
335 assert_eq!(
336 store.read_with_options(
337 "article",
338 &CacheOptions {
339 expires_in: None,
340 version: Some("a".to_owned()),
341 },
342 ),
343 Some(Value::from("body"))
344 );
345 }
346
347 #[test]
348 fn read_without_requested_version_accepts_versioned_entry() {
349 let store = MemoryStore::new();
350 store.write(
351 "article",
352 Value::from("body"),
353 CacheOptions {
354 expires_in: None,
355 version: Some("a".to_owned()),
356 },
357 );
358
359 assert_eq!(store.read("article"), Some(Value::from("body")));
360 }
361
362 #[test]
363 fn increment_and_decrement_update_integer_values() {
364 let store = MemoryStore::new();
365 store.write("counter", Value::from(10), CacheOptions::default());
366
367 assert_eq!(store.increment("counter", 5), Some(15));
368 assert_eq!(store.decrement("counter", 3), Some(12));
369 assert_eq!(store.read("counter"), Some(Value::from(12)));
370 }
371
372 #[test]
373 fn increment_returns_none_for_missing_or_non_integer_entries() {
374 let store = MemoryStore::new();
375 store.write("counter", Value::from("ten"), CacheOptions::default());
376
377 assert_eq!(store.increment("missing", 1), None);
378 assert_eq!(store.increment("counter", 1), None);
379 }
380
381 #[test]
382 fn decrement_returns_none_when_amount_would_overflow() {
383 let store = MemoryStore::new();
384 store.write("counter", Value::from(1), CacheOptions::default());
385
386 assert_eq!(store.decrement("counter", i64::MIN), None);
387 }
388
389 #[test]
390 fn clear_removes_everything() {
391 let store = MemoryStore::new();
392 store.write("a", Value::from(1), CacheOptions::default());
393 store.write("b", Value::from(2), CacheOptions::default());
394
395 store.clear();
396
397 assert_eq!(store.read("a"), None);
398 assert_eq!(store.read("b"), None);
399 }
400
401 #[test]
402 fn read_multi_returns_hits_only() {
403 let store = MemoryStore::new();
404 store.write("a", Value::from(1), CacheOptions::default());
405 store.write("b", Value::from(2), CacheOptions::default());
406
407 let result = store.read_multi(&["a", "b", "c"]);
408
409 assert_eq!(result.len(), 2);
410 assert_eq!(result.get("a"), Some(&Value::from(1)));
411 assert_eq!(result.get("b"), Some(&Value::from(2)));
412 assert_eq!(result.get("c"), None);
413 }
414
415 #[test]
416 fn write_multi_writes_every_entry() {
417 let store = MemoryStore::new();
418 let mut entries = HashMap::new();
419 entries.insert("a".to_owned(), Value::from(1));
420 entries.insert("b".to_owned(), Value::from(2));
421
422 store.write_multi(entries, CacheOptions::default());
423
424 assert_eq!(store.read("a"), Some(Value::from(1)));
425 assert_eq!(store.read("b"), Some(Value::from(2)));
426 }
427
428 #[test]
429 fn write_multi_applies_shared_options() {
430 let store = MemoryStore::new();
431 let mut entries = HashMap::new();
432 entries.insert("a".to_owned(), Value::from(1));
433 entries.insert("b".to_owned(), Value::from(2));
434
435 store.write_multi(
436 entries,
437 CacheOptions {
438 expires_in: Some(StdDuration::from_millis(10)),
439 version: Some("1".to_owned()),
440 },
441 );
442 thread::sleep(StdDuration::from_millis(20));
443
444 assert_eq!(store.read("a"), None);
445 assert_eq!(store.read("b"), None);
446 }
447
448 #[test]
449 fn increment_preserves_expiry() {
450 let store = MemoryStore::new();
451 store.write(
452 "counter",
453 Value::from(1),
454 CacheOptions {
455 expires_in: Some(StdDuration::from_millis(20)),
456 version: None,
457 },
458 );
459
460 assert_eq!(store.increment("counter", 1), Some(2));
461 thread::sleep(StdDuration::from_millis(30));
462
463 assert_eq!(store.read("counter"), None);
464 }
465
466 #[test]
467 fn increment_preserves_version() {
468 let store = MemoryStore::new();
469 store.write(
470 "counter",
471 Value::from(1),
472 CacheOptions {
473 expires_in: None,
474 version: Some("1".to_owned()),
475 },
476 );
477
478 assert_eq!(store.increment("counter", 1), Some(2));
479 assert_eq!(
480 store.read_with_options(
481 "counter",
482 &CacheOptions {
483 expires_in: None,
484 version: Some("1".to_owned()),
485 },
486 ),
487 Some(Value::from(2))
488 );
489 }
490
491 #[test]
492 fn expired_entry_counts_as_absent_for_existence_checks() {
493 let store = MemoryStore::new();
494 store.write(
495 "session",
496 Value::from(true),
497 CacheOptions {
498 expires_in: Some(StdDuration::from_millis(10)),
499 version: None,
500 },
501 );
502 thread::sleep(StdDuration::from_millis(20));
503
504 assert!(!store.exist("session"));
505 }
506
507 #[test]
508 fn concurrent_increment_is_thread_safe() {
509 let store = Arc::new(MemoryStore::new());
510 store.write("counter", Value::from(0), CacheOptions::default());
511
512 let mut handles = Vec::new();
513 for _ in 0..8 {
514 let store = Arc::clone(&store);
515 handles.push(thread::spawn(move || {
516 for _ in 0..100 {
517 let _ = store.increment("counter", 1);
518 }
519 }));
520 }
521
522 for handle in handles {
523 handle.join().unwrap();
524 }
525
526 assert_eq!(store.read("counter"), Some(Value::from(800)));
527 }
528
529 #[test]
530 fn concurrent_reads_and_writes_are_safe() {
531 let store = Arc::new(MemoryStore::new());
532 let mut handles = Vec::new();
533
534 for i in 0..4 {
535 let store = Arc::clone(&store);
536 handles.push(thread::spawn(move || {
537 for j in 0..100 {
538 store.write(
539 &format!("key-{i}-{j}"),
540 Value::from(j as i64),
541 CacheOptions::default(),
542 );
543 }
544 }));
545 }
546
547 for i in 0..4 {
548 let store = Arc::clone(&store);
549 handles.push(thread::spawn(move || {
550 for j in 0..100 {
551 let _ = store.read(&format!("key-{i}-{j}"));
552 }
553 }));
554 }
555
556 for handle in handles {
557 handle.join().unwrap();
558 }
559
560 assert!(store.read("key-0-99").is_some());
561 assert!(store.read("key-3-99").is_some());
562 }
563
564 #[test]
565 fn fetch_recomputes_after_expiration() {
566 let store = MemoryStore::new();
567 let calls = AtomicUsize::new(0);
568
569 let first = store.fetch(
570 "token",
571 CacheOptions {
572 expires_in: Some(StdDuration::from_millis(10)),
573 version: None,
574 },
575 || {
576 calls.fetch_add(1, Ordering::SeqCst);
577 Value::from("first")
578 },
579 );
580 thread::sleep(StdDuration::from_millis(20));
581
582 let second = store.fetch("token", CacheOptions::default(), || {
583 calls.fetch_add(1, Ordering::SeqCst);
584 Value::from("second")
585 });
586
587 assert_eq!(first, Value::from("first"));
588 assert_eq!(second, Value::from("second"));
589 assert_eq!(store.read("token"), Some(Value::from("second")));
590 assert_eq!(calls.load(Ordering::SeqCst), 2);
591 }
592
593 #[test]
594 fn fetch_uses_cached_null_without_recomputing() {
595 let store = MemoryStore::new();
596 let calls = AtomicUsize::new(0);
597 store.write("nothing", Value::Null, CacheOptions::default());
598
599 let value = store.fetch("nothing", CacheOptions::default(), || {
600 calls.fetch_add(1, Ordering::SeqCst);
601 Value::from("fallback")
602 });
603
604 assert_eq!(value, Value::Null);
605 assert_eq!(calls.load(Ordering::SeqCst), 0);
606 }
607
608 #[test]
609 fn exist_with_options_respects_version_matching() {
610 let store = MemoryStore::new();
611 store.write(
612 "article",
613 Value::from("body"),
614 CacheOptions {
615 expires_in: None,
616 version: Some("v1".to_owned()),
617 },
618 );
619
620 assert!(store.exist_with_options(
621 "article",
622 &CacheOptions {
623 expires_in: None,
624 version: Some("v1".to_owned()),
625 },
626 ));
627 assert!(!store.exist_with_options(
628 "article",
629 &CacheOptions {
630 expires_in: None,
631 version: Some("v2".to_owned()),
632 },
633 ));
634 assert!(store.exist("article"));
635 }
636
637 #[test]
638 fn read_multi_omits_expired_entries() {
639 let store = MemoryStore::new();
640 store.write(
641 "stale",
642 Value::from("old"),
643 CacheOptions {
644 expires_in: Some(StdDuration::from_millis(10)),
645 version: None,
646 },
647 );
648 store.write("fresh", Value::from("new"), CacheOptions::default());
649 thread::sleep(StdDuration::from_millis(20));
650
651 let result = store.read_multi(&["stale", "fresh"]);
652
653 assert_eq!(result.len(), 1);
654 assert_eq!(result.get("stale"), None);
655 assert_eq!(result.get("fresh"), Some(&Value::from("new")));
656 assert!(!store.entries.contains_key("stale"));
657 }
658
659 #[test]
660 fn decrement_returns_none_for_missing_or_non_integer_entries() {
661 let store = MemoryStore::new();
662 store.write("counter", Value::from("ten"), CacheOptions::default());
663
664 assert_eq!(store.decrement("missing", 1), None);
665 assert_eq!(store.decrement("counter", 1), None);
666 }
667
668 fn versioned(version: &str) -> CacheOptions {
669 CacheOptions {
670 expires_in: None,
671 version: Some(version.to_owned()),
672 }
673 }
674
675 macro_rules! non_integer_counter_case {
676 ($name:ident, $method:ident, $value:expr) => {
677 #[test]
678 fn $name() {
679 let store = MemoryStore::new();
680 store.write("counter", $value, CacheOptions::default());
681
682 assert_eq!(store.$method("counter", 1), None);
683 assert!(store.exist("counter"));
684 }
685 };
686 }
687
688 macro_rules! read_or_exist_version_case {
689 ($name:ident, exist, $stored:expr, $expected:expr) => {
690 #[test]
691 fn $name() {
692 let store = MemoryStore::new();
693 store.write("entry", Value::from("cached"), $stored);
694
695 assert!(store.exist_with_options("entry", &$expected));
696 }
697 };
698 ($name:ident, $method:ident, $stored:expr, $expected:expr) => {
699 #[test]
700 fn $name() {
701 let store = MemoryStore::new();
702 store.write("entry", Value::from("cached"), $stored);
703
704 assert_eq!(
705 store.$method("entry", &$expected),
706 Some(Value::from("cached"))
707 );
708 }
709 };
710 }
711
712 macro_rules! cached_fetch_case {
713 ($name:ident, $stored:expr, $requested:expr) => {
714 #[test]
715 fn $name() {
716 let store = MemoryStore::new();
717 let calls = AtomicUsize::new(0);
718 store.write("entry", Value::from("cached"), $stored);
719
720 let value = store.fetch("entry", $requested, || {
721 calls.fetch_add(1, Ordering::SeqCst);
722 Value::from("fresh")
723 });
724
725 assert_eq!(value, Value::from("cached"));
726 assert_eq!(calls.load(Ordering::SeqCst), 0);
727 }
728 };
729 }
730
731 read_or_exist_version_case!(
732 read_with_matching_version_returns_cached_value,
733 read_with_options,
734 versioned("v1"),
735 versioned("v1")
736 );
737 read_or_exist_version_case!(
738 read_with_requested_version_accepts_unversioned_entry,
739 read_with_options,
740 CacheOptions::default(),
741 versioned("v1")
742 );
743 read_or_exist_version_case!(
744 exist_with_requested_version_accepts_unversioned_entry,
745 exist,
746 CacheOptions::default(),
747 versioned("v1")
748 );
749 read_or_exist_version_case!(
750 read_with_options_without_version_reads_versioned_entry,
751 read_with_options,
752 versioned("v1"),
753 CacheOptions::default()
754 );
755
756 cached_fetch_case!(
757 fetch_with_matching_version_uses_cached_value,
758 versioned("v1"),
759 versioned("v1")
760 );
761 cached_fetch_case!(
762 fetch_without_requested_version_uses_versioned_entry,
763 versioned("v1"),
764 CacheOptions::default()
765 );
766 cached_fetch_case!(
767 fetch_with_requested_version_uses_unversioned_entry,
768 CacheOptions::default(),
769 versioned("v1")
770 );
771
772 non_integer_counter_case!(
773 increment_rejects_boolean_counter,
774 increment,
775 Value::Bool(true)
776 );
777 non_integer_counter_case!(increment_rejects_null_counter, increment, Value::Null);
778 non_integer_counter_case!(
779 decrement_rejects_boolean_counter,
780 decrement,
781 Value::Bool(true)
782 );
783 non_integer_counter_case!(decrement_rejects_null_counter, decrement, Value::Null);
784
785 #[test]
786 fn write_overwrites_existing_value() {
787 let store = MemoryStore::new();
788 store.write("city", Value::from("Duckburg"), CacheOptions::default());
789 store.write("city", Value::from("St. Canard"), CacheOptions::default());
790
791 assert_eq!(store.read("city"), Some(Value::from("St. Canard")));
792 }
793
794 #[test]
795 fn write_overwrites_existing_version() {
796 let store = MemoryStore::new();
797 store.write("article", Value::from("v1"), versioned("v1"));
798 store.write("article", Value::from("v2"), versioned("v2"));
799
800 assert_eq!(store.read_with_options("article", &versioned("v1")), None);
801 assert_eq!(
802 store.read_with_options("article", &versioned("v2")),
803 Some(Value::from("v2"))
804 );
805 }
806
807 #[test]
808 fn delete_missing_entry_returns_false() {
809 let store = MemoryStore::new();
810
811 assert!(!store.delete("missing"));
812 }
813
814 #[test]
815 fn delete_expired_entry_returns_true() {
816 let store = MemoryStore::new();
817 store.write(
818 "session",
819 Value::from(true),
820 CacheOptions {
821 expires_in: Some(StdDuration::from_millis(10)),
822 version: None,
823 },
824 );
825 thread::sleep(StdDuration::from_millis(20));
826
827 assert!(store.delete("session"));
828 assert_eq!(store.read("session"), None);
829 }
830
831 #[test]
832 fn increment_overflow_returns_none_and_keeps_value() {
833 let store = MemoryStore::new();
834 store.write("counter", Value::from(i64::MAX), CacheOptions::default());
835
836 assert_eq!(store.increment("counter", 1), None);
837 assert_eq!(store.read("counter"), Some(Value::from(i64::MAX)));
838 }
839
840 #[test]
841 fn decrement_underflow_returns_none_and_keeps_value() {
842 let store = MemoryStore::new();
843 store.write("counter", Value::from(i64::MIN), CacheOptions::default());
844
845 assert_eq!(store.decrement("counter", 1), None);
846 assert_eq!(store.read("counter"), Some(Value::from(i64::MIN)));
847 }
848
849 #[test]
850 fn increment_by_zero_returns_current_value() {
851 let store = MemoryStore::new();
852 store.write("counter", Value::from(9), CacheOptions::default());
853
854 assert_eq!(store.increment("counter", 0), Some(9));
855 assert_eq!(store.read("counter"), Some(Value::from(9)));
856 }
857
858 #[test]
859 fn decrement_by_zero_returns_current_value() {
860 let store = MemoryStore::new();
861 store.write("counter", Value::from(9), CacheOptions::default());
862
863 assert_eq!(store.decrement("counter", 0), Some(9));
864 assert_eq!(store.read("counter"), Some(Value::from(9)));
865 }
866
867 #[test]
868 fn decrement_preserves_expiry() {
869 let store = MemoryStore::new();
870 store.write(
871 "counter",
872 Value::from(3),
873 CacheOptions {
874 expires_in: Some(StdDuration::from_millis(10)),
875 version: None,
876 },
877 );
878
879 assert_eq!(store.decrement("counter", 1), Some(2));
880 thread::sleep(StdDuration::from_millis(20));
881
882 assert_eq!(store.read("counter"), None);
883 }
884
885 #[test]
886 fn decrement_preserves_version() {
887 let store = MemoryStore::new();
888 store.write("counter", Value::from(3), versioned("v1"));
889
890 assert_eq!(store.decrement("counter", 1), Some(2));
891 assert_eq!(
892 store.read_with_options("counter", &versioned("v1")),
893 Some(Value::from(2))
894 );
895 }
896
897 #[test]
898 fn decrement_on_expired_entry_removes_it() {
899 let store = MemoryStore::new();
900 store.write(
901 "counter",
902 Value::from(3),
903 CacheOptions {
904 expires_in: Some(StdDuration::from_millis(10)),
905 version: None,
906 },
907 );
908 thread::sleep(StdDuration::from_millis(20));
909
910 assert_eq!(store.decrement("counter", 1), None);
911 assert!(!store.entries.contains_key("counter"));
912 }
913
914 #[test]
915 fn increment_on_expired_entry_removes_it() {
916 let store = MemoryStore::new();
917 store.write(
918 "counter",
919 Value::from(3),
920 CacheOptions {
921 expires_in: Some(StdDuration::from_millis(10)),
922 version: None,
923 },
924 );
925 thread::sleep(StdDuration::from_millis(20));
926
927 assert_eq!(store.increment("counter", 1), None);
928 assert!(!store.entries.contains_key("counter"));
929 }
930
931 #[test]
932 fn write_multi_overwrites_existing_entries() {
933 let store = MemoryStore::new();
934 store.write("a", Value::from(1), CacheOptions::default());
935
936 let mut entries = HashMap::new();
937 entries.insert("a".to_owned(), Value::from(2));
938 entries.insert("b".to_owned(), Value::from(3));
939 store.write_multi(entries, CacheOptions::default());
940
941 assert_eq!(store.read("a"), Some(Value::from(2)));
942 assert_eq!(store.read("b"), Some(Value::from(3)));
943 }
944
945 #[test]
946 fn write_multi_clones_version_to_each_entry() {
947 let store = MemoryStore::new();
948 let mut entries = HashMap::new();
949 entries.insert("a".to_owned(), Value::from(1));
950 entries.insert("b".to_owned(), Value::from(2));
951 store.write_multi(entries, versioned("shared"));
952
953 assert_eq!(
954 store.read_with_options("a", &versioned("shared")),
955 Some(Value::from(1))
956 );
957 assert_eq!(
958 store.read_with_options("b", &versioned("shared")),
959 Some(Value::from(2))
960 );
961 }
962
963 #[test]
964 fn read_multi_empty_slice_returns_empty_map() {
965 let store = MemoryStore::new();
966
967 assert!(store.read_multi(&[]).is_empty());
968 }
969
970 #[test]
971 fn read_multi_duplicate_keys_return_single_entry() {
972 let store = MemoryStore::new();
973 store.write("a", Value::from(1), CacheOptions::default());
974
975 let result = store.read_multi(&["a", "a", "a"]);
976
977 assert_eq!(result.len(), 1);
978 assert_eq!(result.get("a"), Some(&Value::from(1)));
979 }
980
981 #[test]
982 fn clear_on_empty_store_is_safe() {
983 let store = MemoryStore::new();
984 store.clear();
985
986 assert!(store.read_multi(&[]).is_empty());
987 }
988
989 #[test]
990 fn read_with_options_on_expired_entry_removes_it() {
991 let store = MemoryStore::new();
992 store.write(
993 "stale",
994 Value::from("old"),
995 CacheOptions {
996 expires_in: Some(StdDuration::from_millis(10)),
997 version: Some("v1".to_owned()),
998 },
999 );
1000 thread::sleep(StdDuration::from_millis(20));
1001
1002 assert_eq!(store.read_with_options("stale", &versioned("v1")), None);
1003 assert!(!store.entries.contains_key("stale"));
1004 }
1005
1006 #[test]
1007 fn exist_with_options_on_expired_entry_removes_it() {
1008 let store = MemoryStore::new();
1009 store.write(
1010 "stale",
1011 Value::from("old"),
1012 CacheOptions {
1013 expires_in: Some(StdDuration::from_millis(10)),
1014 version: Some("v1".to_owned()),
1015 },
1016 );
1017 thread::sleep(StdDuration::from_millis(20));
1018
1019 assert!(!store.exist_with_options("stale", &versioned("v1")));
1020 assert!(!store.entries.contains_key("stale"));
1021 }
1022
1023 #[test]
1024 fn fetch_rewrites_expired_entry_with_new_version() {
1025 let store = MemoryStore::new();
1026 store.write(
1027 "token",
1028 Value::from("old"),
1029 CacheOptions {
1030 expires_in: Some(StdDuration::from_millis(10)),
1031 version: Some("old".to_owned()),
1032 },
1033 );
1034 thread::sleep(StdDuration::from_millis(20));
1035
1036 let value = store.fetch("token", versioned("new"), || Value::from("fresh"));
1037
1038 assert_eq!(value, Value::from("fresh"));
1039 assert_eq!(store.read_with_options("token", &versioned("old")), None);
1040 assert_eq!(
1041 store.read_with_options("token", &versioned("new")),
1042 Some(Value::from("fresh"))
1043 );
1044 }
1045
1046 #[test]
1047 fn fetch_stores_ttl_on_miss_and_entry_expires() {
1048 let store = MemoryStore::new();
1049 let value = store.fetch(
1050 "ephemeral",
1051 CacheOptions {
1052 expires_in: Some(StdDuration::from_millis(10)),
1053 version: None,
1054 },
1055 || Value::from("value"),
1056 );
1057
1058 assert_eq!(value, Value::from("value"));
1059 thread::sleep(StdDuration::from_millis(20));
1060 assert_eq!(store.read("ephemeral"), None);
1061 }
1062
1063 #[test]
1064 fn expired_entry_can_be_rewritten_by_write() {
1065 let store = MemoryStore::new();
1066 store.write(
1067 "entry",
1068 Value::from("old"),
1069 CacheOptions {
1070 expires_in: Some(StdDuration::from_millis(10)),
1071 version: None,
1072 },
1073 );
1074 thread::sleep(StdDuration::from_millis(20));
1075 store.write("entry", Value::from("new"), CacheOptions::default());
1076
1077 assert_eq!(store.read("entry"), Some(Value::from("new")));
1078 }
1079
1080 #[test]
1081 fn fetch_after_delete_recomputes_value() {
1082 let store = MemoryStore::new();
1083 let calls = AtomicUsize::new(0);
1084 store.write("entry", Value::from("cached"), CacheOptions::default());
1085 assert!(store.delete("entry"));
1086
1087 let value = store.fetch("entry", CacheOptions::default(), || {
1088 calls.fetch_add(1, Ordering::SeqCst);
1089 Value::from("fresh")
1090 });
1091
1092 assert_eq!(value, Value::from("fresh"));
1093 assert_eq!(calls.load(Ordering::SeqCst), 1);
1094 }
1095
1096 #[test]
1097 fn write_allows_null_values() {
1098 let store = MemoryStore::new();
1099 store.write("entry", Value::Null, CacheOptions::default());
1100
1101 assert_eq!(store.read("entry"), Some(Value::Null));
1102 assert!(store.exist("entry"));
1103 }
1104}