use super::*;
pub struct TimedMap<C, K, V>
where
C: Clock,
K: Eq + Copy,
{
#[cfg(feature = "std")]
clock: StdClock,
#[cfg(feature = "std")]
marker: PhantomData<C>,
#[cfg(not(feature = "std"))]
clock: C,
map: BTreeMap<K, ExpirableEntry<V>>,
expiries: BTreeMap<u64, K>,
}
#[cfg(feature = "std")]
impl<C: Clock, K: Copy + Eq + Ord, V> Default for TimedMap<C, K, V> {
fn default() -> Self {
Self {
clock: StdClock::default(),
map: BTreeMap::default(),
expiries: BTreeMap::default(),
marker: PhantomData,
}
}
}
impl<C: Clock, K: Copy + Eq + Ord, V> TimedMap<C, K, V> {
#[inline(always)]
#[cfg(feature = "std")]
pub fn new() -> Self {
Self::default()
}
#[inline(always)]
#[cfg(not(feature = "std"))]
pub fn new(clock: C) -> Self {
Self {
clock,
map: BTreeMap::default(),
expiries: BTreeMap::default(),
}
}
pub fn get(&self, k: &K) -> Option<&V> {
self.map
.get(k)
.filter(|v| !v.is_expired(&self.clock))
.map(|v| v.value())
}
pub fn get_remaining_duration(&self, k: &K) -> Option<Duration> {
self.map
.get(k)
.filter(|v| !v.is_expired(&self.clock))
.map(|v| v.remaining_duration(&self.clock))?
}
fn insert(&mut self, k: K, v: V, duration: Option<Duration>) -> Option<V> {
self.drop_expired_entries();
let entry = ExpirableEntry::new(&self.clock, v, duration);
if let EntryStatus::ExpiresAtSeconds(expires_at_seconds) = entry.status() {
self.expiries.insert(*expires_at_seconds, k);
}
self.map.insert(k, entry).map(|v| v.owned_value())
}
#[inline(always)]
pub fn insert_expirable(&mut self, k: K, v: V, duration: Duration) -> Option<V> {
self.insert(k, v, Some(duration))
}
#[inline(always)]
pub fn insert_constant(&mut self, k: K, v: V) -> Option<V> {
self.insert(k, v, None)
}
pub fn remove(&mut self, k: &K) -> Option<V> {
self.drop_expired_entries();
self.map
.remove(k)
.filter(|v| !v.is_expired(&self.clock))
.map(|v| {
if let EntryStatus::ExpiresAtSeconds(expires_at_seconds) = v.status() {
self.expiries.remove(expires_at_seconds);
}
v.owned_value()
})
}
fn drop_expired_entries(&mut self) {
let now_seconds = self.clock.now_seconds();
while let Some((exp, key)) = self.expiries.pop_first() {
if exp > now_seconds {
self.expiries.insert(exp, key);
break;
}
self.map.remove(&key);
}
}
}
#[cfg(test)]
#[cfg(not(feature = "std"))]
mod tests {
use super::*;
struct MockClock {
current_time: u64,
}
impl Clock for MockClock {
fn now_seconds(&self) -> u64 {
self.current_time
}
}
#[test]
fn nostd_insert_and_get_constant_entry() {
let clock = MockClock { current_time: 1000 };
let mut map: TimedMap<MockClock, u32, &str> = TimedMap::new(clock);
map.insert_constant(1, "constant value");
assert_eq!(map.get(&1), Some(&"constant value"));
assert_eq!(map.get_remaining_duration(&1), None);
}
#[test]
fn nostd_insert_and_get_expirable_entry() {
let clock = MockClock { current_time: 1000 };
let mut map: TimedMap<MockClock, u32, &str> = TimedMap::new(clock);
let duration = Duration::from_secs(60);
map.insert_expirable(1, "expirable value", duration);
assert_eq!(map.get(&1), Some(&"expirable value"));
assert_eq!(map.get_remaining_duration(&1), Some(duration));
}
#[test]
fn nostd_expired_entry() {
let clock = MockClock { current_time: 1000 };
let mut map: TimedMap<MockClock, u32, &str> = TimedMap::new(clock);
let duration = Duration::from_secs(60);
map.insert_expirable(1, "expirable value", duration);
let clock = MockClock { current_time: 1070 };
map.clock = clock;
assert_eq!(map.get(&1), None);
assert_eq!(map.get_remaining_duration(&1), None);
}
#[test]
fn nostd_remove_entry() {
let clock = MockClock { current_time: 1000 };
let mut map: TimedMap<MockClock, u32, &str> = TimedMap::new(clock);
map.insert_constant(1, "constant value");
assert_eq!(map.remove(&1), Some("constant value"));
assert_eq!(map.get(&1), None);
}
#[test]
fn nostd_drop_expired_entries() {
let clock = MockClock { current_time: 1000 };
let mut map: TimedMap<MockClock, u32, &str> = TimedMap::new(clock);
map.insert_expirable(1, "expirable value1", Duration::from_secs(50));
map.insert_expirable(2, "expirable value2", Duration::from_secs(70));
map.insert_constant(3, "constant value");
let clock = MockClock { current_time: 1055 };
map.clock = clock;
assert_eq!(map.get(&1), None);
assert_eq!(map.get(&2), Some(&"expirable value2"));
assert_eq!(map.get(&3), Some(&"constant value"));
let clock = MockClock { current_time: 1071 };
map.clock = clock;
assert_eq!(map.get(&1), None);
assert_eq!(map.get(&2), None);
assert_eq!(map.get(&3), Some(&"constant value"));
}
#[test]
fn nostd_update_existing_entry() {
let clock = MockClock { current_time: 1000 };
let mut map: TimedMap<MockClock, u32, &str> = TimedMap::new(clock);
map.insert_constant(1, "initial value");
assert_eq!(map.get(&1), Some(&"initial value"));
map.insert_expirable(1, "updated value", Duration::from_secs(15));
assert_eq!(map.get(&1), Some(&"updated value"));
let clock = MockClock { current_time: 1016 };
map.clock = clock;
assert_eq!(map.get(&1), None);
}
}
#[cfg(feature = "std")]
#[cfg(test)]
mod std_tests {
use super::*;
#[test]
fn std_expirable_and_constant_entries() {
let mut map: TimedMap<StdClock, u32, &str> = TimedMap::new();
map.insert_constant(1, "constant value");
map.insert_expirable(2, "expirable value", Duration::from_secs(2));
assert_eq!(map.get(&1), Some(&"constant value"));
assert_eq!(map.get(&2), Some(&"expirable value"));
assert_eq!(map.get_remaining_duration(&1), None);
assert!(map.get_remaining_duration(&2).is_some());
}
#[test]
fn std_expired_entry_removal() {
let mut map: TimedMap<StdClock, u32, &str> = TimedMap::new();
let duration = Duration::from_secs(2);
map.insert_expirable(1, "expirable value", duration);
std::thread::sleep(Duration::from_secs(3));
assert_eq!(map.get(&1), None);
assert_eq!(map.get_remaining_duration(&1), None);
}
#[test]
fn std_remove_entry() {
let mut map: TimedMap<StdClock, _, _> = TimedMap::new();
map.insert_constant(1, "constant value");
map.insert_expirable(2, "expirable value", Duration::from_secs(2));
assert_eq!(map.remove(&1), Some("constant value"));
assert_eq!(map.remove(&2), Some("expirable value"));
assert_eq!(map.get(&1), None);
assert_eq!(map.get(&2), None);
}
#[test]
fn std_drop_expired_entries() {
let mut map: TimedMap<StdClock, u32, &str> = TimedMap::new();
map.insert_expirable(1, "expirable value1", Duration::from_secs(2));
map.insert_expirable(2, "expirable value2", Duration::from_secs(4));
std::thread::sleep(Duration::from_secs(3));
assert_eq!(map.get(&1), None);
assert_eq!(map.get(&2), Some(&"expirable value2"));
}
#[test]
fn std_update_existing_entry() {
let mut map: TimedMap<StdClock, u32, &str> = TimedMap::new();
map.insert_constant(1, "initial value");
assert_eq!(map.get(&1), Some(&"initial value"));
map.insert_expirable(1, "updated value", Duration::from_secs(1));
assert_eq!(map.get(&1), Some(&"updated value"));
std::thread::sleep(Duration::from_secs(2));
assert_eq!(map.get(&1), None);
}
#[test]
fn std_insert_constant_and_expirable_combined() {
let mut map: TimedMap<StdClock, u32, &str> = TimedMap::new();
map.insert_constant(1, "constant value");
map.insert_expirable(2, "expirable value", Duration::from_secs(2));
assert_eq!(map.get(&1), Some(&"constant value"));
assert_eq!(map.get(&2), Some(&"expirable value"));
std::thread::sleep(Duration::from_secs(3));
assert_eq!(map.get(&1), Some(&"constant value"));
assert_eq!(map.get(&2), None);
}
#[test]
fn std_expirable_entry_still_valid_before_expiration() {
let mut map: TimedMap<StdClock, u32, &str> = TimedMap::new();
map.insert_expirable(1, "expirable value", Duration::from_secs(3));
std::thread::sleep(Duration::from_secs(2));
assert_eq!(map.get(&1), Some(&"expirable value"));
assert!(map.get_remaining_duration(&1).unwrap().as_secs() == 1);
}
}