Skip to main content

plugmem_core/
storage.rs

1//! The [`Storage`] trait and the in-memory reference implementation
2//!
3//! The core never touches files or the network: every byte entering or
4//! leaving the engine goes through this trait. Native wrappers implement
5//! it over files (`plugmem-host::FileStorage`), the wasm bridge over host
6//! callbacks; [`MemStorage`] backs tests and ephemeral databases.
7
8use alloc::vec::Vec;
9
10/// Where snapshot and journal bytes live.
11///
12/// The engine calls `append_journal` synchronously at the end of every
13/// mutating operation — that call is the durability point. Fsync policy,
14/// atomicity of `write_snapshot` (tmp + rename) and locking are the
15/// implementation's business, not the core's.
16pub trait Storage {
17    /// Implementation-specific failure (I/O error, host callback error…).
18    type Error: core::fmt::Debug;
19
20    /// The full snapshot image, or `None` when no database exists yet
21    /// (first run).
22    fn read_snapshot(&mut self) -> Result<Option<Vec<u8>>, Self::Error>;
23
24    /// Atomically replaces the snapshot image.
25    fn write_snapshot(&mut self, bytes: &[u8]) -> Result<(), Self::Error>;
26
27    /// All journal bytes accumulated since the last snapshot (empty is
28    /// normal).
29    fn read_journal(&mut self) -> Result<Vec<u8>, Self::Error>;
30
31    /// Appends one framed journal entry (see [`crate::journal`]).
32    fn append_journal(&mut self, entry: &[u8]) -> Result<(), Self::Error>;
33
34    /// Discards the journal (called after a successful snapshot).
35    fn clear_journal(&mut self) -> Result<(), Self::Error>;
36}
37
38/// An append-only staging area: build it sequentially with
39/// [`write`](Scratch::write), then [`freeze`](Scratch::freeze) it into a
40/// readable byte view for random and sequential reads. Dropping it discards the
41/// storage.
42///
43/// It is the write-side sibling of [`Storage`]: `no_std` core owns an algorithm
44/// that needs to spill more bytes than fit in RAM and read them back, and the
45/// host injects the actual backing — typically a temp file that is memory-mapped
46/// on `freeze` (see `plugmem-host`'s `FileScratch`). The reference
47/// [`MemScratch`] backs tests with a plain in-RAM buffer.
48///
49/// # Contract
50///
51/// - [`write`](Scratch::write) appends; call it any number of times, in the
52///   order the bytes should land.
53/// - [`freeze`](Scratch::freeze) ends the build and returns **all** written
54///   bytes as one contiguous slice; the borrow keeps the region readable. No
55///   `write` after a `freeze`.
56/// - What `freeze` hands back must equal the concatenation of every `write`.
57///
58/// plugmem uses it for the disk-first `maintain`/`recover` rebuild, which streams
59/// the large pools (vectors, text) through a `Scratch` instead of holding them
60/// in RAM — but the trait itself knows nothing of that.
61pub trait Scratch {
62    /// Implementation-specific failure (I/O error, host callback error…).
63    type Error: core::fmt::Debug;
64
65    /// Appends `bytes` to the staging area. Not to be called after
66    /// [`freeze`](Scratch::freeze).
67    fn write(&mut self, bytes: &[u8]) -> Result<(), Self::Error>;
68
69    /// Bytes appended so far.
70    fn len(&self) -> u64;
71
72    /// Whether nothing has been written yet.
73    fn is_empty(&self) -> bool {
74        self.len() == 0
75    }
76
77    /// Ends the build and borrows everything written as one contiguous slice,
78    /// for random and sequential reads (a file backing typically maps itself
79    /// here). The borrow keeps the region alive, which is also what forbids a
80    /// later `write`.
81    fn freeze(&mut self) -> Result<&[u8], Self::Error>;
82}
83
84/// In-memory [`Scratch`]: a growable buffer that "freezes" to a borrow of
85/// itself. The reference implementation — it backs tests, and because it
86/// drives the streaming path with no files it is the vehicle for the
87/// `disk_first == in_memory` property test. It stages in RAM, so
88/// it does not *save* RAM; a host `Scratch` over a temp file is what bounds RAM
89/// in practice.
90#[derive(Debug, Default, Clone)]
91pub struct MemScratch {
92    buf: Vec<u8>,
93}
94
95impl MemScratch {
96    /// An empty staging buffer.
97    pub fn new() -> Self {
98        Self::default()
99    }
100}
101
102impl Scratch for MemScratch {
103    type Error = core::convert::Infallible;
104
105    fn write(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {
106        self.buf.extend_from_slice(bytes);
107        Ok(())
108    }
109
110    fn len(&self) -> u64 {
111        self.buf.len() as u64
112    }
113
114    fn freeze(&mut self) -> Result<&[u8], Self::Error> {
115        Ok(&self.buf)
116    }
117}
118
119/// In-memory [`Storage`]: two byte buffers, no failure modes.
120///
121/// The reference implementation for tests and for ephemeral databases
122/// (an agent's scratch memory that intentionally dies with the process).
123#[derive(Debug, Default, Clone, PartialEq, Eq)]
124pub struct MemStorage {
125    snapshot: Option<Vec<u8>>,
126    journal: Vec<u8>,
127}
128
129impl MemStorage {
130    /// A storage with no snapshot and an empty journal (first run).
131    pub fn new() -> Self {
132        Self::default()
133    }
134}
135
136impl Storage for MemStorage {
137    type Error = core::convert::Infallible;
138
139    fn read_snapshot(&mut self) -> Result<Option<Vec<u8>>, Self::Error> {
140        Ok(self.snapshot.clone())
141    }
142
143    fn write_snapshot(&mut self, bytes: &[u8]) -> Result<(), Self::Error> {
144        self.snapshot = Some(bytes.to_vec());
145        Ok(())
146    }
147
148    fn read_journal(&mut self) -> Result<Vec<u8>, Self::Error> {
149        Ok(self.journal.clone())
150    }
151
152    fn append_journal(&mut self, entry: &[u8]) -> Result<(), Self::Error> {
153        self.journal.extend_from_slice(entry);
154        Ok(())
155    }
156
157    fn clear_journal(&mut self) -> Result<(), Self::Error> {
158        self.journal.clear();
159        Ok(())
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn mem_scratch_builds_and_freezes_to_the_concatenation() {
169        let mut s = MemScratch::new();
170        assert!(s.is_empty());
171        s.write(b"hello ").unwrap();
172        s.write(b"world").unwrap();
173        assert_eq!(s.len(), 11);
174        assert!(!s.is_empty());
175        // freeze returns every written byte, in order, as one slice.
176        assert_eq!(s.freeze().unwrap(), b"hello world");
177        // random read into the frozen view.
178        assert_eq!(&s.freeze().unwrap()[6..], b"world");
179    }
180}