Skip to main content

hashtree_core/
store.rs

1//! Content-addressed key-value store interfaces and implementations
2
3use async_trait::async_trait;
4use std::collections::HashMap;
5use std::sync::{Arc, RwLock};
6
7use crate::types::{to_hex, Hash};
8
9/// Storage statistics
10#[derive(Debug, Clone, Default)]
11pub struct StoreStats {
12    /// Number of items in store
13    pub count: u64,
14    /// Total bytes stored
15    pub bytes: u64,
16    /// Number of pinned items
17    pub pinned_count: u64,
18    /// Bytes used by pinned items
19    pub pinned_bytes: u64,
20}
21
22/// Result of a batch insert.
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
24pub struct PutManyReport {
25    /// Number of candidate items passed by the caller, including duplicates.
26    pub total: usize,
27    /// Number of blobs newly inserted into the store.
28    pub inserted: usize,
29    /// Logical bytes newly inserted into the store.
30    pub inserted_bytes: u64,
31    /// Hashes that were newly inserted, in insertion order.
32    pub inserted_hashes: Vec<Hash>,
33}
34
35/// Content-addressed key-value store interface
36#[async_trait]
37pub trait Store: Send + Sync {
38    /// Store data by its hash
39    /// Returns true if newly stored, false if already existed
40    async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError>;
41
42    /// Store multiple blobs.
43    /// Returns the number of newly stored items.
44    async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
45        let mut inserted = 0usize;
46        for (hash, data) in items {
47            if self.put(hash, data).await? {
48                inserted += 1;
49            }
50        }
51        Ok(inserted)
52    }
53
54    /// Retrieve data by hash
55    /// Returns data or None if not found
56    async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError>;
57
58    /// Check if hash exists
59    async fn has(&self, hash: &Hash) -> Result<bool, StoreError>;
60
61    /// Delete by hash
62    /// Returns true if deleted, false if didn't exist
63    async fn delete(&self, hash: &Hash) -> Result<bool, StoreError>;
64
65    // ========================================================================
66    // Optional: Storage limits and eviction (default no-op implementations)
67    // ========================================================================
68
69    /// Set maximum storage size in bytes. 0 = unlimited.
70    fn set_max_bytes(&self, _max: u64) {}
71
72    /// Get maximum storage size. None = unlimited.
73    fn max_bytes(&self) -> Option<u64> {
74        None
75    }
76
77    /// Get storage statistics
78    async fn stats(&self) -> StoreStats {
79        StoreStats::default()
80    }
81
82    /// Evict unpinned items if over storage limit.
83    /// Returns number of bytes freed.
84    async fn evict_if_needed(&self) -> Result<u64, StoreError> {
85        Ok(0)
86    }
87
88    // ========================================================================
89    // Optional: Pinning (default no-op implementations)
90    // ========================================================================
91
92    /// Pin a hash (increment ref count). Pinned items are not evicted.
93    async fn pin(&self, _hash: &Hash) -> Result<(), StoreError> {
94        Ok(())
95    }
96
97    /// Unpin a hash (decrement ref count). Item can be evicted when count reaches 0.
98    async fn unpin(&self, _hash: &Hash) -> Result<(), StoreError> {
99        Ok(())
100    }
101
102    /// Get pin count for a hash. 0 = not pinned.
103    fn pin_count(&self, _hash: &Hash) -> u32 {
104        0
105    }
106
107    /// Check if hash is pinned (pin count > 0)
108    fn is_pinned(&self, hash: &Hash) -> bool {
109        self.pin_count(hash) > 0
110    }
111}
112
113/// Store error type
114#[derive(Debug, thiserror::Error)]
115pub enum StoreError {
116    #[error("IO error: {0}")]
117    Io(#[from] std::io::Error),
118    #[error("Store error: {0}")]
119    Other(String),
120}
121
122#[derive(Debug, Default)]
123struct BufferedStoreInner {
124    pending: HashMap<Hash, Vec<u8>>,
125    order: Vec<Hash>,
126}
127
128#[derive(Debug, Clone, Copy)]
129struct BufferedStoreOptions {
130    check_base_on_put: bool,
131}
132
133impl Default for BufferedStoreOptions {
134    fn default() -> Self {
135        Self {
136            check_base_on_put: true,
137        }
138    }
139}
140
141/// Buffered overlay store that keeps writes in memory until flushed.
142#[derive(Debug, Clone)]
143pub struct BufferedStore<S: Store> {
144    base: Arc<S>,
145    inner: Arc<RwLock<BufferedStoreInner>>,
146    options: BufferedStoreOptions,
147}
148
149impl<S: Store> BufferedStore<S> {
150    pub fn new(base: Arc<S>) -> Self {
151        Self::with_options(base, BufferedStoreOptions::default())
152    }
153
154    pub fn new_optimistic(base: Arc<S>) -> Self {
155        Self::with_options(
156            base,
157            BufferedStoreOptions {
158                check_base_on_put: false,
159            },
160        )
161    }
162
163    fn with_options(base: Arc<S>, options: BufferedStoreOptions) -> Self {
164        Self {
165            base,
166            inner: Arc::new(RwLock::new(BufferedStoreInner::default())),
167            options,
168        }
169    }
170
171    pub async fn flush(&self) -> Result<usize, StoreError> {
172        let items = {
173            let mut inner = self.inner.write().unwrap();
174            if inner.order.is_empty() {
175                return Ok(0);
176            }
177
178            let order = std::mem::take(&mut inner.order);
179            let mut items = Vec::with_capacity(order.len());
180            for hash in order {
181                if let Some(data) = inner.pending.remove(&hash) {
182                    items.push((hash, data));
183                }
184            }
185            items
186        };
187
188        self.base.put_many(items).await
189    }
190}
191
192#[async_trait]
193impl<S: Store> Store for BufferedStore<S> {
194    async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError> {
195        {
196            let inner = self.inner.read().unwrap();
197            if inner.pending.contains_key(&hash) {
198                return Ok(false);
199            }
200        }
201
202        if self.options.check_base_on_put && self.base.has(&hash).await? {
203            return Ok(false);
204        }
205
206        let mut inner = self.inner.write().unwrap();
207        if inner.pending.contains_key(&hash) {
208            return Ok(false);
209        }
210        inner.order.push(hash);
211        inner.pending.insert(hash, data);
212        Ok(true)
213    }
214
215    async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
216        let mut inserted = 0usize;
217        for (hash, data) in items {
218            if self.put(hash, data).await? {
219                inserted += 1;
220            }
221        }
222        Ok(inserted)
223    }
224
225    async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
226        if let Some(data) = self.inner.read().unwrap().pending.get(hash).cloned() {
227            return Ok(Some(data));
228        }
229        self.base.get(hash).await
230    }
231
232    async fn has(&self, hash: &Hash) -> Result<bool, StoreError> {
233        if self.inner.read().unwrap().pending.contains_key(hash) {
234            return Ok(true);
235        }
236        self.base.has(hash).await
237    }
238
239    async fn delete(&self, hash: &Hash) -> Result<bool, StoreError> {
240        let removed = {
241            let mut inner = self.inner.write().unwrap();
242            let removed = inner.pending.remove(hash).is_some();
243            if removed {
244                inner.order.retain(|queued| queued != hash);
245            }
246            removed
247        };
248
249        if removed {
250            return Ok(true);
251        }
252
253        self.base.delete(hash).await
254    }
255
256    async fn stats(&self) -> StoreStats {
257        let mut stats = self.base.stats().await;
258        let pending_bytes = self
259            .inner
260            .read()
261            .unwrap()
262            .pending
263            .values()
264            .map(|data| data.len() as u64)
265            .sum::<u64>();
266        stats.count += self.inner.read().unwrap().pending.len() as u64;
267        stats.bytes += pending_bytes;
268        stats
269    }
270
271    async fn evict_if_needed(&self) -> Result<u64, StoreError> {
272        self.base.evict_if_needed().await
273    }
274
275    async fn pin(&self, hash: &Hash) -> Result<(), StoreError> {
276        self.base.pin(hash).await
277    }
278
279    async fn unpin(&self, hash: &Hash) -> Result<(), StoreError> {
280        self.base.unpin(hash).await
281    }
282
283    fn pin_count(&self, hash: &Hash) -> u32 {
284        self.base.pin_count(hash)
285    }
286}
287
288/// Entry in the memory store with metadata for LRU
289#[derive(Debug, Clone)]
290struct MemoryEntry {
291    data: Vec<u8>,
292    /// Insertion order for LRU (lower = older)
293    order: u64,
294}
295
296/// Internal state for MemoryStore
297#[derive(Debug, Default)]
298struct MemoryStoreInner {
299    data: HashMap<String, MemoryEntry>,
300    pins: HashMap<String, u32>,
301    next_order: u64,
302    max_bytes: Option<u64>,
303}
304
305/// In-memory content-addressed store with LRU eviction and pinning
306#[derive(Debug, Clone, Default)]
307pub struct MemoryStore {
308    inner: Arc<RwLock<MemoryStoreInner>>,
309}
310
311impl MemoryStore {
312    pub fn new() -> Self {
313        Self {
314            inner: Arc::new(RwLock::new(MemoryStoreInner::default())),
315        }
316    }
317
318    /// Create a new store with a maximum size limit
319    pub fn with_max_bytes(max_bytes: u64) -> Self {
320        Self {
321            inner: Arc::new(RwLock::new(MemoryStoreInner {
322                max_bytes: if max_bytes > 0 { Some(max_bytes) } else { None },
323                ..Default::default()
324            })),
325        }
326    }
327
328    /// Get number of stored items
329    pub fn size(&self) -> usize {
330        self.inner.read().unwrap().data.len()
331    }
332
333    /// Get total bytes stored
334    pub fn total_bytes(&self) -> usize {
335        self.inner
336            .read()
337            .unwrap()
338            .data
339            .values()
340            .map(|e| e.data.len())
341            .sum()
342    }
343
344    /// Clear all data (but not pins)
345    pub fn clear(&self) {
346        self.inner.write().unwrap().data.clear();
347    }
348
349    /// List all hashes
350    pub fn keys(&self) -> Vec<Hash> {
351        self.inner
352            .read()
353            .unwrap()
354            .data
355            .keys()
356            .filter_map(|hex| {
357                let bytes = hex::decode(hex).ok()?;
358                if bytes.len() != 32 {
359                    return None;
360                }
361                let mut hash = [0u8; 32];
362                hash.copy_from_slice(&bytes);
363                Some(hash)
364            })
365            .collect()
366    }
367
368    /// Evict oldest unpinned entries until under target bytes
369    fn evict_to_target(&self, target_bytes: u64) -> u64 {
370        let mut inner = self.inner.write().unwrap();
371
372        let current_bytes: u64 = inner.data.values().map(|e| e.data.len() as u64).sum();
373        if current_bytes <= target_bytes {
374            return 0;
375        }
376
377        // Collect unpinned entries sorted by order (oldest first)
378        let mut unpinned: Vec<(String, u64, u64)> = inner
379            .data
380            .iter()
381            .filter(|(key, _)| inner.pins.get(*key).copied().unwrap_or(0) == 0)
382            .map(|(key, entry)| (key.clone(), entry.order, entry.data.len() as u64))
383            .collect();
384
385        unpinned.sort_by_key(|(_, order, _)| *order);
386
387        let mut freed = 0u64;
388        let to_free = current_bytes - target_bytes;
389
390        for (key, _, size) in unpinned {
391            if freed >= to_free {
392                break;
393            }
394            inner.data.remove(&key);
395            freed += size;
396        }
397
398        freed
399    }
400}
401
402#[async_trait]
403impl Store for MemoryStore {
404    async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError> {
405        let key = to_hex(&hash);
406        let mut inner = self.inner.write().unwrap();
407        if inner.data.contains_key(&key) {
408            return Ok(false);
409        }
410        let order = inner.next_order;
411        inner.next_order += 1;
412        inner.data.insert(key, MemoryEntry { data, order });
413        Ok(true)
414    }
415
416    async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
417        let mut inserted = 0usize;
418        let mut inner = self.inner.write().unwrap();
419        for (hash, data) in items {
420            let key = to_hex(&hash);
421            if inner.data.contains_key(&key) {
422                continue;
423            }
424            let order = inner.next_order;
425            inner.next_order += 1;
426            inner.data.insert(key, MemoryEntry { data, order });
427            inserted += 1;
428        }
429        Ok(inserted)
430    }
431
432    async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
433        let key = to_hex(hash);
434        let inner = self.inner.read().unwrap();
435        Ok(inner.data.get(&key).map(|e| e.data.clone()))
436    }
437
438    async fn has(&self, hash: &Hash) -> Result<bool, StoreError> {
439        let key = to_hex(hash);
440        Ok(self.inner.read().unwrap().data.contains_key(&key))
441    }
442
443    async fn delete(&self, hash: &Hash) -> Result<bool, StoreError> {
444        let key = to_hex(hash);
445        let mut inner = self.inner.write().unwrap();
446        // Also remove pin entry if exists
447        inner.pins.remove(&key);
448        Ok(inner.data.remove(&key).is_some())
449    }
450
451    fn set_max_bytes(&self, max: u64) {
452        self.inner.write().unwrap().max_bytes = if max > 0 { Some(max) } else { None };
453    }
454
455    fn max_bytes(&self) -> Option<u64> {
456        self.inner.read().unwrap().max_bytes
457    }
458
459    async fn stats(&self) -> StoreStats {
460        let inner = self.inner.read().unwrap();
461        let mut count = 0u64;
462        let mut bytes = 0u64;
463        let mut pinned_count = 0u64;
464        let mut pinned_bytes = 0u64;
465
466        for (key, entry) in &inner.data {
467            count += 1;
468            bytes += entry.data.len() as u64;
469            if inner.pins.get(key).copied().unwrap_or(0) > 0 {
470                pinned_count += 1;
471                pinned_bytes += entry.data.len() as u64;
472            }
473        }
474
475        StoreStats {
476            count,
477            bytes,
478            pinned_count,
479            pinned_bytes,
480        }
481    }
482
483    async fn evict_if_needed(&self) -> Result<u64, StoreError> {
484        let max = match self.inner.read().unwrap().max_bytes {
485            Some(m) => m,
486            None => return Ok(0), // No limit set
487        };
488
489        let current: u64 = self
490            .inner
491            .read()
492            .unwrap()
493            .data
494            .values()
495            .map(|e| e.data.len() as u64)
496            .sum();
497
498        if current <= max {
499            return Ok(0);
500        }
501
502        // Evict to 90% of max to avoid frequent evictions
503        let target = max * 9 / 10;
504        Ok(self.evict_to_target(target))
505    }
506
507    async fn pin(&self, hash: &Hash) -> Result<(), StoreError> {
508        let key = to_hex(hash);
509        let mut inner = self.inner.write().unwrap();
510        *inner.pins.entry(key).or_insert(0) += 1;
511        Ok(())
512    }
513
514    async fn unpin(&self, hash: &Hash) -> Result<(), StoreError> {
515        let key = to_hex(hash);
516        let mut inner = self.inner.write().unwrap();
517        if let Some(count) = inner.pins.get_mut(&key) {
518            if *count > 0 {
519                *count -= 1;
520            }
521            if *count == 0 {
522                inner.pins.remove(&key);
523            }
524        }
525        Ok(())
526    }
527
528    fn pin_count(&self, hash: &Hash) -> u32 {
529        let key = to_hex(hash);
530        self.inner
531            .read()
532            .unwrap()
533            .pins
534            .get(&key)
535            .copied()
536            .unwrap_or(0)
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::hash::sha256;
544
545    #[tokio::test]
546    async fn test_put_returns_true_for_new() {
547        let store = MemoryStore::new();
548        let data = vec![1u8, 2, 3];
549        let hash = sha256(&data);
550
551        let result = store.put(hash, data).await.unwrap();
552        assert!(result);
553    }
554
555    #[tokio::test]
556    async fn test_put_returns_false_for_duplicate() {
557        let store = MemoryStore::new();
558        let data = vec![1u8, 2, 3];
559        let hash = sha256(&data);
560
561        store.put(hash, data.clone()).await.unwrap();
562        let result = store.put(hash, data).await.unwrap();
563        assert!(!result);
564    }
565
566    #[tokio::test]
567    async fn test_put_many_counts_only_new_items() {
568        let store = MemoryStore::new();
569        let data1 = vec![1u8, 2, 3];
570        let data2 = vec![4u8, 5, 6];
571        let hash1 = sha256(&data1);
572        let hash2 = sha256(&data2);
573
574        store.put(hash1, data1.clone()).await.unwrap();
575        let inserted = store
576            .put_many(vec![(hash1, data1), (hash2, data2.clone())])
577            .await
578            .unwrap();
579
580        assert_eq!(inserted, 1);
581        assert_eq!(store.get(&hash2).await.unwrap(), Some(data2));
582    }
583
584    #[tokio::test]
585    async fn test_buffered_store_flushes_pending_writes() {
586        let base = std::sync::Arc::new(MemoryStore::new());
587        let buffered = BufferedStore::new(std::sync::Arc::clone(&base));
588        let data = vec![9u8, 8, 7];
589        let hash = sha256(&data);
590
591        assert!(buffered.put(hash, data.clone()).await.unwrap());
592        assert_eq!(buffered.get(&hash).await.unwrap(), Some(data.clone()));
593        assert_eq!(base.get(&hash).await.unwrap(), None);
594
595        let flushed = buffered.flush().await.unwrap();
596
597        assert_eq!(flushed, 1);
598        assert_eq!(base.get(&hash).await.unwrap(), Some(data));
599    }
600
601    #[tokio::test]
602    async fn test_optimistic_buffered_store_avoids_base_probe_but_preserves_contents() {
603        let base = std::sync::Arc::new(MemoryStore::new());
604        let buffered = BufferedStore::new_optimistic(std::sync::Arc::clone(&base));
605        let data = vec![4u8, 5, 6];
606        let hash = sha256(&data);
607
608        base.put(hash, data.clone()).await.unwrap();
609
610        assert!(buffered.put(hash, data.clone()).await.unwrap());
611        assert_eq!(buffered.get(&hash).await.unwrap(), Some(data.clone()));
612
613        let flushed = buffered.flush().await.unwrap();
614
615        assert_eq!(flushed, 0);
616        assert_eq!(base.get(&hash).await.unwrap(), Some(data));
617    }
618
619    #[tokio::test]
620    async fn test_get_returns_data() {
621        let store = MemoryStore::new();
622        let data = vec![1u8, 2, 3];
623        let hash = sha256(&data);
624
625        store.put(hash, data.clone()).await.unwrap();
626        let result = store.get(&hash).await.unwrap();
627
628        assert_eq!(result, Some(data));
629    }
630
631    #[tokio::test]
632    async fn test_get_returns_none_for_missing() {
633        let store = MemoryStore::new();
634        let hash = [0u8; 32];
635
636        let result = store.get(&hash).await.unwrap();
637        assert!(result.is_none());
638    }
639
640    #[tokio::test]
641    async fn test_has_returns_true() {
642        let store = MemoryStore::new();
643        let data = vec![1u8, 2, 3];
644        let hash = sha256(&data);
645
646        store.put(hash, data).await.unwrap();
647        assert!(store.has(&hash).await.unwrap());
648    }
649
650    #[tokio::test]
651    async fn test_has_returns_false() {
652        let store = MemoryStore::new();
653        let hash = [0u8; 32];
654
655        assert!(!store.has(&hash).await.unwrap());
656    }
657
658    #[tokio::test]
659    async fn test_delete_returns_true() {
660        let store = MemoryStore::new();
661        let data = vec![1u8, 2, 3];
662        let hash = sha256(&data);
663
664        store.put(hash, data).await.unwrap();
665        let result = store.delete(&hash).await.unwrap();
666
667        assert!(result);
668        assert!(!store.has(&hash).await.unwrap());
669    }
670
671    #[tokio::test]
672    async fn test_delete_returns_false() {
673        let store = MemoryStore::new();
674        let hash = [0u8; 32];
675
676        let result = store.delete(&hash).await.unwrap();
677        assert!(!result);
678    }
679
680    #[tokio::test]
681    async fn test_size() {
682        let store = MemoryStore::new();
683        assert_eq!(store.size(), 0);
684
685        let data1 = vec![1u8];
686        let data2 = vec![2u8];
687        let hash1 = sha256(&data1);
688        let hash2 = sha256(&data2);
689
690        store.put(hash1, data1).await.unwrap();
691        store.put(hash2, data2).await.unwrap();
692
693        assert_eq!(store.size(), 2);
694    }
695
696    #[tokio::test]
697    async fn test_total_bytes() {
698        let store = MemoryStore::new();
699        assert_eq!(store.total_bytes(), 0);
700
701        let data1 = vec![1u8, 2, 3];
702        let data2 = vec![4u8, 5];
703        let hash1 = sha256(&data1);
704        let hash2 = sha256(&data2);
705
706        store.put(hash1, data1).await.unwrap();
707        store.put(hash2, data2).await.unwrap();
708
709        assert_eq!(store.total_bytes(), 5);
710    }
711
712    #[tokio::test]
713    async fn test_clear() {
714        let store = MemoryStore::new();
715        let data = vec![1u8, 2, 3];
716        let hash = sha256(&data);
717
718        store.put(hash, data).await.unwrap();
719        store.clear();
720
721        assert_eq!(store.size(), 0);
722        assert!(!store.has(&hash).await.unwrap());
723    }
724
725    #[tokio::test]
726    async fn test_keys() {
727        let store = MemoryStore::new();
728        assert!(store.keys().is_empty());
729
730        let data1 = vec![1u8];
731        let data2 = vec![2u8];
732        let hash1 = sha256(&data1);
733        let hash2 = sha256(&data2);
734
735        store.put(hash1, data1).await.unwrap();
736        store.put(hash2, data2).await.unwrap();
737
738        let keys = store.keys();
739        assert_eq!(keys.len(), 2);
740
741        let mut hex_keys: Vec<_> = keys.iter().map(to_hex).collect();
742        hex_keys.sort();
743        let mut expected: Vec<_> = vec![to_hex(&hash1), to_hex(&hash2)];
744        expected.sort();
745        assert_eq!(hex_keys, expected);
746    }
747
748    #[tokio::test]
749    async fn test_pin_and_unpin() {
750        let store = MemoryStore::new();
751        let data = vec![1u8, 2, 3];
752        let hash = sha256(&data);
753
754        store.put(hash, data).await.unwrap();
755
756        // Initially not pinned
757        assert!(!store.is_pinned(&hash));
758        assert_eq!(store.pin_count(&hash), 0);
759
760        // Pin
761        store.pin(&hash).await.unwrap();
762        assert!(store.is_pinned(&hash));
763        assert_eq!(store.pin_count(&hash), 1);
764
765        // Unpin
766        store.unpin(&hash).await.unwrap();
767        assert!(!store.is_pinned(&hash));
768        assert_eq!(store.pin_count(&hash), 0);
769    }
770
771    #[tokio::test]
772    async fn test_pin_count_ref_counting() {
773        let store = MemoryStore::new();
774        let data = vec![1u8, 2, 3];
775        let hash = sha256(&data);
776
777        store.put(hash, data).await.unwrap();
778
779        // Pin multiple times
780        store.pin(&hash).await.unwrap();
781        store.pin(&hash).await.unwrap();
782        store.pin(&hash).await.unwrap();
783        assert_eq!(store.pin_count(&hash), 3);
784
785        // Unpin once
786        store.unpin(&hash).await.unwrap();
787        assert_eq!(store.pin_count(&hash), 2);
788        assert!(store.is_pinned(&hash));
789
790        // Unpin remaining
791        store.unpin(&hash).await.unwrap();
792        store.unpin(&hash).await.unwrap();
793        assert_eq!(store.pin_count(&hash), 0);
794        assert!(!store.is_pinned(&hash));
795
796        // Extra unpin shouldn't go negative
797        store.unpin(&hash).await.unwrap();
798        assert_eq!(store.pin_count(&hash), 0);
799    }
800
801    #[tokio::test]
802    async fn test_stats() {
803        let store = MemoryStore::new();
804
805        let data1 = vec![1u8, 2, 3]; // 3 bytes
806        let data2 = vec![4u8, 5]; // 2 bytes
807        let hash1 = sha256(&data1);
808        let hash2 = sha256(&data2);
809
810        store.put(hash1, data1).await.unwrap();
811        store.put(hash2, data2).await.unwrap();
812
813        // Pin one item
814        store.pin(&hash1).await.unwrap();
815
816        let stats = store.stats().await;
817        assert_eq!(stats.count, 2);
818        assert_eq!(stats.bytes, 5);
819        assert_eq!(stats.pinned_count, 1);
820        assert_eq!(stats.pinned_bytes, 3);
821    }
822
823    #[tokio::test]
824    async fn test_max_bytes() {
825        let store = MemoryStore::new();
826        assert!(store.max_bytes().is_none());
827
828        store.set_max_bytes(1000);
829        assert_eq!(store.max_bytes(), Some(1000));
830
831        // 0 means unlimited
832        store.set_max_bytes(0);
833        assert!(store.max_bytes().is_none());
834    }
835
836    #[tokio::test]
837    async fn test_with_max_bytes() {
838        let store = MemoryStore::with_max_bytes(500);
839        assert_eq!(store.max_bytes(), Some(500));
840
841        let store_unlimited = MemoryStore::with_max_bytes(0);
842        assert!(store_unlimited.max_bytes().is_none());
843    }
844
845    #[tokio::test]
846    async fn test_eviction_respects_pins() {
847        // Store with 10 byte limit
848        let store = MemoryStore::with_max_bytes(10);
849
850        // Insert 3 items: 3 + 3 + 3 = 9 bytes
851        let data1 = vec![1u8, 1, 1]; // oldest
852        let data2 = vec![2u8, 2, 2];
853        let data3 = vec![3u8, 3, 3]; // newest
854        let hash1 = sha256(&data1);
855        let hash2 = sha256(&data2);
856        let hash3 = sha256(&data3);
857
858        store.put(hash1, data1).await.unwrap();
859        store.put(hash2, data2).await.unwrap();
860        store.put(hash3, data3).await.unwrap();
861
862        // Pin the oldest item
863        store.pin(&hash1).await.unwrap();
864
865        // Add more data to exceed limit: 9 + 3 = 12 bytes > 10
866        let data4 = vec![4u8, 4, 4];
867        let hash4 = sha256(&data4);
868        store.put(hash4, data4).await.unwrap();
869
870        // Evict - should remove hash2 (oldest unpinned)
871        let freed = store.evict_if_needed().await.unwrap();
872        assert!(freed > 0);
873
874        // hash1 should still exist (pinned)
875        assert!(store.has(&hash1).await.unwrap());
876        // hash2 should be gone (oldest unpinned)
877        assert!(!store.has(&hash2).await.unwrap());
878        // hash3 and hash4 should exist
879        assert!(store.has(&hash3).await.unwrap());
880        assert!(store.has(&hash4).await.unwrap());
881    }
882
883    #[tokio::test]
884    async fn test_eviction_lru_order() {
885        // Store with 15 byte limit
886        let store = MemoryStore::with_max_bytes(15);
887
888        // Insert items in order (oldest first)
889        let data1 = vec![1u8; 5]; // oldest
890        let data2 = vec![2u8; 5];
891        let data3 = vec![3u8; 5];
892        let data4 = vec![4u8; 5]; // newest
893        let hash1 = sha256(&data1);
894        let hash2 = sha256(&data2);
895        let hash3 = sha256(&data3);
896        let hash4 = sha256(&data4);
897
898        store.put(hash1, data1).await.unwrap();
899        store.put(hash2, data2).await.unwrap();
900        store.put(hash3, data3).await.unwrap();
901        store.put(hash4, data4).await.unwrap();
902
903        // Now at 20 bytes, limit is 15
904        assert_eq!(store.total_bytes(), 20);
905
906        // Evict - should remove oldest items first
907        let freed = store.evict_if_needed().await.unwrap();
908        assert!(freed >= 5); // At least one item evicted
909
910        // Oldest should be gone
911        assert!(!store.has(&hash1).await.unwrap());
912        // Newest should still exist
913        assert!(store.has(&hash4).await.unwrap());
914    }
915
916    #[tokio::test]
917    async fn test_no_eviction_when_under_limit() {
918        let store = MemoryStore::with_max_bytes(100);
919
920        let data = vec![1u8, 2, 3];
921        let hash = sha256(&data);
922        store.put(hash, data).await.unwrap();
923
924        let freed = store.evict_if_needed().await.unwrap();
925        assert_eq!(freed, 0);
926        assert!(store.has(&hash).await.unwrap());
927    }
928
929    #[tokio::test]
930    async fn test_no_eviction_without_limit() {
931        let store = MemoryStore::new();
932
933        // Add lots of data
934        for i in 0..100u8 {
935            let data = vec![i; 100];
936            let hash = sha256(&data);
937            store.put(hash, data).await.unwrap();
938        }
939
940        let freed = store.evict_if_needed().await.unwrap();
941        assert_eq!(freed, 0);
942        assert_eq!(store.size(), 100);
943    }
944
945    #[tokio::test]
946    async fn test_delete_removes_pin() {
947        let store = MemoryStore::new();
948        let data = vec![1u8, 2, 3];
949        let hash = sha256(&data);
950
951        store.put(hash, data).await.unwrap();
952        store.pin(&hash).await.unwrap();
953        assert!(store.is_pinned(&hash));
954
955        store.delete(&hash).await.unwrap();
956        // Pin should be gone after delete
957        assert_eq!(store.pin_count(&hash), 0);
958    }
959}