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/// Return a byte range from an in-memory blob.
10///
11/// `end_inclusive` follows HTTP/storage range conventions. Out-of-bounds ranges
12/// return an empty or clamped slice instead of an error.
13pub fn slice_blob_range(
14    data: &[u8],
15    start: u64,
16    end_inclusive: u64,
17) -> Result<Vec<u8>, StoreError> {
18    if data.is_empty() || end_inclusive < start {
19        return Ok(Vec::new());
20    }
21
22    let len = data.len() as u64;
23    if start >= len {
24        return Ok(Vec::new());
25    }
26
27    let actual_end = end_inclusive.min(len - 1);
28    let start = usize::try_from(start)
29        .map_err(|_| StoreError::Other("blob range start is too large".to_string()))?;
30    let end_exclusive = usize::try_from(actual_end.saturating_add(1))
31        .map_err(|_| StoreError::Other("blob range end is too large".to_string()))?;
32
33    Ok(data[start..end_exclusive].to_vec())
34}
35
36/// Storage statistics
37#[derive(Debug, Clone, Default)]
38pub struct StoreStats {
39    /// Number of items in store
40    pub count: u64,
41    /// Total bytes stored
42    pub bytes: u64,
43    /// Number of pinned items
44    pub pinned_count: u64,
45    /// Bytes used by pinned items
46    pub pinned_bytes: u64,
47}
48
49/// Result of a batch insert.
50#[derive(Debug, Clone, Default, PartialEq, Eq)]
51pub struct PutManyReport {
52    /// Number of candidate items passed by the caller, including duplicates.
53    pub total: usize,
54    /// Number of blobs newly inserted into the store.
55    pub inserted: usize,
56    /// Logical bytes newly inserted into the store.
57    pub inserted_bytes: u64,
58    /// Hashes that were newly inserted, in insertion order.
59    pub inserted_hashes: Vec<Hash>,
60}
61
62/// Content-addressed key-value store interface
63#[async_trait]
64pub trait Store: Send + Sync {
65    /// Store data by its hash
66    /// Returns true if newly stored, false if already existed
67    async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError>;
68
69    /// Store multiple blobs.
70    /// Returns the number of newly stored items.
71    async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
72        let mut inserted = 0usize;
73        for (hash, data) in items {
74            if self.put(hash, data).await? {
75                inserted += 1;
76            }
77        }
78        Ok(inserted)
79    }
80
81    /// Store a locally generated content-addressed batch without requiring an
82    /// implementation to reread bytes for hashes it already tracks.
83    ///
84    /// Callers must provide bytes matching every hash. The default keeps the
85    /// ordinary verified batch behavior; stores with an authenticated local
86    /// location catalog may override this to skip known hashes cheaply.
87    async fn put_many_optimistic(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
88        self.put_many(items).await
89    }
90
91    /// Flush writes retained by an overlay store.
92    ///
93    /// Ordinary stores write through and keep the default no-op. Buffered
94    /// stores override this so callers that build several independent index
95    /// projections can bound memory without exposing an incomplete root.
96    async fn flush_pending(&self) -> Result<usize, StoreError> {
97        Ok(0)
98    }
99
100    /// Flush at an intermediate memory boundary. Buffered stores may retain a
101    /// bounded amount between boundaries; callers must still explicitly flush
102    /// all pending writes before returning a committed root.
103    async fn flush_pending_if_needed(&self) -> Result<usize, StoreError> {
104        self.flush_pending().await
105    }
106
107    /// Retrieve data by hash
108    /// Returns data or None if not found
109    async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError>;
110
111    /// Retrieve a byte range from a blob by hash.
112    ///
113    /// `end_inclusive` is inclusive, matching HTTP range requests and the sync
114    /// filesystem/LMDB storage primitives. Implementations may override this to
115    /// avoid loading whole blobs.
116    async fn get_range(
117        &self,
118        hash: &Hash,
119        start: u64,
120        end_inclusive: u64,
121    ) -> Result<Option<Vec<u8>>, StoreError> {
122        let Some(data) = self.get(hash).await? else {
123            return Ok(None);
124        };
125        Ok(Some(slice_blob_range(&data, start, end_inclusive)?))
126    }
127
128    /// Return the stored blob size in bytes.
129    ///
130    /// The default implementation reads the blob. Backends with metadata should
131    /// override this to answer without loading payload bytes.
132    async fn blob_size(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
133        Ok(self.get(hash).await?.map(|data| data.len() as u64))
134    }
135
136    /// Check if hash exists
137    async fn has(&self, hash: &Hash) -> Result<bool, StoreError>;
138
139    /// Delete by hash
140    /// Returns true if deleted, false if didn't exist
141    async fn delete(&self, hash: &Hash) -> Result<bool, StoreError>;
142
143    /// Delete multiple hashes and return the number that existed.
144    ///
145    /// Persistent stores should override this to use one bounded transaction
146    /// rather than forcing one durability barrier per hash.
147    async fn delete_many(&self, hashes: Vec<Hash>) -> Result<usize, StoreError> {
148        let mut deleted = 0usize;
149        for hash in hashes {
150            if self.delete(&hash).await? {
151                deleted += 1;
152            }
153        }
154        Ok(deleted)
155    }
156
157    // ========================================================================
158    // Optional: Storage limits and eviction (default no-op implementations)
159    // ========================================================================
160
161    /// Set maximum storage size in bytes. 0 = unlimited.
162    fn set_max_bytes(&self, _max: u64) {}
163
164    /// Get maximum storage size. None = unlimited.
165    fn max_bytes(&self) -> Option<u64> {
166        None
167    }
168
169    /// Get storage statistics
170    async fn stats(&self) -> StoreStats {
171        StoreStats::default()
172    }
173
174    /// Evict unpinned items if over storage limit.
175    /// Returns number of bytes freed.
176    async fn evict_if_needed(&self) -> Result<u64, StoreError> {
177        Ok(0)
178    }
179
180    // ========================================================================
181    // Optional: Pinning (default no-op implementations)
182    // ========================================================================
183
184    /// Pin a hash (increment ref count). Pinned items are not evicted.
185    async fn pin(&self, _hash: &Hash) -> Result<(), StoreError> {
186        Ok(())
187    }
188
189    /// Unpin a hash (decrement ref count). Item can be evicted when count reaches 0.
190    async fn unpin(&self, _hash: &Hash) -> Result<(), StoreError> {
191        Ok(())
192    }
193
194    /// Get pin count for a hash. 0 = not pinned.
195    fn pin_count(&self, _hash: &Hash) -> u32 {
196        0
197    }
198
199    /// Check if hash is pinned (pin count > 0)
200    fn is_pinned(&self, hash: &Hash) -> bool {
201        self.pin_count(hash) > 0
202    }
203}
204
205/// Store error type
206#[derive(Debug, thiserror::Error)]
207pub enum StoreError {
208    #[error("IO error: {0}")]
209    Io(#[from] std::io::Error),
210    #[error("Store error: {0}")]
211    Other(String),
212}
213
214#[derive(Debug, Default)]
215struct BufferedStoreInner {
216    pending: HashMap<Hash, Vec<u8>>,
217    order: Vec<Hash>,
218    pending_bytes: usize,
219}
220
221#[derive(Debug, Clone, Copy)]
222struct BufferedStoreOptions {
223    check_base_on_put: bool,
224    flush_threshold_bytes: usize,
225}
226
227impl Default for BufferedStoreOptions {
228    fn default() -> Self {
229        Self {
230            check_base_on_put: true,
231            flush_threshold_bytes: 0,
232        }
233    }
234}
235
236/// Buffered overlay store that keeps writes in memory until flushed.
237#[derive(Debug, Clone)]
238pub struct BufferedStore<S: Store> {
239    base: Arc<S>,
240    inner: Arc<RwLock<BufferedStoreInner>>,
241    options: BufferedStoreOptions,
242}
243
244impl<S: Store> BufferedStore<S> {
245    pub fn new(base: Arc<S>) -> Self {
246        Self::with_options(base, BufferedStoreOptions::default())
247    }
248
249    pub fn new_optimistic(base: Arc<S>) -> Self {
250        Self::with_options(
251            base,
252            BufferedStoreOptions {
253                check_base_on_put: false,
254                flush_threshold_bytes: 0,
255            },
256        )
257    }
258
259    fn with_options(base: Arc<S>, options: BufferedStoreOptions) -> Self {
260        Self {
261            base,
262            inner: Arc::new(RwLock::new(BufferedStoreInner::default())),
263            options,
264        }
265    }
266
267    /// Coalesce intermediate flush hints below this payload threshold. This
268    /// bounds retention between hints, not within one caller operation; an
269    /// operation may exceed the threshold. `flush` and `flush_pending` always
270    /// flush everything, including below-threshold writes.
271    pub fn with_flush_threshold(mut self, bytes: usize) -> Self {
272        self.options.flush_threshold_bytes = bytes;
273        self
274    }
275
276    pub async fn flush(&self) -> Result<usize, StoreError> {
277        let items = {
278            let mut inner = self.inner.write().unwrap();
279            if inner.order.is_empty() {
280                return Ok(0);
281            }
282
283            let order = std::mem::take(&mut inner.order);
284            inner.pending_bytes = 0;
285            let mut items = Vec::with_capacity(order.len());
286            for hash in order {
287                if let Some(data) = inner.pending.remove(&hash) {
288                    items.push((hash, data));
289                }
290            }
291            items
292        };
293
294        if self.options.check_base_on_put {
295            self.base.put_many(items).await
296        } else {
297            self.base.put_many_optimistic(items).await
298        }
299    }
300}
301
302#[async_trait]
303impl<S: Store> Store for BufferedStore<S> {
304    async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError> {
305        {
306            let inner = self.inner.read().unwrap();
307            if inner.pending.contains_key(&hash) {
308                return Ok(false);
309            }
310        }
311
312        if self.options.check_base_on_put && self.base.has(&hash).await? {
313            return Ok(false);
314        }
315
316        let mut inner = self.inner.write().unwrap();
317        if inner.pending.contains_key(&hash) {
318            return Ok(false);
319        }
320        inner.order.push(hash);
321        inner.pending_bytes += data.len();
322        inner.pending.insert(hash, data);
323        Ok(true)
324    }
325
326    async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
327        let mut inserted = 0usize;
328        for (hash, data) in items {
329            if self.put(hash, data).await? {
330                inserted += 1;
331            }
332        }
333        Ok(inserted)
334    }
335
336    async fn flush_pending(&self) -> Result<usize, StoreError> {
337        BufferedStore::flush(self).await
338    }
339
340    async fn flush_pending_if_needed(&self) -> Result<usize, StoreError> {
341        if self.inner.read().unwrap().pending_bytes < self.options.flush_threshold_bytes {
342            return Ok(0);
343        }
344        self.flush().await
345    }
346
347    async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
348        if let Some(data) = self.inner.read().unwrap().pending.get(hash).cloned() {
349            return Ok(Some(data));
350        }
351        self.base.get(hash).await
352    }
353
354    async fn get_range(
355        &self,
356        hash: &Hash,
357        start: u64,
358        end_inclusive: u64,
359    ) -> Result<Option<Vec<u8>>, StoreError> {
360        let pending = {
361            let inner = self.inner.read().unwrap();
362            inner
363                .pending
364                .get(hash)
365                .map(|data| slice_blob_range(data, start, end_inclusive))
366                .transpose()?
367        };
368        if pending.is_some() {
369            return Ok(pending);
370        }
371        self.base.get_range(hash, start, end_inclusive).await
372    }
373
374    async fn blob_size(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
375        if let Some(size) = self
376            .inner
377            .read()
378            .unwrap()
379            .pending
380            .get(hash)
381            .map(|data| data.len() as u64)
382        {
383            return Ok(Some(size));
384        }
385        self.base.blob_size(hash).await
386    }
387
388    async fn has(&self, hash: &Hash) -> Result<bool, StoreError> {
389        if self.inner.read().unwrap().pending.contains_key(hash) {
390            return Ok(true);
391        }
392        self.base.has(hash).await
393    }
394
395    async fn delete(&self, hash: &Hash) -> Result<bool, StoreError> {
396        let removed = {
397            let mut inner = self.inner.write().unwrap();
398            let removed = inner.pending.remove(hash);
399            if let Some(bytes) = &removed {
400                inner.pending_bytes -= bytes.len();
401                inner.order.retain(|queued| queued != hash);
402            }
403            removed.is_some()
404        };
405
406        if removed {
407            return Ok(true);
408        }
409
410        self.base.delete(hash).await
411    }
412
413    async fn stats(&self) -> StoreStats {
414        let mut stats = self.base.stats().await;
415        let pending_bytes = self
416            .inner
417            .read()
418            .unwrap()
419            .pending
420            .values()
421            .map(|data| data.len() as u64)
422            .sum::<u64>();
423        stats.count += self.inner.read().unwrap().pending.len() as u64;
424        stats.bytes += pending_bytes;
425        stats
426    }
427
428    async fn evict_if_needed(&self) -> Result<u64, StoreError> {
429        self.base.evict_if_needed().await
430    }
431
432    async fn pin(&self, hash: &Hash) -> Result<(), StoreError> {
433        self.base.pin(hash).await
434    }
435
436    async fn unpin(&self, hash: &Hash) -> Result<(), StoreError> {
437        self.base.unpin(hash).await
438    }
439
440    fn pin_count(&self, hash: &Hash) -> u32 {
441        self.base.pin_count(hash)
442    }
443}
444
445/// Entry in the memory store with metadata for LRU
446#[derive(Debug, Clone)]
447struct MemoryEntry {
448    data: Vec<u8>,
449    /// Insertion order for LRU (lower = older)
450    order: u64,
451}
452
453/// Internal state for MemoryStore
454#[derive(Debug, Default)]
455struct MemoryStoreInner {
456    data: HashMap<String, MemoryEntry>,
457    pins: HashMap<String, u32>,
458    next_order: u64,
459    max_bytes: Option<u64>,
460}
461
462/// In-memory content-addressed store with LRU eviction and pinning
463#[derive(Debug, Clone, Default)]
464pub struct MemoryStore {
465    inner: Arc<RwLock<MemoryStoreInner>>,
466}
467
468impl MemoryStore {
469    pub fn new() -> Self {
470        Self {
471            inner: Arc::new(RwLock::new(MemoryStoreInner::default())),
472        }
473    }
474
475    /// Create a new store with a maximum size limit
476    pub fn with_max_bytes(max_bytes: u64) -> Self {
477        Self {
478            inner: Arc::new(RwLock::new(MemoryStoreInner {
479                max_bytes: if max_bytes > 0 { Some(max_bytes) } else { None },
480                ..Default::default()
481            })),
482        }
483    }
484
485    /// Get number of stored items
486    pub fn size(&self) -> usize {
487        self.inner.read().unwrap().data.len()
488    }
489
490    /// Get total bytes stored
491    pub fn total_bytes(&self) -> usize {
492        self.inner
493            .read()
494            .unwrap()
495            .data
496            .values()
497            .map(|e| e.data.len())
498            .sum()
499    }
500
501    /// Clear all data (but not pins)
502    pub fn clear(&self) {
503        self.inner.write().unwrap().data.clear();
504    }
505
506    /// List all hashes
507    pub fn keys(&self) -> Vec<Hash> {
508        self.inner
509            .read()
510            .unwrap()
511            .data
512            .keys()
513            .filter_map(|hex| {
514                let bytes = hex::decode(hex).ok()?;
515                if bytes.len() != 32 {
516                    return None;
517                }
518                let mut hash = [0u8; 32];
519                hash.copy_from_slice(&bytes);
520                Some(hash)
521            })
522            .collect()
523    }
524
525    /// Evict oldest unpinned entries until under target bytes
526    fn evict_to_target(&self, target_bytes: u64) -> u64 {
527        let mut inner = self.inner.write().unwrap();
528
529        let current_bytes: u64 = inner.data.values().map(|e| e.data.len() as u64).sum();
530        if current_bytes <= target_bytes {
531            return 0;
532        }
533
534        // Collect unpinned entries sorted by order (oldest first)
535        let mut unpinned: Vec<(String, u64, u64)> = inner
536            .data
537            .iter()
538            .filter(|(key, _)| inner.pins.get(*key).copied().unwrap_or(0) == 0)
539            .map(|(key, entry)| (key.clone(), entry.order, entry.data.len() as u64))
540            .collect();
541
542        unpinned.sort_by_key(|(_, order, _)| *order);
543
544        let mut freed = 0u64;
545        let to_free = current_bytes - target_bytes;
546
547        for (key, _, size) in unpinned {
548            if freed >= to_free {
549                break;
550            }
551            inner.data.remove(&key);
552            freed += size;
553        }
554
555        freed
556    }
557}
558
559#[async_trait]
560impl Store for MemoryStore {
561    async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError> {
562        let key = to_hex(&hash);
563        let mut inner = self.inner.write().unwrap();
564        if inner.data.contains_key(&key) {
565            return Ok(false);
566        }
567        let order = inner.next_order;
568        inner.next_order += 1;
569        inner.data.insert(key, MemoryEntry { data, order });
570        Ok(true)
571    }
572
573    async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
574        let mut inserted = 0usize;
575        let mut inner = self.inner.write().unwrap();
576        for (hash, data) in items {
577            let key = to_hex(&hash);
578            if inner.data.contains_key(&key) {
579                continue;
580            }
581            let order = inner.next_order;
582            inner.next_order += 1;
583            inner.data.insert(key, MemoryEntry { data, order });
584            inserted += 1;
585        }
586        Ok(inserted)
587    }
588
589    async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
590        let key = to_hex(hash);
591        let inner = self.inner.read().unwrap();
592        Ok(inner.data.get(&key).map(|e| e.data.clone()))
593    }
594
595    async fn get_range(
596        &self,
597        hash: &Hash,
598        start: u64,
599        end_inclusive: u64,
600    ) -> Result<Option<Vec<u8>>, StoreError> {
601        let key = to_hex(hash);
602        let inner = self.inner.read().unwrap();
603        inner
604            .data
605            .get(&key)
606            .map(|entry| slice_blob_range(&entry.data, start, end_inclusive))
607            .transpose()
608    }
609
610    async fn blob_size(&self, hash: &Hash) -> Result<Option<u64>, StoreError> {
611        let key = to_hex(hash);
612        let inner = self.inner.read().unwrap();
613        Ok(inner.data.get(&key).map(|entry| entry.data.len() as u64))
614    }
615
616    async fn has(&self, hash: &Hash) -> Result<bool, StoreError> {
617        let key = to_hex(hash);
618        Ok(self.inner.read().unwrap().data.contains_key(&key))
619    }
620
621    async fn delete(&self, hash: &Hash) -> Result<bool, StoreError> {
622        let key = to_hex(hash);
623        let mut inner = self.inner.write().unwrap();
624        // Also remove pin entry if exists
625        inner.pins.remove(&key);
626        Ok(inner.data.remove(&key).is_some())
627    }
628
629    fn set_max_bytes(&self, max: u64) {
630        self.inner.write().unwrap().max_bytes = if max > 0 { Some(max) } else { None };
631    }
632
633    fn max_bytes(&self) -> Option<u64> {
634        self.inner.read().unwrap().max_bytes
635    }
636
637    async fn stats(&self) -> StoreStats {
638        let inner = self.inner.read().unwrap();
639        let mut count = 0u64;
640        let mut bytes = 0u64;
641        let mut pinned_count = 0u64;
642        let mut pinned_bytes = 0u64;
643
644        for (key, entry) in &inner.data {
645            count += 1;
646            bytes += entry.data.len() as u64;
647            if inner.pins.get(key).copied().unwrap_or(0) > 0 {
648                pinned_count += 1;
649                pinned_bytes += entry.data.len() as u64;
650            }
651        }
652
653        StoreStats {
654            count,
655            bytes,
656            pinned_count,
657            pinned_bytes,
658        }
659    }
660
661    async fn evict_if_needed(&self) -> Result<u64, StoreError> {
662        let max = match self.inner.read().unwrap().max_bytes {
663            Some(m) => m,
664            None => return Ok(0), // No limit set
665        };
666
667        let current: u64 = self
668            .inner
669            .read()
670            .unwrap()
671            .data
672            .values()
673            .map(|e| e.data.len() as u64)
674            .sum();
675
676        if current <= max {
677            return Ok(0);
678        }
679
680        // Evict to 90% of max to avoid frequent evictions
681        let target = max * 9 / 10;
682        Ok(self.evict_to_target(target))
683    }
684
685    async fn pin(&self, hash: &Hash) -> Result<(), StoreError> {
686        let key = to_hex(hash);
687        let mut inner = self.inner.write().unwrap();
688        *inner.pins.entry(key).or_insert(0) += 1;
689        Ok(())
690    }
691
692    async fn unpin(&self, hash: &Hash) -> Result<(), StoreError> {
693        let key = to_hex(hash);
694        let mut inner = self.inner.write().unwrap();
695        if let Some(count) = inner.pins.get_mut(&key) {
696            if *count > 0 {
697                *count -= 1;
698            }
699            if *count == 0 {
700                inner.pins.remove(&key);
701            }
702        }
703        Ok(())
704    }
705
706    fn pin_count(&self, hash: &Hash) -> u32 {
707        let key = to_hex(hash);
708        self.inner
709            .read()
710            .unwrap()
711            .pins
712            .get(&key)
713            .copied()
714            .unwrap_or(0)
715    }
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721    use crate::hash::sha256;
722    use std::sync::atomic::{AtomicUsize, Ordering};
723
724    #[derive(Default)]
725    struct OptimisticBatchStore {
726        inner: MemoryStore,
727        regular_batches: AtomicUsize,
728        optimistic_batches: AtomicUsize,
729    }
730
731    #[async_trait]
732    impl Store for OptimisticBatchStore {
733        async fn put(&self, hash: Hash, data: Vec<u8>) -> Result<bool, StoreError> {
734            self.inner.put(hash, data).await
735        }
736
737        async fn put_many(&self, items: Vec<(Hash, Vec<u8>)>) -> Result<usize, StoreError> {
738            self.regular_batches.fetch_add(1, Ordering::Relaxed);
739            self.inner.put_many(items).await
740        }
741
742        async fn put_many_optimistic(
743            &self,
744            items: Vec<(Hash, Vec<u8>)>,
745        ) -> Result<usize, StoreError> {
746            self.optimistic_batches.fetch_add(1, Ordering::Relaxed);
747            self.inner.put_many(items).await
748        }
749
750        async fn get(&self, hash: &Hash) -> Result<Option<Vec<u8>>, StoreError> {
751            self.inner.get(hash).await
752        }
753
754        async fn has(&self, hash: &Hash) -> Result<bool, StoreError> {
755            self.inner.has(hash).await
756        }
757
758        async fn delete(&self, hash: &Hash) -> Result<bool, StoreError> {
759            self.inner.delete(hash).await
760        }
761    }
762
763    #[tokio::test]
764    async fn test_put_returns_true_for_new() {
765        let store = MemoryStore::new();
766        let data = vec![1u8, 2, 3];
767        let hash = sha256(&data);
768
769        let result = store.put(hash, data).await.unwrap();
770        assert!(result);
771    }
772
773    #[tokio::test]
774    async fn test_put_returns_false_for_duplicate() {
775        let store = MemoryStore::new();
776        let data = vec![1u8, 2, 3];
777        let hash = sha256(&data);
778
779        store.put(hash, data.clone()).await.unwrap();
780        let result = store.put(hash, data).await.unwrap();
781        assert!(!result);
782    }
783
784    #[tokio::test]
785    async fn test_put_many_counts_only_new_items() {
786        let store = MemoryStore::new();
787        let data1 = vec![1u8, 2, 3];
788        let data2 = vec![4u8, 5, 6];
789        let hash1 = sha256(&data1);
790        let hash2 = sha256(&data2);
791
792        store.put(hash1, data1.clone()).await.unwrap();
793        let inserted = store
794            .put_many(vec![(hash1, data1), (hash2, data2.clone())])
795            .await
796            .unwrap();
797
798        assert_eq!(inserted, 1);
799        assert_eq!(store.get(&hash2).await.unwrap(), Some(data2));
800    }
801
802    #[tokio::test]
803    async fn test_buffered_store_flushes_pending_writes() {
804        let base = std::sync::Arc::new(MemoryStore::new());
805        let buffered = BufferedStore::new(std::sync::Arc::clone(&base));
806        let data = vec![9u8, 8, 7];
807        let hash = sha256(&data);
808
809        assert!(buffered.put(hash, data.clone()).await.unwrap());
810        assert_eq!(buffered.get(&hash).await.unwrap(), Some(data.clone()));
811        assert_eq!(base.get(&hash).await.unwrap(), None);
812
813        let flushed = buffered.flush().await.unwrap();
814
815        assert_eq!(flushed, 1);
816        assert_eq!(base.get(&hash).await.unwrap(), Some(data));
817    }
818
819    #[tokio::test]
820    async fn test_optimistic_buffered_store_avoids_base_probe_but_preserves_contents() {
821        let base = std::sync::Arc::new(MemoryStore::new());
822        let buffered = BufferedStore::new_optimistic(std::sync::Arc::clone(&base));
823        let data = vec![4u8, 5, 6];
824        let hash = sha256(&data);
825
826        base.put(hash, data.clone()).await.unwrap();
827
828        assert!(buffered.put(hash, data.clone()).await.unwrap());
829        assert_eq!(buffered.get(&hash).await.unwrap(), Some(data.clone()));
830
831        let flushed = buffered.flush().await.unwrap();
832
833        assert_eq!(flushed, 0);
834        assert_eq!(base.get(&hash).await.unwrap(), Some(data));
835    }
836
837    #[tokio::test]
838    async fn test_optimistic_buffered_store_uses_idempotent_batch_flush() {
839        let base = std::sync::Arc::new(OptimisticBatchStore::default());
840        let buffered = BufferedStore::new_optimistic(std::sync::Arc::clone(&base));
841        let data = vec![7u8; 1024];
842        let hash = sha256(&data);
843
844        base.inner.put(hash, data.clone()).await.unwrap();
845        assert!(buffered.put(hash, data).await.unwrap());
846        assert_eq!(buffered.flush().await.unwrap(), 0);
847
848        assert_eq!(base.regular_batches.load(Ordering::Relaxed), 0);
849        assert_eq!(base.optimistic_batches.load(Ordering::Relaxed), 1);
850    }
851
852    #[tokio::test]
853    async fn threshold_coalesces_hints_but_explicit_flush_never_defers_writes() {
854        let base = Arc::new(OptimisticBatchStore::default());
855        let buffered = BufferedStore::new_optimistic(base.clone()).with_flush_threshold(8);
856        let first = vec![1; 4];
857        let second = vec![2; 4];
858        let h1 = sha256(&first);
859        let h2 = sha256(&second);
860        buffered.put(h1, first.clone()).await.unwrap();
861        assert!(!buffered.put(h1, first.clone()).await.unwrap());
862        assert_eq!(buffered.flush_pending_if_needed().await.unwrap(), 0);
863        assert_eq!(buffered.get(&h1).await.unwrap(), Some(first));
864        assert!(!base.has(&h1).await.unwrap());
865        buffered.put(h2, second.clone()).await.unwrap();
866        buffered.delete(&h1).await.unwrap();
867        assert_eq!(buffered.flush_pending_if_needed().await.unwrap(), 0);
868        assert_eq!(buffered.inner.read().unwrap().pending_bytes, 4);
869        buffered.put(h1, vec![1; 4]).await.unwrap();
870        assert_eq!(buffered.flush_pending_if_needed().await.unwrap(), 2);
871        assert_eq!(buffered.inner.read().unwrap().pending_bytes, 0);
872        assert_eq!(base.optimistic_batches.load(Ordering::Relaxed), 1);
873        assert_eq!(base.get(&h2).await.unwrap(), Some(second));
874
875        let tiny = vec![3];
876        let h3 = sha256(&tiny);
877        buffered.put(h3, tiny.clone()).await.unwrap();
878        assert_eq!(buffered.flush_pending().await.unwrap(), 1);
879        assert_eq!(base.get(&h3).await.unwrap(), Some(tiny));
880        assert_eq!(base.regular_batches.load(Ordering::Relaxed), 0);
881    }
882
883    #[tokio::test]
884    async fn threshold_does_not_leave_large_operations_pending_after_a_hint() {
885        let base = Arc::new(MemoryStore::new());
886        let buffered = BufferedStore::new(base.clone()).with_flush_threshold(8);
887        let bytes = vec![9; 16];
888        let hash = sha256(&bytes);
889        buffered.put(hash, bytes.clone()).await.unwrap();
890        assert_eq!(buffered.flush_pending_if_needed().await.unwrap(), 1);
891        assert_eq!(base.get(&hash).await.unwrap(), Some(bytes));
892        assert_eq!(buffered.inner.read().unwrap().pending_bytes, 0);
893
894        let ordinary = BufferedStore::new(base.clone());
895        let hash = sha256(&[5]);
896        ordinary.put(hash, vec![5]).await.unwrap();
897        assert_eq!(ordinary.flush_pending_if_needed().await.unwrap(), 1);
898        assert!(
899            base.has(&hash).await.unwrap(),
900            "default hints remain unconditional"
901        );
902    }
903
904    #[tokio::test]
905    async fn test_get_returns_data() {
906        let store = MemoryStore::new();
907        let data = vec![1u8, 2, 3];
908        let hash = sha256(&data);
909
910        store.put(hash, data.clone()).await.unwrap();
911        let result = store.get(&hash).await.unwrap();
912
913        assert_eq!(result, Some(data));
914    }
915
916    #[tokio::test]
917    async fn test_get_returns_none_for_missing() {
918        let store = MemoryStore::new();
919        let hash = [0u8; 32];
920
921        let result = store.get(&hash).await.unwrap();
922        assert!(result.is_none());
923    }
924
925    #[tokio::test]
926    async fn test_has_returns_true() {
927        let store = MemoryStore::new();
928        let data = vec![1u8, 2, 3];
929        let hash = sha256(&data);
930
931        store.put(hash, data).await.unwrap();
932        assert!(store.has(&hash).await.unwrap());
933    }
934
935    #[tokio::test]
936    async fn test_has_returns_false() {
937        let store = MemoryStore::new();
938        let hash = [0u8; 32];
939
940        assert!(!store.has(&hash).await.unwrap());
941    }
942
943    #[tokio::test]
944    async fn test_delete_returns_true() {
945        let store = MemoryStore::new();
946        let data = vec![1u8, 2, 3];
947        let hash = sha256(&data);
948
949        store.put(hash, data).await.unwrap();
950        let result = store.delete(&hash).await.unwrap();
951
952        assert!(result);
953        assert!(!store.has(&hash).await.unwrap());
954    }
955
956    #[tokio::test]
957    async fn test_delete_returns_false() {
958        let store = MemoryStore::new();
959        let hash = [0u8; 32];
960
961        let result = store.delete(&hash).await.unwrap();
962        assert!(!result);
963    }
964
965    #[tokio::test]
966    async fn test_size() {
967        let store = MemoryStore::new();
968        assert_eq!(store.size(), 0);
969
970        let data1 = vec![1u8];
971        let data2 = vec![2u8];
972        let hash1 = sha256(&data1);
973        let hash2 = sha256(&data2);
974
975        store.put(hash1, data1).await.unwrap();
976        store.put(hash2, data2).await.unwrap();
977
978        assert_eq!(store.size(), 2);
979    }
980
981    #[tokio::test]
982    async fn test_total_bytes() {
983        let store = MemoryStore::new();
984        assert_eq!(store.total_bytes(), 0);
985
986        let data1 = vec![1u8, 2, 3];
987        let data2 = vec![4u8, 5];
988        let hash1 = sha256(&data1);
989        let hash2 = sha256(&data2);
990
991        store.put(hash1, data1).await.unwrap();
992        store.put(hash2, data2).await.unwrap();
993
994        assert_eq!(store.total_bytes(), 5);
995    }
996
997    #[tokio::test]
998    async fn test_clear() {
999        let store = MemoryStore::new();
1000        let data = vec![1u8, 2, 3];
1001        let hash = sha256(&data);
1002
1003        store.put(hash, data).await.unwrap();
1004        store.clear();
1005
1006        assert_eq!(store.size(), 0);
1007        assert!(!store.has(&hash).await.unwrap());
1008    }
1009
1010    #[tokio::test]
1011    async fn test_keys() {
1012        let store = MemoryStore::new();
1013        assert!(store.keys().is_empty());
1014
1015        let data1 = vec![1u8];
1016        let data2 = vec![2u8];
1017        let hash1 = sha256(&data1);
1018        let hash2 = sha256(&data2);
1019
1020        store.put(hash1, data1).await.unwrap();
1021        store.put(hash2, data2).await.unwrap();
1022
1023        let keys = store.keys();
1024        assert_eq!(keys.len(), 2);
1025
1026        let mut hex_keys: Vec<_> = keys.iter().map(to_hex).collect();
1027        hex_keys.sort();
1028        let mut expected: Vec<_> = vec![to_hex(&hash1), to_hex(&hash2)];
1029        expected.sort();
1030        assert_eq!(hex_keys, expected);
1031    }
1032
1033    #[tokio::test]
1034    async fn test_pin_and_unpin() {
1035        let store = MemoryStore::new();
1036        let data = vec![1u8, 2, 3];
1037        let hash = sha256(&data);
1038
1039        store.put(hash, data).await.unwrap();
1040
1041        // Initially not pinned
1042        assert!(!store.is_pinned(&hash));
1043        assert_eq!(store.pin_count(&hash), 0);
1044
1045        // Pin
1046        store.pin(&hash).await.unwrap();
1047        assert!(store.is_pinned(&hash));
1048        assert_eq!(store.pin_count(&hash), 1);
1049
1050        // Unpin
1051        store.unpin(&hash).await.unwrap();
1052        assert!(!store.is_pinned(&hash));
1053        assert_eq!(store.pin_count(&hash), 0);
1054    }
1055
1056    #[tokio::test]
1057    async fn test_pin_count_ref_counting() {
1058        let store = MemoryStore::new();
1059        let data = vec![1u8, 2, 3];
1060        let hash = sha256(&data);
1061
1062        store.put(hash, data).await.unwrap();
1063
1064        // Pin multiple times
1065        store.pin(&hash).await.unwrap();
1066        store.pin(&hash).await.unwrap();
1067        store.pin(&hash).await.unwrap();
1068        assert_eq!(store.pin_count(&hash), 3);
1069
1070        // Unpin once
1071        store.unpin(&hash).await.unwrap();
1072        assert_eq!(store.pin_count(&hash), 2);
1073        assert!(store.is_pinned(&hash));
1074
1075        // Unpin remaining
1076        store.unpin(&hash).await.unwrap();
1077        store.unpin(&hash).await.unwrap();
1078        assert_eq!(store.pin_count(&hash), 0);
1079        assert!(!store.is_pinned(&hash));
1080
1081        // Extra unpin shouldn't go negative
1082        store.unpin(&hash).await.unwrap();
1083        assert_eq!(store.pin_count(&hash), 0);
1084    }
1085
1086    #[tokio::test]
1087    async fn test_stats() {
1088        let store = MemoryStore::new();
1089
1090        let data1 = vec![1u8, 2, 3]; // 3 bytes
1091        let data2 = vec![4u8, 5]; // 2 bytes
1092        let hash1 = sha256(&data1);
1093        let hash2 = sha256(&data2);
1094
1095        store.put(hash1, data1).await.unwrap();
1096        store.put(hash2, data2).await.unwrap();
1097
1098        // Pin one item
1099        store.pin(&hash1).await.unwrap();
1100
1101        let stats = store.stats().await;
1102        assert_eq!(stats.count, 2);
1103        assert_eq!(stats.bytes, 5);
1104        assert_eq!(stats.pinned_count, 1);
1105        assert_eq!(stats.pinned_bytes, 3);
1106    }
1107
1108    #[tokio::test]
1109    async fn test_max_bytes() {
1110        let store = MemoryStore::new();
1111        assert!(store.max_bytes().is_none());
1112
1113        store.set_max_bytes(1000);
1114        assert_eq!(store.max_bytes(), Some(1000));
1115
1116        // 0 means unlimited
1117        store.set_max_bytes(0);
1118        assert!(store.max_bytes().is_none());
1119    }
1120
1121    #[tokio::test]
1122    async fn test_with_max_bytes() {
1123        let store = MemoryStore::with_max_bytes(500);
1124        assert_eq!(store.max_bytes(), Some(500));
1125
1126        let store_unlimited = MemoryStore::with_max_bytes(0);
1127        assert!(store_unlimited.max_bytes().is_none());
1128    }
1129
1130    #[tokio::test]
1131    async fn test_eviction_respects_pins() {
1132        // Store with 10 byte limit
1133        let store = MemoryStore::with_max_bytes(10);
1134
1135        // Insert 3 items: 3 + 3 + 3 = 9 bytes
1136        let data1 = vec![1u8, 1, 1]; // oldest
1137        let data2 = vec![2u8, 2, 2];
1138        let data3 = vec![3u8, 3, 3]; // newest
1139        let hash1 = sha256(&data1);
1140        let hash2 = sha256(&data2);
1141        let hash3 = sha256(&data3);
1142
1143        store.put(hash1, data1).await.unwrap();
1144        store.put(hash2, data2).await.unwrap();
1145        store.put(hash3, data3).await.unwrap();
1146
1147        // Pin the oldest item
1148        store.pin(&hash1).await.unwrap();
1149
1150        // Add more data to exceed limit: 9 + 3 = 12 bytes > 10
1151        let data4 = vec![4u8, 4, 4];
1152        let hash4 = sha256(&data4);
1153        store.put(hash4, data4).await.unwrap();
1154
1155        // Evict - should remove hash2 (oldest unpinned)
1156        let freed = store.evict_if_needed().await.unwrap();
1157        assert!(freed > 0);
1158
1159        // hash1 should still exist (pinned)
1160        assert!(store.has(&hash1).await.unwrap());
1161        // hash2 should be gone (oldest unpinned)
1162        assert!(!store.has(&hash2).await.unwrap());
1163        // hash3 and hash4 should exist
1164        assert!(store.has(&hash3).await.unwrap());
1165        assert!(store.has(&hash4).await.unwrap());
1166    }
1167
1168    #[tokio::test]
1169    async fn test_eviction_lru_order() {
1170        // Store with 15 byte limit
1171        let store = MemoryStore::with_max_bytes(15);
1172
1173        // Insert items in order (oldest first)
1174        let data1 = vec![1u8; 5]; // oldest
1175        let data2 = vec![2u8; 5];
1176        let data3 = vec![3u8; 5];
1177        let data4 = vec![4u8; 5]; // newest
1178        let hash1 = sha256(&data1);
1179        let hash2 = sha256(&data2);
1180        let hash3 = sha256(&data3);
1181        let hash4 = sha256(&data4);
1182
1183        store.put(hash1, data1).await.unwrap();
1184        store.put(hash2, data2).await.unwrap();
1185        store.put(hash3, data3).await.unwrap();
1186        store.put(hash4, data4).await.unwrap();
1187
1188        // Now at 20 bytes, limit is 15
1189        assert_eq!(store.total_bytes(), 20);
1190
1191        // Evict - should remove oldest items first
1192        let freed = store.evict_if_needed().await.unwrap();
1193        assert!(freed >= 5); // At least one item evicted
1194
1195        // Oldest should be gone
1196        assert!(!store.has(&hash1).await.unwrap());
1197        // Newest should still exist
1198        assert!(store.has(&hash4).await.unwrap());
1199    }
1200
1201    #[tokio::test]
1202    async fn test_no_eviction_when_under_limit() {
1203        let store = MemoryStore::with_max_bytes(100);
1204
1205        let data = vec![1u8, 2, 3];
1206        let hash = sha256(&data);
1207        store.put(hash, data).await.unwrap();
1208
1209        let freed = store.evict_if_needed().await.unwrap();
1210        assert_eq!(freed, 0);
1211        assert!(store.has(&hash).await.unwrap());
1212    }
1213
1214    #[tokio::test]
1215    async fn test_no_eviction_without_limit() {
1216        let store = MemoryStore::new();
1217
1218        // Add lots of data
1219        for i in 0..100u8 {
1220            let data = vec![i; 100];
1221            let hash = sha256(&data);
1222            store.put(hash, data).await.unwrap();
1223        }
1224
1225        let freed = store.evict_if_needed().await.unwrap();
1226        assert_eq!(freed, 0);
1227        assert_eq!(store.size(), 100);
1228    }
1229
1230    #[tokio::test]
1231    async fn test_delete_removes_pin() {
1232        let store = MemoryStore::new();
1233        let data = vec![1u8, 2, 3];
1234        let hash = sha256(&data);
1235
1236        store.put(hash, data).await.unwrap();
1237        store.pin(&hash).await.unwrap();
1238        assert!(store.is_pinned(&hash));
1239
1240        store.delete(&hash).await.unwrap();
1241        // Pin should be gone after delete
1242        assert_eq!(store.pin_count(&hash), 0);
1243    }
1244}