use std::collections::HashMap;
use std::time::{Duration as StdDuration, Instant};
use dashmap::DashMap;
use serde_json::Value;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CacheOptions {
pub expires_in: Option<StdDuration>,
pub version: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub value: Value,
pub version: Option<String>,
pub expires_at: Option<Instant>,
pub created_at: Instant,
}
impl CacheEntry {
fn is_expired(&self, now: Instant) -> bool {
self.expires_at.is_some_and(|expires_at| now >= expires_at)
}
fn version_matches(&self, requested: Option<&str>) -> bool {
match (self.version.as_deref(), requested) {
(Some(stored), Some(requested)) => stored == requested,
_ => true,
}
}
}
pub trait CacheStore: Send + Sync {
fn read(&self, key: &str) -> Option<Value>;
fn write(&self, key: &str, value: Value, options: CacheOptions);
fn delete(&self, key: &str) -> bool;
fn exist(&self, key: &str) -> bool;
fn fetch(&self, key: &str, options: CacheOptions, f: impl FnOnce() -> Value) -> Value;
fn increment(&self, key: &str, amount: i64) -> Option<i64>;
fn decrement(&self, key: &str, amount: i64) -> Option<i64>;
fn clear(&self);
fn read_multi(&self, keys: &[&str]) -> HashMap<String, Value>;
fn write_multi(&self, entries: HashMap<String, Value>, options: CacheOptions);
}
#[derive(Debug, Default)]
pub struct MemoryStore {
entries: DashMap<String, CacheEntry>,
}
impl MemoryStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn read_with_options(&self, key: &str, options: &CacheOptions) -> Option<Value> {
self.read_entry(key, options)
.map(|entry| entry.value.clone())
}
pub fn exist_with_options(&self, key: &str, options: &CacheOptions) -> bool {
self.read_entry(key, options).is_some()
}
fn read_entry(&self, key: &str, options: &CacheOptions) -> Option<CacheEntry> {
let now = Instant::now();
let entry = self.entries.get(key)?.clone();
if entry.is_expired(now) {
self.entries.remove(key);
return None;
}
if !entry.version_matches(options.version.as_deref()) {
return None;
}
Some(entry)
}
fn make_entry(value: Value, options: CacheOptions) -> CacheEntry {
let created_at = Instant::now();
let expires_at = options
.expires_in
.and_then(|duration| created_at.checked_add(duration));
CacheEntry {
value,
version: options.version,
expires_at,
created_at,
}
}
fn adjust_counter(&self, key: &str, delta: i64) -> Option<i64> {
let now = Instant::now();
let mut entry = self.entries.get_mut(key)?;
if entry.is_expired(now) {
drop(entry);
self.entries.remove(key);
return None;
}
let current = entry.value.as_i64()?;
let updated = current.checked_add(delta)?;
entry.value = Value::from(updated);
Some(updated)
}
}
impl CacheStore for MemoryStore {
fn read(&self, key: &str) -> Option<Value> {
self.read_with_options(key, &CacheOptions::default())
}
fn write(&self, key: &str, value: Value, options: CacheOptions) {
self.entries
.insert(key.to_owned(), Self::make_entry(value, options));
}
fn delete(&self, key: &str) -> bool {
self.entries.remove(key).is_some()
}
fn exist(&self, key: &str) -> bool {
self.exist_with_options(key, &CacheOptions::default())
}
fn fetch(&self, key: &str, options: CacheOptions, f: impl FnOnce() -> Value) -> Value {
if let Some(value) = self.read_with_options(key, &options) {
return value;
}
let value = f();
self.write(key, value.clone(), options);
value
}
fn increment(&self, key: &str, amount: i64) -> Option<i64> {
self.adjust_counter(key, amount)
}
fn decrement(&self, key: &str, amount: i64) -> Option<i64> {
self.adjust_counter(key, amount.checked_neg()?)
}
fn clear(&self) {
self.entries.clear();
}
fn read_multi(&self, keys: &[&str]) -> HashMap<String, Value> {
keys.iter()
.filter_map(|key| self.read(key).map(|value| ((*key).to_owned(), value)))
.collect()
}
fn write_multi(&self, entries: HashMap<String, Value>, options: CacheOptions) {
for (key, value) in entries {
self.write(&key, value, options.clone());
}
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::Duration as StdDuration;
use super::*;
#[test]
fn read_write_round_trip() {
let store = MemoryStore::new();
store.write("city", Value::from("Duckburg"), CacheOptions::default());
assert_eq!(store.read("city"), Some(Value::from("Duckburg")));
}
#[test]
fn read_missing_returns_none() {
let store = MemoryStore::new();
assert_eq!(store.read("missing"), None);
}
#[test]
fn expired_entries_are_missed_and_removed() {
let store = MemoryStore::new();
store.write(
"token",
Value::from("abc"),
CacheOptions {
expires_in: Some(StdDuration::from_millis(20)),
version: None,
},
);
thread::sleep(StdDuration::from_millis(40));
assert_eq!(store.read("token"), None);
assert!(!store.entries.contains_key("token"));
}
#[test]
fn fetch_computes_on_miss_and_caches_value() {
let store = MemoryStore::new();
let calls = AtomicUsize::new(0);
let first = store.fetch("answer", CacheOptions::default(), || {
calls.fetch_add(1, Ordering::SeqCst);
Value::from(42)
});
let second = store.fetch("answer", CacheOptions::default(), || {
calls.fetch_add(1, Ordering::SeqCst);
Value::from(100)
});
assert_eq!(first, Value::from(42));
assert_eq!(second, Value::from(42));
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn fetch_recomputes_when_version_mismatches() {
let store = MemoryStore::new();
store.write(
"user",
Value::from("v1"),
CacheOptions {
expires_in: None,
version: Some("1".to_owned()),
},
);
let value = store.fetch(
"user",
CacheOptions {
expires_in: None,
version: Some("2".to_owned()),
},
|| Value::from("v2"),
);
assert_eq!(value, Value::from("v2"));
assert_eq!(
store.read_with_options(
"user",
&CacheOptions {
expires_in: None,
version: Some("2".to_owned()),
},
),
Some(Value::from("v2"))
);
}
#[test]
fn delete_removes_entry() {
let store = MemoryStore::new();
store.write("city", Value::from("Duckburg"), CacheOptions::default());
assert!(store.delete("city"));
assert_eq!(store.read("city"), None);
assert!(!store.delete("city"));
}
#[test]
fn exist_checks_presence() {
let store = MemoryStore::new();
store.write("present", Value::from(true), CacheOptions::default());
assert!(store.exist("present"));
assert!(!store.exist("missing"));
}
#[test]
fn version_mismatch_returns_none() {
let store = MemoryStore::new();
store.write(
"article",
Value::from("body"),
CacheOptions {
expires_in: None,
version: Some("a".to_owned()),
},
);
assert_eq!(
store.read_with_options(
"article",
&CacheOptions {
expires_in: None,
version: Some("b".to_owned()),
},
),
None
);
assert_eq!(
store.read_with_options(
"article",
&CacheOptions {
expires_in: None,
version: Some("a".to_owned()),
},
),
Some(Value::from("body"))
);
}
#[test]
fn read_without_requested_version_accepts_versioned_entry() {
let store = MemoryStore::new();
store.write(
"article",
Value::from("body"),
CacheOptions {
expires_in: None,
version: Some("a".to_owned()),
},
);
assert_eq!(store.read("article"), Some(Value::from("body")));
}
#[test]
fn increment_and_decrement_update_integer_values() {
let store = MemoryStore::new();
store.write("counter", Value::from(10), CacheOptions::default());
assert_eq!(store.increment("counter", 5), Some(15));
assert_eq!(store.decrement("counter", 3), Some(12));
assert_eq!(store.read("counter"), Some(Value::from(12)));
}
#[test]
fn increment_returns_none_for_missing_or_non_integer_entries() {
let store = MemoryStore::new();
store.write("counter", Value::from("ten"), CacheOptions::default());
assert_eq!(store.increment("missing", 1), None);
assert_eq!(store.increment("counter", 1), None);
}
#[test]
fn decrement_returns_none_when_amount_would_overflow() {
let store = MemoryStore::new();
store.write("counter", Value::from(1), CacheOptions::default());
assert_eq!(store.decrement("counter", i64::MIN), None);
}
#[test]
fn clear_removes_everything() {
let store = MemoryStore::new();
store.write("a", Value::from(1), CacheOptions::default());
store.write("b", Value::from(2), CacheOptions::default());
store.clear();
assert_eq!(store.read("a"), None);
assert_eq!(store.read("b"), None);
}
#[test]
fn read_multi_returns_hits_only() {
let store = MemoryStore::new();
store.write("a", Value::from(1), CacheOptions::default());
store.write("b", Value::from(2), CacheOptions::default());
let result = store.read_multi(&["a", "b", "c"]);
assert_eq!(result.len(), 2);
assert_eq!(result.get("a"), Some(&Value::from(1)));
assert_eq!(result.get("b"), Some(&Value::from(2)));
assert_eq!(result.get("c"), None);
}
#[test]
fn write_multi_writes_every_entry() {
let store = MemoryStore::new();
let mut entries = HashMap::new();
entries.insert("a".to_owned(), Value::from(1));
entries.insert("b".to_owned(), Value::from(2));
store.write_multi(entries, CacheOptions::default());
assert_eq!(store.read("a"), Some(Value::from(1)));
assert_eq!(store.read("b"), Some(Value::from(2)));
}
#[test]
fn write_multi_applies_shared_options() {
let store = MemoryStore::new();
let mut entries = HashMap::new();
entries.insert("a".to_owned(), Value::from(1));
entries.insert("b".to_owned(), Value::from(2));
store.write_multi(
entries,
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: Some("1".to_owned()),
},
);
thread::sleep(StdDuration::from_millis(20));
assert_eq!(store.read("a"), None);
assert_eq!(store.read("b"), None);
}
#[test]
fn increment_preserves_expiry() {
let store = MemoryStore::new();
store.write(
"counter",
Value::from(1),
CacheOptions {
expires_in: Some(StdDuration::from_millis(20)),
version: None,
},
);
assert_eq!(store.increment("counter", 1), Some(2));
thread::sleep(StdDuration::from_millis(30));
assert_eq!(store.read("counter"), None);
}
#[test]
fn increment_preserves_version() {
let store = MemoryStore::new();
store.write(
"counter",
Value::from(1),
CacheOptions {
expires_in: None,
version: Some("1".to_owned()),
},
);
assert_eq!(store.increment("counter", 1), Some(2));
assert_eq!(
store.read_with_options(
"counter",
&CacheOptions {
expires_in: None,
version: Some("1".to_owned()),
},
),
Some(Value::from(2))
);
}
#[test]
fn expired_entry_counts_as_absent_for_existence_checks() {
let store = MemoryStore::new();
store.write(
"session",
Value::from(true),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
);
thread::sleep(StdDuration::from_millis(20));
assert!(!store.exist("session"));
}
#[test]
fn concurrent_increment_is_thread_safe() {
let store = Arc::new(MemoryStore::new());
store.write("counter", Value::from(0), CacheOptions::default());
let mut handles = Vec::new();
for _ in 0..8 {
let store = Arc::clone(&store);
handles.push(thread::spawn(move || {
for _ in 0..100 {
let _ = store.increment("counter", 1);
}
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(store.read("counter"), Some(Value::from(800)));
}
#[test]
fn concurrent_reads_and_writes_are_safe() {
let store = Arc::new(MemoryStore::new());
let mut handles = Vec::new();
for i in 0..4 {
let store = Arc::clone(&store);
handles.push(thread::spawn(move || {
for j in 0..100 {
store.write(
&format!("key-{i}-{j}"),
Value::from(j as i64),
CacheOptions::default(),
);
}
}));
}
for i in 0..4 {
let store = Arc::clone(&store);
handles.push(thread::spawn(move || {
for j in 0..100 {
let _ = store.read(&format!("key-{i}-{j}"));
}
}));
}
for handle in handles {
handle.join().unwrap();
}
assert!(store.read("key-0-99").is_some());
assert!(store.read("key-3-99").is_some());
}
#[test]
fn fetch_recomputes_after_expiration() {
let store = MemoryStore::new();
let calls = AtomicUsize::new(0);
let first = store.fetch(
"token",
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
|| {
calls.fetch_add(1, Ordering::SeqCst);
Value::from("first")
},
);
thread::sleep(StdDuration::from_millis(20));
let second = store.fetch("token", CacheOptions::default(), || {
calls.fetch_add(1, Ordering::SeqCst);
Value::from("second")
});
assert_eq!(first, Value::from("first"));
assert_eq!(second, Value::from("second"));
assert_eq!(store.read("token"), Some(Value::from("second")));
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[test]
fn fetch_uses_cached_null_without_recomputing() {
let store = MemoryStore::new();
let calls = AtomicUsize::new(0);
store.write("nothing", Value::Null, CacheOptions::default());
let value = store.fetch("nothing", CacheOptions::default(), || {
calls.fetch_add(1, Ordering::SeqCst);
Value::from("fallback")
});
assert_eq!(value, Value::Null);
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
#[test]
fn exist_with_options_respects_version_matching() {
let store = MemoryStore::new();
store.write(
"article",
Value::from("body"),
CacheOptions {
expires_in: None,
version: Some("v1".to_owned()),
},
);
assert!(store.exist_with_options(
"article",
&CacheOptions {
expires_in: None,
version: Some("v1".to_owned()),
},
));
assert!(!store.exist_with_options(
"article",
&CacheOptions {
expires_in: None,
version: Some("v2".to_owned()),
},
));
assert!(store.exist("article"));
}
#[test]
fn read_multi_omits_expired_entries() {
let store = MemoryStore::new();
store.write(
"stale",
Value::from("old"),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
);
store.write("fresh", Value::from("new"), CacheOptions::default());
thread::sleep(StdDuration::from_millis(20));
let result = store.read_multi(&["stale", "fresh"]);
assert_eq!(result.len(), 1);
assert_eq!(result.get("stale"), None);
assert_eq!(result.get("fresh"), Some(&Value::from("new")));
assert!(!store.entries.contains_key("stale"));
}
#[test]
fn decrement_returns_none_for_missing_or_non_integer_entries() {
let store = MemoryStore::new();
store.write("counter", Value::from("ten"), CacheOptions::default());
assert_eq!(store.decrement("missing", 1), None);
assert_eq!(store.decrement("counter", 1), None);
}
fn versioned(version: &str) -> CacheOptions {
CacheOptions {
expires_in: None,
version: Some(version.to_owned()),
}
}
macro_rules! non_integer_counter_case {
($name:ident, $method:ident, $value:expr) => {
#[test]
fn $name() {
let store = MemoryStore::new();
store.write("counter", $value, CacheOptions::default());
assert_eq!(store.$method("counter", 1), None);
assert!(store.exist("counter"));
}
};
}
macro_rules! read_or_exist_version_case {
($name:ident, exist, $stored:expr, $expected:expr) => {
#[test]
fn $name() {
let store = MemoryStore::new();
store.write("entry", Value::from("cached"), $stored);
assert!(store.exist_with_options("entry", &$expected));
}
};
($name:ident, $method:ident, $stored:expr, $expected:expr) => {
#[test]
fn $name() {
let store = MemoryStore::new();
store.write("entry", Value::from("cached"), $stored);
assert_eq!(
store.$method("entry", &$expected),
Some(Value::from("cached"))
);
}
};
}
macro_rules! cached_fetch_case {
($name:ident, $stored:expr, $requested:expr) => {
#[test]
fn $name() {
let store = MemoryStore::new();
let calls = AtomicUsize::new(0);
store.write("entry", Value::from("cached"), $stored);
let value = store.fetch("entry", $requested, || {
calls.fetch_add(1, Ordering::SeqCst);
Value::from("fresh")
});
assert_eq!(value, Value::from("cached"));
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
};
}
read_or_exist_version_case!(
read_with_matching_version_returns_cached_value,
read_with_options,
versioned("v1"),
versioned("v1")
);
read_or_exist_version_case!(
read_with_requested_version_accepts_unversioned_entry,
read_with_options,
CacheOptions::default(),
versioned("v1")
);
read_or_exist_version_case!(
exist_with_requested_version_accepts_unversioned_entry,
exist,
CacheOptions::default(),
versioned("v1")
);
read_or_exist_version_case!(
read_with_options_without_version_reads_versioned_entry,
read_with_options,
versioned("v1"),
CacheOptions::default()
);
cached_fetch_case!(
fetch_with_matching_version_uses_cached_value,
versioned("v1"),
versioned("v1")
);
cached_fetch_case!(
fetch_without_requested_version_uses_versioned_entry,
versioned("v1"),
CacheOptions::default()
);
cached_fetch_case!(
fetch_with_requested_version_uses_unversioned_entry,
CacheOptions::default(),
versioned("v1")
);
non_integer_counter_case!(
increment_rejects_boolean_counter,
increment,
Value::Bool(true)
);
non_integer_counter_case!(increment_rejects_null_counter, increment, Value::Null);
non_integer_counter_case!(
decrement_rejects_boolean_counter,
decrement,
Value::Bool(true)
);
non_integer_counter_case!(decrement_rejects_null_counter, decrement, Value::Null);
#[test]
fn write_overwrites_existing_value() {
let store = MemoryStore::new();
store.write("city", Value::from("Duckburg"), CacheOptions::default());
store.write("city", Value::from("St. Canard"), CacheOptions::default());
assert_eq!(store.read("city"), Some(Value::from("St. Canard")));
}
#[test]
fn write_overwrites_existing_version() {
let store = MemoryStore::new();
store.write("article", Value::from("v1"), versioned("v1"));
store.write("article", Value::from("v2"), versioned("v2"));
assert_eq!(store.read_with_options("article", &versioned("v1")), None);
assert_eq!(
store.read_with_options("article", &versioned("v2")),
Some(Value::from("v2"))
);
}
#[test]
fn delete_missing_entry_returns_false() {
let store = MemoryStore::new();
assert!(!store.delete("missing"));
}
#[test]
fn delete_expired_entry_returns_true() {
let store = MemoryStore::new();
store.write(
"session",
Value::from(true),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
);
thread::sleep(StdDuration::from_millis(20));
assert!(store.delete("session"));
assert_eq!(store.read("session"), None);
}
#[test]
fn increment_overflow_returns_none_and_keeps_value() {
let store = MemoryStore::new();
store.write("counter", Value::from(i64::MAX), CacheOptions::default());
assert_eq!(store.increment("counter", 1), None);
assert_eq!(store.read("counter"), Some(Value::from(i64::MAX)));
}
#[test]
fn decrement_underflow_returns_none_and_keeps_value() {
let store = MemoryStore::new();
store.write("counter", Value::from(i64::MIN), CacheOptions::default());
assert_eq!(store.decrement("counter", 1), None);
assert_eq!(store.read("counter"), Some(Value::from(i64::MIN)));
}
#[test]
fn increment_by_zero_returns_current_value() {
let store = MemoryStore::new();
store.write("counter", Value::from(9), CacheOptions::default());
assert_eq!(store.increment("counter", 0), Some(9));
assert_eq!(store.read("counter"), Some(Value::from(9)));
}
#[test]
fn decrement_by_zero_returns_current_value() {
let store = MemoryStore::new();
store.write("counter", Value::from(9), CacheOptions::default());
assert_eq!(store.decrement("counter", 0), Some(9));
assert_eq!(store.read("counter"), Some(Value::from(9)));
}
#[test]
fn decrement_preserves_expiry() {
let store = MemoryStore::new();
store.write(
"counter",
Value::from(3),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
);
assert_eq!(store.decrement("counter", 1), Some(2));
thread::sleep(StdDuration::from_millis(20));
assert_eq!(store.read("counter"), None);
}
#[test]
fn decrement_preserves_version() {
let store = MemoryStore::new();
store.write("counter", Value::from(3), versioned("v1"));
assert_eq!(store.decrement("counter", 1), Some(2));
assert_eq!(
store.read_with_options("counter", &versioned("v1")),
Some(Value::from(2))
);
}
#[test]
fn decrement_on_expired_entry_removes_it() {
let store = MemoryStore::new();
store.write(
"counter",
Value::from(3),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
);
thread::sleep(StdDuration::from_millis(20));
assert_eq!(store.decrement("counter", 1), None);
assert!(!store.entries.contains_key("counter"));
}
#[test]
fn increment_on_expired_entry_removes_it() {
let store = MemoryStore::new();
store.write(
"counter",
Value::from(3),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
);
thread::sleep(StdDuration::from_millis(20));
assert_eq!(store.increment("counter", 1), None);
assert!(!store.entries.contains_key("counter"));
}
#[test]
fn write_multi_overwrites_existing_entries() {
let store = MemoryStore::new();
store.write("a", Value::from(1), CacheOptions::default());
let mut entries = HashMap::new();
entries.insert("a".to_owned(), Value::from(2));
entries.insert("b".to_owned(), Value::from(3));
store.write_multi(entries, CacheOptions::default());
assert_eq!(store.read("a"), Some(Value::from(2)));
assert_eq!(store.read("b"), Some(Value::from(3)));
}
#[test]
fn write_multi_clones_version_to_each_entry() {
let store = MemoryStore::new();
let mut entries = HashMap::new();
entries.insert("a".to_owned(), Value::from(1));
entries.insert("b".to_owned(), Value::from(2));
store.write_multi(entries, versioned("shared"));
assert_eq!(
store.read_with_options("a", &versioned("shared")),
Some(Value::from(1))
);
assert_eq!(
store.read_with_options("b", &versioned("shared")),
Some(Value::from(2))
);
}
#[test]
fn read_multi_empty_slice_returns_empty_map() {
let store = MemoryStore::new();
assert!(store.read_multi(&[]).is_empty());
}
#[test]
fn read_multi_duplicate_keys_return_single_entry() {
let store = MemoryStore::new();
store.write("a", Value::from(1), CacheOptions::default());
let result = store.read_multi(&["a", "a", "a"]);
assert_eq!(result.len(), 1);
assert_eq!(result.get("a"), Some(&Value::from(1)));
}
#[test]
fn clear_on_empty_store_is_safe() {
let store = MemoryStore::new();
store.clear();
assert!(store.read_multi(&[]).is_empty());
}
#[test]
fn read_with_options_on_expired_entry_removes_it() {
let store = MemoryStore::new();
store.write(
"stale",
Value::from("old"),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: Some("v1".to_owned()),
},
);
thread::sleep(StdDuration::from_millis(20));
assert_eq!(store.read_with_options("stale", &versioned("v1")), None);
assert!(!store.entries.contains_key("stale"));
}
#[test]
fn exist_with_options_on_expired_entry_removes_it() {
let store = MemoryStore::new();
store.write(
"stale",
Value::from("old"),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: Some("v1".to_owned()),
},
);
thread::sleep(StdDuration::from_millis(20));
assert!(!store.exist_with_options("stale", &versioned("v1")));
assert!(!store.entries.contains_key("stale"));
}
#[test]
fn fetch_rewrites_expired_entry_with_new_version() {
let store = MemoryStore::new();
store.write(
"token",
Value::from("old"),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: Some("old".to_owned()),
},
);
thread::sleep(StdDuration::from_millis(20));
let value = store.fetch("token", versioned("new"), || Value::from("fresh"));
assert_eq!(value, Value::from("fresh"));
assert_eq!(store.read_with_options("token", &versioned("old")), None);
assert_eq!(
store.read_with_options("token", &versioned("new")),
Some(Value::from("fresh"))
);
}
#[test]
fn fetch_stores_ttl_on_miss_and_entry_expires() {
let store = MemoryStore::new();
let value = store.fetch(
"ephemeral",
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
|| Value::from("value"),
);
assert_eq!(value, Value::from("value"));
thread::sleep(StdDuration::from_millis(20));
assert_eq!(store.read("ephemeral"), None);
}
#[test]
fn expired_entry_can_be_rewritten_by_write() {
let store = MemoryStore::new();
store.write(
"entry",
Value::from("old"),
CacheOptions {
expires_in: Some(StdDuration::from_millis(10)),
version: None,
},
);
thread::sleep(StdDuration::from_millis(20));
store.write("entry", Value::from("new"), CacheOptions::default());
assert_eq!(store.read("entry"), Some(Value::from("new")));
}
#[test]
fn fetch_after_delete_recomputes_value() {
let store = MemoryStore::new();
let calls = AtomicUsize::new(0);
store.write("entry", Value::from("cached"), CacheOptions::default());
assert!(store.delete("entry"));
let value = store.fetch("entry", CacheOptions::default(), || {
calls.fetch_add(1, Ordering::SeqCst);
Value::from("fresh")
});
assert_eq!(value, Value::from("fresh"));
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn write_allows_null_values() {
let store = MemoryStore::new();
store.write("entry", Value::Null, CacheOptions::default());
assert_eq!(store.read("entry"), Some(Value::Null));
assert!(store.exist("entry"));
}
}