Skip to main content

graphrefly_storage/
backend.rs

1//! Bytes-level kv backend (Phase 14.6 — DS-14-storage L1, M4.B 2026-05-10).
2//!
3//! One responsibility: read/write byte ranges under string keys. Tier
4//! specializations (`tier.rs`, `memory.rs`) layer typed serialization on top
5//! via [`Codec`](crate::codec::Codec).
6//!
7//! All operations are **sync** (D143 — pre-Q1 lock). Backends that need
8//! async I/O (network, `tokio::fs`) wrap their async surface in
9//! `tokio::Handle::block_on` at the call site, or expose a sync facade via
10//! `spawn_blocking`. The memory backend in this module is fully sync.
11
12use std::collections::HashMap;
13use std::sync::Arc;
14
15use parking_lot::Mutex;
16
17use crate::error::StorageError;
18
19/// Bytes-level kv backend. Tiers layer typed serialization on top via
20/// [`Codec`](crate::codec::Codec).
21pub trait StorageBackend: Send + Sync {
22    /// Diagnostic name (e.g. `"memory"`, `"file:./checkpoints"`). Surfaces
23    /// in error messages and tier `Display` impls.
24    fn name(&self) -> &str;
25
26    /// Read raw bytes; returns `Ok(None)` on miss.
27    fn read(&self, key: &str) -> Result<Option<Vec<u8>>, StorageError>;
28
29    /// Write raw bytes.
30    fn write(&self, key: &str, bytes: &[u8]) -> Result<(), StorageError>;
31
32    /// Optional delete-by-key. Default is no-op so append-only or read-only
33    /// backends can stay quiet.
34    fn delete(&self, key: &str) -> Result<(), StorageError> {
35        let _ = key;
36        Ok(())
37    }
38
39    /// Enumerate keys matching `prefix` (lex-ASC). Empty `prefix` enumerates
40    /// all keys. Default returns `BackendNoListSupport` — backends that don't
41    /// support enumeration surface the diagnostic here at first call, NOT at
42    /// attach (mirrors TS lazy-throw semantics for `list_by_prefix`).
43    fn list(&self, prefix: &str) -> Result<Vec<String>, StorageError> {
44        let _ = prefix;
45        Err(StorageError::BackendNoListSupport {
46            tier: self.name().to_string(),
47        })
48    }
49
50    /// Optional drain hook — adapter authors implement when buffering writes.
51    /// Default no-op; tier `flush()` does NOT cascade into this by default
52    /// (the tier owns its own buffer; backend buffering is a separate concern
53    /// the backend author opts into).
54    fn flush(&self) -> Result<(), StorageError> {
55        Ok(())
56    }
57}
58
59/// In-memory bytes backend backed by `parking_lot::Mutex<HashMap<String,
60/// Vec<u8>>>`. All operations are synchronous and in-process. Useful for
61/// tests, hot tiers, and as the default backend for the `memory_*`
62/// convenience factories.
63///
64/// `Default` uses `"memory"` as the diagnostic name; use [`Self::with_name`]
65/// to set a different one (the per-tier-name pattern from the TS impl —
66/// helps disambiguate multiple in-process tiers in diagnostics).
67#[derive(Debug)]
68pub struct MemoryBackend {
69    name: String,
70    data: Mutex<HashMap<String, Vec<u8>>>,
71}
72
73impl Default for MemoryBackend {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl MemoryBackend {
80    #[must_use]
81    pub fn new() -> Self {
82        Self {
83            name: "memory".into(),
84            data: Mutex::new(HashMap::new()),
85        }
86    }
87
88    #[must_use]
89    pub fn with_name(name: impl Into<String>) -> Self {
90        Self {
91            name: name.into(),
92            data: Mutex::new(HashMap::new()),
93        }
94    }
95
96    /// Diagnostic helper: number of keys currently stored.
97    #[must_use]
98    pub fn len(&self) -> usize {
99        self.data.lock().len()
100    }
101
102    /// Diagnostic helper: whether the backend holds any keys.
103    #[must_use]
104    pub fn is_empty(&self) -> bool {
105        self.data.lock().is_empty()
106    }
107}
108
109impl StorageBackend for MemoryBackend {
110    fn name(&self) -> &str {
111        &self.name
112    }
113
114    fn read(&self, key: &str) -> Result<Option<Vec<u8>>, StorageError> {
115        Ok(self.data.lock().get(key).cloned())
116    }
117
118    fn write(&self, key: &str, bytes: &[u8]) -> Result<(), StorageError> {
119        self.data.lock().insert(key.to_string(), bytes.to_vec());
120        Ok(())
121    }
122
123    fn delete(&self, key: &str) -> Result<(), StorageError> {
124        self.data.lock().remove(key);
125        Ok(())
126    }
127
128    fn list(&self, prefix: &str) -> Result<Vec<String>, StorageError> {
129        let guard = self.data.lock();
130        let mut keys: Vec<String> = if prefix.is_empty() {
131            guard.keys().cloned().collect()
132        } else {
133            guard
134                .keys()
135                .filter(|k| k.starts_with(prefix))
136                .cloned()
137                .collect()
138        };
139        keys.sort();
140        Ok(keys)
141    }
142}
143
144/// Convenience constructor returning an `Arc<MemoryBackend>`. Use this when
145/// sharing a single backend across multiple tiers (the paired
146/// `{ snapshot, wal }` pattern from DS-14-storage §a).
147#[must_use]
148pub fn memory_backend() -> Arc<MemoryBackend> {
149    Arc::new(MemoryBackend::new())
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn memory_backend_read_write_round_trip() {
158        let b = MemoryBackend::new();
159        assert!(b.is_empty());
160        b.write("k1", b"hello").unwrap();
161        assert_eq!(b.read("k1").unwrap(), Some(b"hello".to_vec()));
162        assert_eq!(b.len(), 1);
163    }
164
165    #[test]
166    fn memory_backend_read_miss_returns_none() {
167        let b = MemoryBackend::new();
168        assert!(b.read("nope").unwrap().is_none());
169    }
170
171    #[test]
172    fn memory_backend_delete_removes_key() {
173        let b = MemoryBackend::new();
174        b.write("k", b"v").unwrap();
175        b.delete("k").unwrap();
176        assert!(b.read("k").unwrap().is_none());
177    }
178
179    #[test]
180    fn memory_backend_list_lex_asc() {
181        let b = MemoryBackend::new();
182        b.write("g/10", b"a").unwrap();
183        b.write("g/02", b"b").unwrap();
184        b.write("g/01", b"c").unwrap();
185        b.write("other", b"d").unwrap();
186        let keys = b.list("g/").unwrap();
187        assert_eq!(keys, vec!["g/01", "g/02", "g/10"]);
188    }
189
190    #[test]
191    fn memory_backend_list_empty_prefix_returns_all() {
192        let b = MemoryBackend::new();
193        b.write("a", b"x").unwrap();
194        b.write("b", b"y").unwrap();
195        let keys = b.list("").unwrap();
196        assert_eq!(keys, vec!["a", "b"]);
197    }
198
199    #[test]
200    fn memory_backend_with_custom_name() {
201        let b = MemoryBackend::with_name("test");
202        assert_eq!(b.name(), "test");
203    }
204
205    #[test]
206    fn memory_backend_factory_returns_shared_arc() {
207        let b = memory_backend();
208        let b2 = Arc::clone(&b);
209        b.write("k", b"v").unwrap();
210        assert_eq!(b2.read("k").unwrap(), Some(b"v".to_vec()));
211    }
212
213    /// Verify that a non-list-supporting backend returns the expected error
214    /// shape. Constructed via a stub backend that doesn't override `list`.
215    #[test]
216    fn default_list_returns_backend_no_list_support() {
217        struct NoList;
218        impl StorageBackend for NoList {
219            fn name(&self) -> &'static str {
220                "no-list"
221            }
222            fn read(&self, _key: &str) -> Result<Option<Vec<u8>>, StorageError> {
223                Ok(None)
224            }
225            fn write(&self, _k: &str, _b: &[u8]) -> Result<(), StorageError> {
226                Ok(())
227            }
228        }
229        let b = NoList;
230        let r = b.list("g/");
231        assert!(matches!(r, Err(StorageError::BackendNoListSupport { .. })));
232    }
233}