Skip to main content

telltale_types/
content_store.rs

1//! Content Store for Memoization and Deduplication
2//!
3//! This module provides a content-addressed store for memoizing expensive
4//! computations and deduplicating structurally identical values.
5//!
6//! # Design
7//!
8//! The `ContentStore` maps `ContentId` to values, enabling:
9//! - Memoization of expensive computations (e.g., projection)
10//! - Structural sharing of identical protocol artifacts
11//! - Cache hit/miss metrics for performance analysis
12//!
13//! # Lean Correspondence
14//!
15//! This corresponds to the memoization infrastructure in the Lean formalization.
16
17use crate::content_id::{ContentId, DefaultContentHasher, Hasher};
18use crate::contentable::{Contentable, ContentableError};
19use std::collections::HashMap;
20use std::hash::Hash as StdHash;
21use std::sync::atomic::{AtomicU64, Ordering};
22
23/// Metrics for cache performance analysis.
24#[derive(Debug, Clone, Default)]
25pub struct CacheMetrics {
26    /// Number of cache hits
27    pub hits: u64,
28    /// Number of cache misses
29    pub misses: u64,
30    /// Number of items currently stored
31    pub size: usize,
32}
33
34impl CacheMetrics {
35    /// Calculate the hit rate as a percentage.
36    #[must_use]
37    #[allow(clippy::as_conversions)] // u64 -> f64 is acceptable for percentage display
38    pub fn hit_rate(&self) -> f64 {
39        let total = self.hits + self.misses;
40        if total == 0 {
41            0.0
42        } else {
43            (self.hits as f64 / total as f64) * 100.0
44        }
45    }
46}
47
48/// A content-addressed store for memoization and deduplication.
49///
50/// Values are stored by their content ID, ensuring that structurally
51/// identical values share storage and enabling efficient cache lookups.
52///
53/// # Type Parameters
54///
55/// - `K`: The content key type (must implement `Contentable`)
56/// - `V`: The cached value type
57/// - `H`: The hash algorithm (default: `DefaultContentHasher`)
58///
59/// # Examples
60///
61/// ```
62/// use telltale_types::content_store::ContentStore;
63/// use telltale_types::{GlobalType, LocalTypeR, Label};
64///
65/// let mut store: ContentStore<GlobalType, LocalTypeR> = ContentStore::new();
66///
67/// let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
68/// let local = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
69///
70/// // Store a projection result
71/// store.insert(&global, local.clone()).unwrap();
72///
73/// // Retrieve it later (cache hit)
74/// assert_eq!(store.get(&global).unwrap(), Some(&local));
75/// ```
76#[derive(Debug)]
77pub struct ContentStore<K: Contentable, V, H: Hasher + Eq + StdHash = DefaultContentHasher> {
78    store: HashMap<ContentId<H>, V>,
79    collision_witnesses: Option<HashMap<ContentId<H>, Vec<u8>>>,
80    hits: AtomicU64,
81    misses: AtomicU64,
82    _key: std::marker::PhantomData<K>,
83}
84
85impl<K: Contentable, V, H: Hasher + Eq + StdHash> Default for ContentStore<K, V, H> {
86    fn default() -> Self {
87        Self::new()
88    }
89}
90
91impl<K: Contentable, V, H: Hasher + Eq + StdHash> ContentStore<K, V, H> {
92    /// Create a new empty content store.
93    #[must_use]
94    pub fn new() -> Self {
95        Self {
96            store: HashMap::new(),
97            collision_witnesses: None,
98            hits: AtomicU64::new(0),
99            misses: AtomicU64::new(0),
100            _key: std::marker::PhantomData,
101        }
102    }
103
104    /// Create a new content store that validates `ContentId` collisions by
105    /// storing canonical-byte witnesses.
106    #[must_use]
107    pub fn new_collision_defended() -> Self {
108        Self {
109            store: HashMap::new(),
110            collision_witnesses: Some(HashMap::new()),
111            hits: AtomicU64::new(0),
112            misses: AtomicU64::new(0),
113            _key: std::marker::PhantomData,
114        }
115    }
116
117    /// Create a content store with pre-allocated capacity.
118    #[must_use]
119    pub fn with_capacity(capacity: usize) -> Self {
120        Self {
121            store: HashMap::with_capacity(capacity),
122            collision_witnesses: None,
123            hits: AtomicU64::new(0),
124            misses: AtomicU64::new(0),
125            _key: std::marker::PhantomData,
126        }
127    }
128
129    /// Create a pre-sized collision-defended content store.
130    #[must_use]
131    pub fn with_capacity_collision_defended(capacity: usize) -> Self {
132        Self {
133            store: HashMap::with_capacity(capacity),
134            collision_witnesses: Some(HashMap::with_capacity(capacity)),
135            hits: AtomicU64::new(0),
136            misses: AtomicU64::new(0),
137            _key: std::marker::PhantomData,
138        }
139    }
140
141    /// Get a cached value by its content key.
142    ///
143    /// Updates cache metrics (hit/miss counters).
144    ///
145    /// # Errors
146    ///
147    /// Returns [`ContentableError`] if computing the content ID fails or if a
148    /// hash collision is detected (when collision detection is enabled).
149    pub fn get(&self, key: &K) -> Result<Option<&V>, ContentableError> {
150        let cid = key.content_id::<H>()?;
151        if let Some(v) = self.store.get(&cid) {
152            if let Some(witnesses) = &self.collision_witnesses {
153                let bytes = key.to_bytes()?;
154                if witnesses.get(&cid).is_some_and(|stored| stored != &bytes) {
155                    return Err(ContentableError::InvalidFormat(
156                        "content-id collision detected during get".to_string(),
157                    ));
158                }
159            }
160            self.hits.fetch_add(1, Ordering::Relaxed);
161            return Ok(Some(v));
162        }
163        self.misses.fetch_add(1, Ordering::Relaxed);
164        Ok(None)
165    }
166
167    /// Insert a value into the store.
168    ///
169    /// Returns the previous value if the key already existed.
170    ///
171    /// # Errors
172    ///
173    /// Returns [`ContentableError`] if computing the content ID fails or if a
174    /// hash collision is detected (when collision detection is enabled).
175    pub fn insert(&mut self, key: &K, value: V) -> Result<Option<V>, ContentableError> {
176        let cid = key.content_id::<H>()?;
177        if let Some(witnesses) = &mut self.collision_witnesses {
178            let bytes = key.to_bytes()?;
179            if let Some(stored) = witnesses.get(&cid) {
180                if stored != &bytes {
181                    return Err(ContentableError::InvalidFormat(
182                        "content-id collision detected during insert".to_string(),
183                    ));
184                }
185            } else {
186                witnesses.insert(cid.clone(), bytes);
187            }
188        }
189        Ok(self.store.insert(cid, value))
190    }
191
192    /// Get or compute a value.
193    ///
194    /// If the key exists, returns the cached value (cache hit).
195    /// Otherwise, computes the value using the provided function,
196    /// stores it, and returns a reference (cache miss).
197    ///
198    /// # Errors
199    ///
200    /// Returns [`ContentableError`] if computing the content ID fails or if a
201    /// hash collision is detected (when collision detection is enabled).
202    pub fn get_or_insert_with<F>(&mut self, key: &K, f: F) -> Result<&V, ContentableError>
203    where
204        F: FnOnce() -> V,
205    {
206        let cid = key.content_id::<H>()?;
207        if let Some(witnesses) = &mut self.collision_witnesses {
208            let bytes = key.to_bytes()?;
209            if let Some(stored) = witnesses.get(&cid) {
210                if stored != &bytes {
211                    return Err(ContentableError::InvalidFormat(
212                        "content-id collision detected during get_or_insert_with".to_string(),
213                    ));
214                }
215            } else {
216                witnesses.insert(cid.clone(), bytes);
217            }
218        }
219        match self.store.entry(cid) {
220            std::collections::hash_map::Entry::Occupied(entry) => {
221                self.hits.fetch_add(1, Ordering::Relaxed);
222                Ok(entry.into_mut())
223            }
224            std::collections::hash_map::Entry::Vacant(entry) => {
225                self.misses.fetch_add(1, Ordering::Relaxed);
226                Ok(entry.insert(f()))
227            }
228        }
229    }
230
231    /// Check if a key exists in the store.
232    ///
233    /// # Errors
234    ///
235    /// Returns [`ContentableError`] if computing the content ID fails.
236    pub fn contains(&self, key: &K) -> Result<bool, ContentableError> {
237        let cid = key.content_id::<H>()?;
238        Ok(self.store.contains_key(&cid))
239    }
240
241    /// Remove a value from the store.
242    ///
243    /// # Errors
244    ///
245    /// Returns [`ContentableError`] if computing the content ID fails.
246    pub fn remove(&mut self, key: &K) -> Result<Option<V>, ContentableError> {
247        let cid = key.content_id::<H>()?;
248        let removed = self.store.remove(&cid);
249        if removed.is_some() && self.collision_witnesses.is_some() {
250            if let Some(witnesses) = &mut self.collision_witnesses {
251                witnesses.remove(&cid);
252            }
253        }
254        Ok(removed)
255    }
256
257    /// Clear all entries from the store.
258    pub fn clear(&mut self) {
259        self.store.clear();
260        if let Some(witnesses) = &mut self.collision_witnesses {
261            witnesses.clear();
262        }
263    }
264
265    /// Get the number of entries in the store.
266    #[must_use]
267    pub fn len(&self) -> usize {
268        self.store.len()
269    }
270
271    /// Check if the store is empty.
272    #[must_use]
273    pub fn is_empty(&self) -> bool {
274        self.store.is_empty()
275    }
276
277    /// Get cache performance metrics.
278    #[must_use]
279    pub fn metrics(&self) -> CacheMetrics {
280        CacheMetrics {
281            hits: self.hits.load(Ordering::Relaxed),
282            misses: self.misses.load(Ordering::Relaxed),
283            size: self.store.len(),
284        }
285    }
286
287    /// Reset cache metrics to zero.
288    pub fn reset_metrics(&self) {
289        self.hits.store(0, Ordering::Relaxed);
290        self.misses.store(0, Ordering::Relaxed);
291    }
292}
293
294impl<K: Contentable, V: Clone, H: Hasher + Eq + StdHash> Clone for ContentStore<K, V, H> {
295    fn clone(&self) -> Self {
296        Self {
297            store: self.store.clone(),
298            collision_witnesses: self.collision_witnesses.clone(),
299            hits: AtomicU64::new(self.hits.load(Ordering::Relaxed)),
300            misses: AtomicU64::new(self.misses.load(Ordering::Relaxed)),
301            _key: std::marker::PhantomData,
302        }
303    }
304}
305
306/// A keyed content store for memoizing functions with multiple parameters.
307///
308/// This store uses a composite key of (ContentId, extra key) for caching,
309/// useful for functions like projection that depend on both a type and a role.
310///
311/// # Type Parameters
312///
313/// - `K`: The content key type (must implement `Contentable`)
314/// - `E`: The extra key type (e.g., role name)
315/// - `V`: The cached value type
316/// - `H`: The hash algorithm (default: `DefaultContentHasher`)
317///
318/// # Examples
319///
320/// ```
321/// use telltale_types::content_store::KeyedContentStore;
322/// use telltale_types::{GlobalType, LocalTypeR, Label};
323///
324/// let mut store: KeyedContentStore<GlobalType, String, LocalTypeR> = KeyedContentStore::new();
325///
326/// let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
327///
328/// // Store projection result for role "A"
329/// let local_a = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
330/// store.insert(&global, "A".to_string(), local_a.clone()).unwrap();
331///
332/// // Retrieve it later
333/// assert_eq!(store.get(&global, &"A".to_string()).unwrap(), Some(&local_a));
334///
335/// // Different role has different projection
336/// assert_eq!(store.get(&global, &"B".to_string()).unwrap(), None);
337/// ```
338#[derive(Debug)]
339pub struct KeyedContentStore<
340    K: Contentable,
341    E: StdHash + Eq,
342    V,
343    H: Hasher + Eq + StdHash = DefaultContentHasher,
344> {
345    store: HashMap<ContentId<H>, HashMap<E, V>>,
346    collision_witnesses: Option<HashMap<ContentId<H>, Vec<u8>>>,
347    hits: AtomicU64,
348    misses: AtomicU64,
349    _key: std::marker::PhantomData<K>,
350}
351
352impl<K: Contentable, E: StdHash + Eq + Clone, V, H: Hasher + Eq + StdHash> Default
353    for KeyedContentStore<K, E, V, H>
354{
355    fn default() -> Self {
356        Self::new()
357    }
358}
359
360impl<K: Contentable, E: StdHash + Eq + Clone, V, H: Hasher + Eq + StdHash>
361    KeyedContentStore<K, E, V, H>
362{
363    /// Create a new empty keyed content store.
364    #[must_use]
365    pub fn new() -> Self {
366        Self {
367            store: HashMap::new(),
368            collision_witnesses: None,
369            hits: AtomicU64::new(0),
370            misses: AtomicU64::new(0),
371            _key: std::marker::PhantomData,
372        }
373    }
374
375    /// Create a new keyed content store with collision-defense enabled.
376    #[must_use]
377    pub fn new_collision_defended() -> Self {
378        Self {
379            store: HashMap::new(),
380            collision_witnesses: Some(HashMap::new()),
381            hits: AtomicU64::new(0),
382            misses: AtomicU64::new(0),
383            _key: std::marker::PhantomData,
384        }
385    }
386
387    /// Create a keyed content store with pre-allocated capacity.
388    #[must_use]
389    pub fn with_capacity(capacity: usize) -> Self {
390        Self {
391            store: HashMap::with_capacity(capacity),
392            collision_witnesses: None,
393            hits: AtomicU64::new(0),
394            misses: AtomicU64::new(0),
395            _key: std::marker::PhantomData,
396        }
397    }
398
399    /// Create a pre-sized keyed collision-defended store.
400    #[must_use]
401    pub fn with_capacity_collision_defended(capacity: usize) -> Self {
402        Self {
403            store: HashMap::with_capacity(capacity),
404            collision_witnesses: Some(HashMap::with_capacity(capacity)),
405            hits: AtomicU64::new(0),
406            misses: AtomicU64::new(0),
407            _key: std::marker::PhantomData,
408        }
409    }
410
411    /// Get a cached value by precomputed content id and extra key.
412    #[must_use]
413    pub fn get_with_content_id(&self, cid: &ContentId<H>, extra: &E) -> Option<&V> {
414        match self.store.get(cid).and_then(|inner| inner.get(extra)) {
415            Some(v) => {
416                self.hits.fetch_add(1, Ordering::Relaxed);
417                Some(v)
418            }
419            None => {
420                self.misses.fetch_add(1, Ordering::Relaxed);
421                None
422            }
423        }
424    }
425
426    /// Get a cached value by content key and extra key.
427    ///
428    /// # Errors
429    ///
430    /// Returns [`ContentableError`] if computing the content ID fails or if a
431    /// hash collision is detected (when collision detection is enabled).
432    pub fn get(&self, key: &K, extra: &E) -> Result<Option<&V>, ContentableError> {
433        let cid = key.content_id::<H>()?;
434        if let Some(witnesses) = &self.collision_witnesses {
435            if witnesses.contains_key(&cid) {
436                let bytes = key.to_bytes()?;
437                if witnesses.get(&cid).is_some_and(|stored| stored != &bytes) {
438                    return Err(ContentableError::InvalidFormat(
439                        "content-id collision detected during keyed get".to_string(),
440                    ));
441                }
442            }
443        }
444        Ok(self.get_with_content_id(&cid, extra))
445    }
446
447    /// Insert a value by precomputed content id and extra key.
448    pub fn insert_with_content_id(&mut self, cid: ContentId<H>, extra: E, value: V) -> Option<V> {
449        self.store.entry(cid).or_default().insert(extra, value)
450    }
451
452    /// Insert a value into the store.
453    ///
454    /// # Errors
455    ///
456    /// Returns [`ContentableError`] if computing the content ID fails or if a
457    /// hash collision is detected (when collision detection is enabled).
458    pub fn insert(&mut self, key: &K, extra: E, value: V) -> Result<Option<V>, ContentableError> {
459        let cid = key.content_id::<H>()?;
460        if let Some(witnesses) = &mut self.collision_witnesses {
461            let bytes = key.to_bytes()?;
462            if let Some(stored) = witnesses.get(&cid) {
463                if stored != &bytes {
464                    return Err(ContentableError::InvalidFormat(
465                        "content-id collision detected during keyed insert".to_string(),
466                    ));
467                }
468            } else {
469                witnesses.insert(cid.clone(), bytes);
470            }
471        }
472        Ok(self.insert_with_content_id(cid, extra, value))
473    }
474
475    /// Get or compute a value.
476    ///
477    /// # Errors
478    ///
479    /// Returns [`ContentableError`] if computing the content ID fails or if a
480    /// hash collision is detected (when collision detection is enabled).
481    pub fn get_or_insert_with<F>(&mut self, key: &K, extra: E, f: F) -> Result<&V, ContentableError>
482    where
483        F: FnOnce() -> V,
484    {
485        let cid = key.content_id::<H>()?;
486        if let Some(witnesses) = &mut self.collision_witnesses {
487            let bytes = key.to_bytes()?;
488            if let Some(stored) = witnesses.get(&cid) {
489                if stored != &bytes {
490                    return Err(ContentableError::InvalidFormat(
491                        "content-id collision detected during keyed get_or_insert_with".to_string(),
492                    ));
493                }
494            } else {
495                witnesses.insert(cid.clone(), bytes);
496            }
497        }
498        match self.store.entry(cid).or_default().entry(extra) {
499            std::collections::hash_map::Entry::Occupied(entry) => {
500                self.hits.fetch_add(1, Ordering::Relaxed);
501                Ok(entry.into_mut())
502            }
503            std::collections::hash_map::Entry::Vacant(entry) => {
504                self.misses.fetch_add(1, Ordering::Relaxed);
505                Ok(entry.insert(f()))
506            }
507        }
508    }
509
510    /// Check if a key pair exists in the store.
511    ///
512    /// # Errors
513    ///
514    /// Returns [`ContentableError`] if computing the content ID fails.
515    pub fn contains(&self, key: &K, extra: &E) -> Result<bool, ContentableError> {
516        let cid = key.content_id::<H>()?;
517        Ok(self.contains_with_content_id(&cid, extra))
518    }
519
520    /// Check if a precomputed content id + extra key exists.
521    #[must_use]
522    pub fn contains_with_content_id(&self, cid: &ContentId<H>, extra: &E) -> bool {
523        self.store
524            .get(cid)
525            .is_some_and(|inner| inner.contains_key(extra))
526    }
527
528    /// Remove a value from the store.
529    ///
530    /// # Errors
531    ///
532    /// Returns [`ContentableError`] if computing the content ID fails.
533    pub fn remove(&mut self, key: &K, extra: &E) -> Result<Option<V>, ContentableError> {
534        let cid = key.content_id::<H>()?;
535        if let Some(inner) = self.store.get_mut(&cid) {
536            let removed = inner.remove(extra);
537            if inner.is_empty() {
538                self.store.remove(&cid);
539                if let Some(witnesses) = &mut self.collision_witnesses {
540                    witnesses.remove(&cid);
541                }
542            }
543            Ok(removed)
544        } else {
545            Ok(None)
546        }
547    }
548
549    /// Clear all entries from the store.
550    pub fn clear(&mut self) {
551        self.store.clear();
552        if let Some(witnesses) = &mut self.collision_witnesses {
553            witnesses.clear();
554        }
555    }
556
557    /// Get the number of entries in the store.
558    #[must_use]
559    pub fn len(&self) -> usize {
560        self.store.values().map(HashMap::len).sum()
561    }
562
563    /// Check if the store is empty.
564    #[must_use]
565    pub fn is_empty(&self) -> bool {
566        self.len() == 0
567    }
568
569    /// Get cache performance metrics.
570    #[must_use]
571    pub fn metrics(&self) -> CacheMetrics {
572        CacheMetrics {
573            hits: self.hits.load(Ordering::Relaxed),
574            misses: self.misses.load(Ordering::Relaxed),
575            size: self.store.len(),
576        }
577    }
578
579    /// Reset cache metrics to zero.
580    pub fn reset_metrics(&self) {
581        self.hits.store(0, Ordering::Relaxed);
582        self.misses.store(0, Ordering::Relaxed);
583    }
584}
585
586impl<K: Contentable, E: StdHash + Eq + Clone, V: Clone, H: Hasher + Eq + StdHash> Clone
587    for KeyedContentStore<K, E, V, H>
588{
589    fn clone(&self) -> Self {
590        Self {
591            store: self.store.clone(),
592            collision_witnesses: self.collision_witnesses.clone(),
593            hits: AtomicU64::new(self.hits.load(Ordering::Relaxed)),
594            misses: AtomicU64::new(self.misses.load(Ordering::Relaxed)),
595            _key: std::marker::PhantomData,
596        }
597    }
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    use crate::content_id::Hasher;
604    use crate::{GlobalType, Label, LocalTypeR};
605
606    #[derive(Clone, Default, PartialEq, Eq, Hash)]
607    struct ConstantHasher;
608
609    impl Hasher for ConstantHasher {
610        type Digest = [u8; 1];
611        const HASH_SIZE: usize = 1;
612
613        fn digest(_data: &[u8]) -> Self::Digest {
614            [0u8]
615        }
616
617        fn algorithm_name() -> &'static str {
618            "constant"
619        }
620    }
621
622    #[test]
623    fn test_content_store_basic() {
624        let mut store: ContentStore<GlobalType, LocalTypeR> = ContentStore::new();
625
626        let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
627        let local = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
628
629        // Initially empty
630        assert!(store.is_empty());
631        assert_eq!(store.get(&global).unwrap(), None);
632
633        // Insert
634        store.insert(&global, local.clone()).unwrap();
635        assert_eq!(store.len(), 1);
636
637        // Get (cache hit)
638        assert_eq!(store.get(&global).unwrap(), Some(&local));
639
640        // Metrics
641        let metrics = store.metrics();
642        assert_eq!(metrics.hits, 1); // The second get
643        assert_eq!(metrics.misses, 1); // The first get before insert
644    }
645
646    #[test]
647    fn test_content_store_alpha_equivalence() {
648        let mut store: ContentStore<GlobalType, String> = ContentStore::new();
649
650        // Two α-equivalent types should map to the same cache entry
651        let g1 = GlobalType::mu(
652            "x",
653            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("x")),
654        );
655        let g2 = GlobalType::mu(
656            "y",
657            GlobalType::send("A", "B", Label::new("msg"), GlobalType::var("y")),
658        );
659
660        store.insert(&g1, "result".to_string()).unwrap();
661
662        // g2 should hit the same cache entry
663        assert_eq!(store.get(&g2).unwrap(), Some(&"result".to_string()));
664    }
665
666    #[test]
667    fn test_content_store_get_or_insert_with() {
668        let mut store: ContentStore<GlobalType, i32> = ContentStore::new();
669        let global = GlobalType::End;
670
671        let mut computed = false;
672        let value = store
673            .get_or_insert_with(&global, || {
674                computed = true;
675                42
676            })
677            .unwrap();
678        assert_eq!(*value, 42);
679        assert!(computed);
680
681        // Second call should not compute
682        computed = false;
683        let value = store
684            .get_or_insert_with(&global, || {
685                computed = true;
686                99
687            })
688            .unwrap();
689        assert_eq!(*value, 42); // Same value
690        assert!(!computed); // Not recomputed
691
692        let metrics = store.metrics();
693        assert_eq!(metrics.hits, 1);
694        assert_eq!(metrics.misses, 1);
695    }
696
697    #[test]
698    fn test_keyed_content_store() {
699        let mut store: KeyedContentStore<GlobalType, String, LocalTypeR> = KeyedContentStore::new();
700
701        let global = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
702        let local_a = LocalTypeR::send("B", Label::new("msg"), LocalTypeR::End);
703        let local_b = LocalTypeR::recv("A", Label::new("msg"), LocalTypeR::End);
704
705        // Store projections for different roles
706        store
707            .insert(&global, "A".to_string(), local_a.clone())
708            .unwrap();
709        store
710            .insert(&global, "B".to_string(), local_b.clone())
711            .unwrap();
712
713        assert_eq!(store.len(), 2);
714        assert_eq!(
715            store.get(&global, &"A".to_string()).unwrap(),
716            Some(&local_a)
717        );
718        assert_eq!(
719            store.get(&global, &"B".to_string()).unwrap(),
720            Some(&local_b)
721        );
722        assert_eq!(store.get(&global, &"C".to_string()).unwrap(), None);
723    }
724
725    #[test]
726    fn test_cache_metrics() {
727        let mut store: ContentStore<GlobalType, i32> = ContentStore::new();
728        let g1 = GlobalType::End;
729        let g2 = GlobalType::send("A", "B", Label::new("msg"), GlobalType::End);
730
731        // Miss
732        store.get(&g1).unwrap();
733        store.get(&g2).unwrap();
734
735        // Insert
736        store.insert(&g1, 1).unwrap();
737
738        // Hit
739        store.get(&g1).unwrap();
740        store.get(&g1).unwrap();
741
742        // Miss again
743        store.get(&g2).unwrap();
744
745        let metrics = store.metrics();
746        assert_eq!(metrics.misses, 3); // g1 miss, g2 miss, g2 miss
747        assert_eq!(metrics.hits, 2); // g1 hit x2
748        assert!((metrics.hit_rate() - 40.0).abs() < 0.01); // 2 / 5 = 40%
749    }
750
751    #[test]
752    fn test_content_store_clear() {
753        let mut store: ContentStore<GlobalType, i32> = ContentStore::new();
754
755        store.insert(&GlobalType::End, 1).unwrap();
756        store
757            .insert(
758                &GlobalType::send("A", "B", Label::new("msg"), GlobalType::End),
759                2,
760            )
761            .unwrap();
762
763        assert_eq!(store.len(), 2);
764
765        store.clear();
766        assert!(store.is_empty());
767    }
768
769    #[test]
770    fn test_content_store_remove() {
771        let mut store: ContentStore<GlobalType, i32> = ContentStore::new();
772        let global = GlobalType::End;
773
774        store.insert(&global, 42).unwrap();
775        assert!(store.contains(&global).unwrap());
776
777        let removed = store.remove(&global).unwrap();
778        assert_eq!(removed, Some(42));
779        assert!(!store.contains(&global).unwrap());
780    }
781
782    #[test]
783    fn test_collision_defense_rejects_hash_alias_in_content_store() {
784        let mut store: ContentStore<GlobalType, i32, ConstantHasher> =
785            ContentStore::new_collision_defended();
786        let g1 = GlobalType::send("A", "B", Label::new("x"), GlobalType::End);
787        let g2 = GlobalType::send("A", "B", Label::new("y"), GlobalType::End);
788        store.insert(&g1, 1).expect("first insert should succeed");
789        let result = store.insert(&g2, 2);
790        assert!(matches!(result, Err(ContentableError::InvalidFormat(_))));
791    }
792
793    #[test]
794    fn test_collision_defense_rejects_hash_alias_in_keyed_store() {
795        let mut store: KeyedContentStore<GlobalType, String, i32, ConstantHasher> =
796            KeyedContentStore::new_collision_defended();
797        let g1 = GlobalType::send("A", "B", Label::new("x"), GlobalType::End);
798        let g2 = GlobalType::send("A", "B", Label::new("y"), GlobalType::End);
799        store
800            .insert(&g1, "A".to_string(), 1)
801            .expect("first insert should succeed");
802        let result = store.insert(&g2, "B".to_string(), 2);
803        assert!(matches!(result, Err(ContentableError::InvalidFormat(_))));
804    }
805}