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