#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
#[cfg(feature = "std")]
use std::{string::String, vec::Vec};
mod engine;
mod iter;
mod raw;
#[cfg(feature = "std")]
mod sharded;
#[cfg(all(target_arch = "x86_64", feature = "simd"))]
mod simd;
#[cfg(feature = "std")]
mod sync;
mod traits;
pub use crate::engine::bucket::Bucket;
pub use crate::engine::meta::MetaWord;
pub use crate::engine::slot::Slot;
pub use iter::{RawIter, TypedIter};
pub use raw::PulseMapRaw;
#[cfg(feature = "std")]
pub use sharded::ShardedPulseMap;
#[cfg(feature = "std")]
pub use sync::ConcurrentPulseMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum SlotState {
Empty = 0,
Full = 1,
Tombstone = 2,
}
impl SlotState {
#[inline]
pub(crate) fn from_bits(bits: u8) -> Self {
match bits & 0x03 {
1 => SlotState::Full,
2 => SlotState::Tombstone,
_ => SlotState::Empty, }
}
}
pub type PulseMap = PulseMapRaw;
pub trait PulseKey: Sized {
type Bytes: AsRef<[u8]>;
fn to_bytes(&self) -> Self::Bytes;
fn from_bytes(bytes: &[u8]) -> Option<Self>;
#[inline]
fn with_key_bytes<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
f(self.to_bytes().as_ref())
}
}
pub trait PulseValue: Sized {
type Bytes: AsRef<[u8]>;
fn to_bytes(&self) -> Self::Bytes;
fn from_bytes(bytes: &[u8]) -> Option<Self>;
}
impl PulseKey for u8 {
type Bytes = [u8; 1];
fn to_bytes(&self) -> [u8; 1] {
[*self]
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.first().copied()
}
}
impl PulseKey for u16 {
type Bytes = [u8; 2];
fn to_bytes(&self) -> [u8; 2] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(u16::from_le_bytes)
}
}
impl PulseKey for u32 {
type Bytes = [u8; 4];
fn to_bytes(&self) -> [u8; 4] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(u32::from_le_bytes)
}
}
impl PulseKey for u64 {
type Bytes = [u8; 8];
fn to_bytes(&self) -> [u8; 8] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(u64::from_le_bytes)
}
}
impl PulseKey for i32 {
type Bytes = [u8; 4];
fn to_bytes(&self) -> [u8; 4] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(i32::from_le_bytes)
}
}
impl PulseKey for i64 {
type Bytes = [u8; 8];
fn to_bytes(&self) -> [u8; 8] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(i64::from_le_bytes)
}
}
impl PulseKey for String {
type Bytes = Vec<u8>;
fn to_bytes(&self) -> Vec<u8> {
self.as_bytes().to_vec()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
core::str::from_utf8(b).ok().map(String::from)
}
#[inline]
fn with_key_bytes<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
f(self.as_bytes())
}
}
impl PulseKey for Vec<u8> {
type Bytes = Vec<u8>;
fn to_bytes(&self) -> Vec<u8> {
self.clone()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
Some(b.to_vec())
}
#[inline]
fn with_key_bytes<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
f(self)
}
}
impl<const N: usize> PulseKey for [u8; N] {
type Bytes = [u8; N];
fn to_bytes(&self) -> [u8; N] {
*self
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok()
}
#[inline]
fn with_key_bytes<R>(&self, f: impl FnOnce(&[u8]) -> R) -> R {
f(self)
}
}
impl PulseValue for u8 {
type Bytes = [u8; 1];
fn to_bytes(&self) -> [u8; 1] {
[*self]
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.first().copied()
}
}
impl PulseValue for u16 {
type Bytes = [u8; 2];
fn to_bytes(&self) -> [u8; 2] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(u16::from_le_bytes)
}
}
impl PulseValue for u32 {
type Bytes = [u8; 4];
fn to_bytes(&self) -> [u8; 4] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(u32::from_le_bytes)
}
}
impl PulseValue for u64 {
type Bytes = [u8; 8];
fn to_bytes(&self) -> [u8; 8] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(u64::from_le_bytes)
}
}
impl PulseValue for i32 {
type Bytes = [u8; 4];
fn to_bytes(&self) -> [u8; 4] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(i32::from_le_bytes)
}
}
impl PulseValue for i64 {
type Bytes = [u8; 8];
fn to_bytes(&self) -> [u8; 8] {
self.to_le_bytes()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.try_into().ok().map(i64::from_le_bytes)
}
}
impl PulseValue for String {
type Bytes = Vec<u8>;
fn to_bytes(&self) -> Vec<u8> {
self.as_bytes().to_vec()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
core::str::from_utf8(b).ok().map(String::from)
}
}
impl PulseValue for Vec<u8> {
type Bytes = Vec<u8>;
fn to_bytes(&self) -> Vec<u8> {
self.clone()
}
fn from_bytes(b: &[u8]) -> Option<Self> {
Some(b.to_vec())
}
}
impl PulseValue for bool {
type Bytes = [u8; 1];
fn to_bytes(&self) -> [u8; 1] {
[*self as u8]
}
fn from_bytes(b: &[u8]) -> Option<Self> {
b.first().map(|&v| v != 0)
}
}
pub struct TypedPulseMap<K: PulseKey, V: PulseValue> {
raw: PulseMapRaw,
_marker: core::marker::PhantomData<(K, V)>,
}
impl<K: PulseKey, V: PulseValue> TypedPulseMap<K, V> {
pub fn new(num_buckets: usize) -> Self {
Self {
raw: PulseMapRaw::new(num_buckets),
_marker: core::marker::PhantomData,
}
}
pub fn insert(&mut self, key: K, value: V) {
let kb = key.to_bytes();
let vb = value.to_bytes();
self.raw.insert(kb.as_ref(), vb.as_ref());
}
pub fn insert_ttl(&mut self, key: K, value: V, ttl: u64) {
let kb = key.to_bytes();
let vb = value.to_bytes();
self.raw.insert_ttl(kb.as_ref(), vb.as_ref(), ttl);
}
pub fn get(&self, key: &K) -> Option<V> {
key.with_key_bytes(|kb| self.raw.get(kb).and_then(V::from_bytes))
}
pub fn peek(&self, key: &K) -> Option<V> {
key.with_key_bytes(|kb| self.raw.peek(kb).and_then(V::from_bytes))
}
pub fn remove(&mut self, key: &K) -> bool {
key.with_key_bytes(|kb| self.raw.remove(kb))
}
pub fn contains_key(&self, key: &K) -> bool {
key.with_key_bytes(|kb| self.raw.peek(kb).is_some())
}
pub fn iter(&self) -> TypedIter<'_, K, V> {
TypedIter::new(&self.raw)
}
#[inline]
pub fn len(&self) -> usize {
self.raw.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.raw.is_empty()
}
#[inline]
pub fn capacity(&self) -> usize {
self.raw.capacity()
}
#[inline]
pub fn load_factor(&self) -> f64 {
self.raw.load_factor()
}
#[inline]
pub fn eviction_count(&self) -> usize {
self.raw.eviction_count()
}
#[inline]
pub fn set_ttl(&mut self, ttl_epochs: u64) {
self.raw.set_ttl(ttl_epochs);
}
#[inline]
pub fn get_ttl(&self) -> u64 {
self.raw.get_ttl()
}
#[inline]
pub fn current_epoch(&self) -> u64 {
self.raw.current_epoch()
}
pub fn entry(&mut self, key: K) -> Entry<'_, K, V> {
let kb = key.to_bytes();
let existing = self.raw.peek(kb.as_ref()).and_then(|vb| V::from_bytes(vb));
match existing {
Some(val) => Entry::Occupied(OccupiedEntry {
map: self,
key,
value: val,
}),
None => Entry::Vacant(VacantEntry { map: self, key }),
}
}
}
pub enum Entry<'a, K: PulseKey, V: PulseValue> {
Occupied(OccupiedEntry<'a, K, V>),
Vacant(VacantEntry<'a, K, V>),
}
pub struct OccupiedEntry<'a, K: PulseKey, V: PulseValue> {
map: &'a mut TypedPulseMap<K, V>,
key: K,
value: V,
}
pub struct VacantEntry<'a, K: PulseKey, V: PulseValue> {
map: &'a mut TypedPulseMap<K, V>,
key: K,
}
impl<'a, K: PulseKey, V: PulseValue> Entry<'a, K, V> {
pub fn or_insert(self, default: V) {
if let Entry::Vacant(e) = self {
e.map.insert(e.key, default);
}
}
pub fn or_insert_with<F: FnOnce() -> V>(self, f: F) {
if let Entry::Vacant(e) = self {
e.map.insert(e.key, f());
}
}
pub fn and_modify<F: FnOnce(&mut V)>(self, f: F) -> Self {
match self {
Entry::Occupied(mut e) => {
f(&mut e.value);
let kb = e.key.to_bytes();
let vb = e.value.to_bytes();
e.map.raw.insert(kb.as_ref(), vb.as_ref());
Entry::Occupied(e)
}
Entry::Vacant(e) => Entry::Vacant(e),
}
}
}
impl<'a, K: PulseKey, V: PulseValue> OccupiedEntry<'a, K, V> {
pub fn get(&self) -> &V {
&self.value
}
pub fn key(&self) -> &K {
&self.key
}
pub fn insert(self, value: V) -> V {
self.map.insert(self.key, value);
self.value
}
pub fn remove(self) -> V {
self.map.remove(&self.key);
self.value
}
}
impl<'a, K: PulseKey, V: PulseValue> VacantEntry<'a, K, V> {
pub fn key(&self) -> &K {
&self.key
}
pub fn insert(self, value: V) {
self.map.insert(self.key, value);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_raw_insert_and_get() {
let mut map = PulseMap::new(16);
map.insert(b"hello", b"world");
assert_eq!(map.get(b"hello"), Some(&b"world"[..]));
assert_eq!(map.len(), 1);
}
#[test]
fn test_raw_get_missing() {
let map = PulseMap::new(16);
assert_eq!(map.get(b"nope"), None);
}
#[test]
fn test_raw_update_existing() {
let mut map = PulseMap::new(16);
map.insert(b"key", b"val1");
map.insert(b"key", b"val2");
assert_eq!(map.get(b"key"), Some(&b"val2"[..]));
assert_eq!(map.len(), 1);
}
#[test]
fn test_raw_remove() {
let mut map = PulseMap::new(16);
map.insert(b"key", b"val");
assert!(map.remove(b"key"));
assert_eq!(map.get(b"key"), None);
assert_eq!(map.len(), 0);
}
#[test]
fn test_raw_remove_missing() {
let mut map = PulseMap::new(16);
assert!(!map.remove(b"nope"));
}
#[test]
fn test_raw_many_inserts() {
let mut map = PulseMap::new(1024);
for i in 0u32..1000 {
map.insert(&i.to_le_bytes(), &(i * 2).to_le_bytes());
}
let mut hits = 0;
for i in 0u32..1000 {
if map.get(&i.to_le_bytes()).is_some() {
hits += 1;
}
}
assert!(hits > 500, "Expected >500 hits, got {}", hits);
}
#[test]
fn test_raw_eviction() {
let mut map = PulseMap::new(4);
for i in 0u32..100 {
map.insert(&i.to_le_bytes(), b"val");
}
assert!(map.eviction_count() > 0);
assert!(map.len() <= 16);
}
#[test]
fn test_raw_slab_mode() {
let mut map = PulseMap::new(16);
let long_key = b"this_is_a_very_long_key_that_exceeds_six_bytes";
let long_val = b"this_is_a_very_long_value_that_also_exceeds_seven_bytes";
map.insert(long_key, long_val);
assert_eq!(map.get(long_key), Some(&long_val[..]));
}
#[test]
fn test_raw_load_factor() {
let mut map = PulseMap::new(100);
assert_eq!(map.capacity(), 512);
for i in 0u32..200 {
map.insert(&i.to_le_bytes(), b"v");
}
assert!(map.load_factor() > 0.0);
assert!(map.load_factor() <= 1.0);
}
#[test]
fn test_raw_peek() {
let map = PulseMap::new(16);
assert_eq!(map.peek(b"key"), None);
}
#[test]
fn test_bucket_size() {
assert_eq!(
core::mem::size_of::<Bucket>(),
64,
"Bucket must be exactly 64 bytes"
);
}
#[test]
fn test_typed_u32_u64() {
let mut map = TypedPulseMap::<u32, u64>::new(16);
map.insert(42, 100);
assert_eq!(map.get(&42), Some(100));
assert_eq!(map.len(), 1);
}
#[cfg(feature = "std")]
#[test]
fn test_typed_string() {
let mut map = TypedPulseMap::<String, String>::new(16);
map.insert("hello".to_string(), "world".to_string());
assert_eq!(map.get(&"hello".to_string()), Some("world".to_string()));
}
#[test]
fn test_typed_remove() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.insert(1, 10);
assert!(map.remove(&1));
assert_eq!(map.get(&1), None);
}
#[test]
fn test_typed_contains_key() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.insert(5, 50);
assert!(map.contains_key(&5));
assert!(!map.contains_key(&6));
}
#[cfg(feature = "std")]
#[test]
fn test_typed_extend() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.extend(vec![(1, 10), (2, 20), (3, 30)]);
assert_eq!(map.len(), 3);
assert_eq!(map.get(&2), Some(20));
}
#[cfg(feature = "std")]
#[test]
fn test_typed_debug() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.insert(1, 10);
let debug = format!("{:?}", map);
assert!(debug.contains("TypedPulseMap"));
assert!(debug.contains("len"));
}
#[cfg(feature = "std")]
#[test]
fn test_typed_display() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.insert(1, 10);
let display = format!("{}", map);
assert!(display.contains("PulseMap("));
}
#[test]
fn test_typed_eviction() {
let mut map = TypedPulseMap::<u32, u32>::new(4);
for i in 0..100u32 {
map.insert(i, i * 10);
}
assert!(map.eviction_count() > 0);
}
#[cfg(feature = "std")]
#[test]
fn test_typed_iterator() {
let mut map = TypedPulseMap::<u32, u32>::new(256);
map.insert(1, 10);
map.insert(2, 20);
map.insert(3, 30);
let collected: Vec<(u32, u32)> = map.iter().collect();
assert_eq!(collected.len(), 3);
}
#[cfg(feature = "std")]
#[test]
fn test_typed_from_hashmap() {
use std::collections::HashMap;
let mut std_map = HashMap::new();
std_map.insert(1u32, 100u32);
std_map.insert(2, 200);
std_map.insert(3, 300);
let pulse: TypedPulseMap<u32, u32> = TypedPulseMap::from(std_map);
assert_eq!(pulse.len(), 3);
assert_eq!(pulse.get(&1), Some(100));
assert_eq!(pulse.get(&2), Some(200));
assert_eq!(pulse.get(&3), Some(300));
}
#[test]
fn test_entry_or_insert_vacant() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.entry(42).or_insert(100);
assert_eq!(map.get(&42), Some(100));
}
#[test]
fn test_entry_or_insert_occupied() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.insert(42, 100);
map.entry(42).or_insert(999); assert_eq!(map.get(&42), Some(100));
}
#[test]
fn test_entry_or_insert_with() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.entry(10).or_insert_with(|| 42 * 2);
assert_eq!(map.get(&10), Some(84));
}
#[test]
fn test_entry_and_modify() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.insert(1, 10);
map.entry(1).and_modify(|v| *v += 5).or_insert(0);
assert_eq!(map.get(&1), Some(15));
}
#[test]
fn test_entry_and_modify_vacant() {
let mut map = TypedPulseMap::<u32, u32>::new(16);
map.entry(99).and_modify(|v| *v += 5).or_insert(42);
assert_eq!(map.get(&99), Some(42));
}
#[cfg(feature = "std")]
#[test]
fn test_concurrent_basic() {
let map = ConcurrentPulseMap::<u32, u32>::new(64);
map.insert(1, 10);
map.insert(2, 20);
assert_eq!(map.get(&1), Some(10));
assert_eq!(map.get(&2), Some(20));
assert_eq!(map.get(&3), None);
assert_eq!(map.len(), 2);
}
#[cfg(feature = "std")]
#[test]
fn test_concurrent_remove() {
let map = ConcurrentPulseMap::<u32, u32>::new(64);
map.insert(1, 10);
assert!(map.remove(&1));
assert_eq!(map.get(&1), None);
assert!(!map.remove(&1));
}
#[cfg(feature = "std")]
#[test]
fn test_concurrent_contains_key() {
let map = ConcurrentPulseMap::<u32, u32>::new(64);
map.insert(5, 50);
assert!(map.contains_key(&5));
assert!(!map.contains_key(&6));
}
#[cfg(feature = "std")]
#[test]
fn test_concurrent_multithread_insert() {
use std::sync::Arc;
use std::thread;
let map = Arc::new(ConcurrentPulseMap::<u32, u32>::new(16384));
let handles: Vec<_> = (0..4)
.map(|t| {
let m = map.clone();
thread::spawn(move || {
for i in 0..1000u32 {
m.insert(t * 10000 + i, i);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert!(map.len() >= 3900); }
#[cfg(feature = "std")]
#[test]
fn test_concurrent_multithread_read_write() {
use std::sync::Arc;
use std::thread;
let map = Arc::new(ConcurrentPulseMap::<u32, u32>::new(4096));
for i in 0..500u32 {
map.insert(i, i * 10);
}
let handles: Vec<_> = (0..4)
.map(|t| {
let m = map.clone();
thread::spawn(move || {
for i in 0..500u32 {
if t % 2 == 0 {
m.insert(500 + t * 1000 + i, i);
} else {
let _ = m.get(&(i % 500));
}
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert!(!map.is_empty());
}
#[cfg(feature = "std")]
#[test]
fn test_concurrent_display() {
let map = ConcurrentPulseMap::<u32, u32>::new(16);
map.insert(1, 10);
let s = format!("{}", map);
assert!(s.contains("ConcurrentPulseMap"));
assert!(s.contains("1/"));
}
#[cfg(feature = "std")]
#[test]
fn test_concurrent_manual_resize() {
let map = ConcurrentPulseMap::<u32, u32>::new(256);
assert_eq!(map.capacity(), 1024);
for i in 0..40u32 {
map.insert(i, i * 10);
}
assert_eq!(map.len(), 40);
map.resize(512);
assert_eq!(map.capacity(), 2048);
assert_eq!(map.len(), 40);
assert_eq!(map.get(&0), Some(0));
assert_eq!(map.get(&39), Some(390));
}
#[cfg(feature = "std")]
#[test]
fn test_concurrent_auto_resize() {
let map = ConcurrentPulseMap::<u32, u32>::with_auto_resize(16);
let initial_cap = map.capacity();
for i in 0..200u32 {
map.insert(i, i * 10);
}
assert!(map.capacity() > initial_cap);
assert!(map.len() > 100);
}
#[test]
fn test_ttl_disabled_by_default() {
let mut map = PulseMap::new(16);
map.insert(b"key", b"val");
for i in 0u32..1000 {
map.insert(&i.to_le_bytes(), b"x");
}
assert_eq!(map.get_ttl(), 0);
}
#[test]
fn test_ttl_basic_expiry() {
let mut map = PulseMap::new(64);
map.set_ttl(3);
map.insert(b"old_key", b"old_val");
map.insert(b"k2", b"v2"); map.insert(b"k3", b"v3"); map.insert(b"k4", b"v4");
assert!(
map.get(b"old_key").is_some() || map.get(b"old_key").is_none(),
"boundary behavior is defined"
);
map.insert(b"k5", b"v5");
assert_eq!(
map.get(b"old_key"),
None,
"Entry must be expired after ttl_epochs+1 insertions"
);
}
#[test]
fn test_ttl_update_refreshes_epoch() {
let mut map = PulseMap::new(64);
map.set_ttl(2);
map.insert(b"key", b"v1");
map.insert(b"a", b"1"); map.insert(b"key", b"v2"); map.insert(b"b", b"2"); map.insert(b"c", b"3");
assert_eq!(map.get(b"key"), Some(&b"v2"[..]));
}
#[test]
fn test_ttl_typed_map() {
let mut map = TypedPulseMap::<u32, u32>::new(64);
map.set_ttl(3);
assert_eq!(map.get_ttl(), 3);
map.insert(1, 100);
for i in 2..6u32 {
map.insert(i, i * 10); }
assert_eq!(map.get(&1), None, "Typed TTL expiry must work");
assert_eq!(map.get(&5), Some(50));
}
#[test]
fn test_ttl_zero_disables() {
let mut map = PulseMap::new(64);
map.set_ttl(0);
map.insert(b"key", b"val");
for i in 0u32..500 {
map.insert(&i.to_le_bytes(), b"x");
}
assert_eq!(map.get_ttl(), 0);
assert_eq!(map.current_epoch(), 501); }
#[test]
fn test_per_entry_ttl_different_expiries() {
let mut map = PulseMap::new(64);
map.insert_ttl(b"k1", b"v1", 3);
map.insert_ttl(b"k2", b"v2", 10);
for i in 0u32..3 {
map.insert(&i.to_le_bytes(), b"x");
}
assert_eq!(
map.get(b"k1"),
None,
"k1 should be expired after 3+1 inserts"
);
assert_eq!(map.get(b"k2"), Some(&b"v2"[..]), "k2 should still be alive");
}
#[test]
fn test_per_entry_ttl_never_expire() {
let mut map = PulseMap::new(64);
map.set_ttl(2); map.insert_ttl(b"forever", b"val", u64::MAX); map.insert(b"normal", b"val");
for i in 0u32..3 {
map.insert(&i.to_le_bytes(), b"x");
}
assert_eq!(
map.get(b"forever"),
Some(&b"val"[..]),
"u64::MAX entry must never expire"
);
assert_eq!(map.get(b"normal"), None, "normal entry should have expired");
}
#[test]
fn test_per_entry_ttl_overrides_global() {
let mut map = PulseMap::new(64);
map.set_ttl(100);
map.insert_ttl(b"short", b"val", 2);
map.insert(b"a", b"1"); map.insert(b"b", b"2"); map.insert(b"c", b"3");
assert_eq!(
map.get(b"short"),
None,
"per-entry TTL=2 should override global TTL=100"
);
}
#[test]
fn test_per_entry_ttl_typed_map() {
let mut map = TypedPulseMap::<u32, u32>::new(64);
map.set_ttl(100);
map.insert_ttl(1, 100, 3); map.insert_ttl(2, 200, u64::MAX); map.insert(3, 300);
for i in 10..14u32 {
map.insert(i, i);
}
assert_eq!(map.get(&1), None, "key=1 should be expired (TTL=3)");
assert_eq!(map.get(&2), Some(200), "key=2 should never expire");
assert_eq!(
map.get(&3),
Some(300),
"key=3 uses global TTL=100, still alive"
);
}
#[cfg(feature = "std")]
#[test]
fn test_per_entry_ttl_concurrent_map() {
let map = ConcurrentPulseMap::<u32, u32>::new(64);
map.set_ttl(100);
map.insert_ttl(1, 100, 3);
map.insert_ttl(2, 200, u64::MAX);
for i in 10..14u32 {
map.insert(i, i);
}
assert_eq!(map.get(&1), None, "concurrent: key=1 expired (TTL=3)");
assert_eq!(map.get(&2), Some(200), "concurrent: key=2 never expires");
}
#[test]
fn test_insert_ttl_refresh_on_reinsert() {
let mut map = PulseMap::new(64);
map.insert_ttl(b"key", b"v1", 3);
map.insert(b"a", b"1"); map.insert(b"b", b"2");
map.insert_ttl(b"key", b"v2", 3);
map.insert(b"c", b"3"); map.insert(b"d", b"4"); assert_eq!(
map.get(b"key"),
Some(&b"v2"[..]),
"re-insert should refresh epoch"
);
map.insert(b"e", b"5"); map.insert(b"f", b"6"); assert_eq!(map.get(b"key"), None, "key should expire after refresh+TTL");
}
}