Skip to main content

rustrails_support/
cache.rs

1use std::collections::HashMap;
2use std::time::{Duration as StdDuration, Instant};
3
4use dashmap::DashMap;
5use serde_json::Value;
6
7/// Options that control cache read and write behavior.
8#[derive(Debug, Clone, Default, PartialEq, Eq)]
9pub struct CacheOptions {
10    /// Time-to-live for the entry from the moment it is written.
11    pub expires_in: Option<StdDuration>,
12    /// Optional version used to invalidate stale entries.
13    pub version: Option<String>,
14}
15
16/// A cache entry stored by a cache backend.
17#[derive(Debug, Clone)]
18pub struct CacheEntry {
19    /// Cached JSON value.
20    pub value: Value,
21    /// Optional application-defined version string.
22    pub version: Option<String>,
23    /// Absolute expiration instant, if the entry expires.
24    pub expires_at: Option<Instant>,
25    /// Instant when the entry was created.
26    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
42/// Shared behavior implemented by cache backends.
43pub trait CacheStore: Send + Sync {
44    /// Reads a cached value for `key`.
45    fn read(&self, key: &str) -> Option<Value>;
46
47    /// Writes a value under `key` with the given options.
48    fn write(&self, key: &str, value: Value, options: CacheOptions);
49
50    /// Deletes `key`, returning `true` when an entry existed.
51    fn delete(&self, key: &str) -> bool;
52
53    /// Returns `true` when a live entry exists for `key`.
54    fn exist(&self, key: &str) -> bool;
55
56    /// Reads a cached value or computes and stores it on a miss.
57    fn fetch(&self, key: &str, options: CacheOptions, f: impl FnOnce() -> Value) -> Value;
58
59    /// Increments an integer entry by `amount`.
60    fn increment(&self, key: &str, amount: i64) -> Option<i64>;
61
62    /// Decrements an integer entry by `amount`.
63    fn decrement(&self, key: &str, amount: i64) -> Option<i64>;
64
65    /// Removes all entries from the store.
66    fn clear(&self);
67
68    /// Reads many keys at once, omitting misses.
69    fn read_multi(&self, keys: &[&str]) -> HashMap<String, Value>;
70
71    /// Writes many key/value pairs with shared options.
72    fn write_multi(&self, entries: HashMap<String, Value>, options: CacheOptions);
73}
74
75/// A thread-safe in-memory cache store backed by `DashMap`.
76#[derive(Debug, Default)]
77pub struct MemoryStore {
78    entries: DashMap<String, CacheEntry>,
79}
80
81impl MemoryStore {
82    /// Creates an empty memory store.
83    #[must_use]
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Reads a cached value using explicit options such as version matching.
89    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    /// Returns `true` when a live entry exists for `key` under the given options.
95    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}