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}