Skip to main content

rig_memory/
store.rs

1//! Where a session is kept: [`Files`] by name, and a [`Store`] that also
2//! holds each log of the session. The blobs its messages name are files
3//! under `blobs/`, named by their content. [`AsyncStore`] is the same
4//! storage reached asynchronously, such as a database, an object store or
5//! IndexedDB; every [`Store`] is one. A log write names the length the
6//! writer read, and the store writes only if the log is still that long,
7//! checking and writing in one step ([`LogWriteError::Changed`]).
8
9use std::collections::BTreeMap;
10use std::future::{Future, ready};
11use std::io;
12use std::pin::pin;
13use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
14use std::task::{Context, Poll, Waker};
15
16use rig_core::wasm_compat::WasmCompatSend;
17
18use crate::SessionError;
19
20/// Why a log write wrote nothing.
21#[derive(Debug, thiserror::Error)]
22pub enum LogWriteError {
23    /// The log is not the `len` bytes the writer read: another writer
24    /// changed it. Nothing was written.
25    #[error("the log changed since it was read")]
26    Changed,
27    /// The store failed.
28    #[error(transparent)]
29    Io(#[from] io::Error),
30}
31
32impl From<LogWriteError> for io::Error {
33    /// The store's own error back; [`LogWriteError::Changed`] as an
34    /// [`io::ErrorKind::Other`] saying so.
35    fn from(error: LogWriteError) -> Self {
36        match error {
37            LogWriteError::Changed => io::Error::other(LogWriteError::Changed.to_string()),
38            LogWriteError::Io(error) => error,
39        }
40    }
41}
42
43/// A place that keeps files by name, such as a session's plugin data or
44/// effect log. A name is relative and `/`-separated ([`check_name`]).
45/// Every method takes `&self`, so one value is shared, between threads on
46/// every target: a host keeps it as `Arc<dyn Files>` or `Arc<dyn Store>`
47/// in state its systems share, such as a Bevy resource, which is `Send`
48/// and `Sync` on wasm too; `Arc<dyn Store>` and `Box<dyn Store>` are
49/// themselves a [`Store`], so an [`AsyncStore`] too. A store that is not
50/// `Send` and `Sync`, such as one over a JavaScript promise, is an
51/// [`AsyncStore`].
52pub trait Files: Send + Sync + 'static {
53    /// Replaces the file `name` with `bytes`, atomically: a reader sees the
54    /// old content or the new.
55    fn write_file(&self, name: &str, bytes: &[u8]) -> io::Result<()>;
56
57    /// Adds `bytes` at the end of the file `name`, which it creates when
58    /// there is none.
59    fn append_file(&self, name: &str, bytes: &[u8]) -> io::Result<()>;
60
61    /// The file `name`; [`io::ErrorKind::NotFound`] when there is none.
62    fn read_file(&self, name: &str) -> io::Result<Vec<u8>>;
63
64    /// Removes the file `name`; removing one that is not there succeeds.
65    fn remove_file(&self, name: &str) -> io::Result<()>;
66
67    /// The names of the files, in no particular order.
68    fn files(&self) -> io::Result<Vec<String>>;
69}
70
71/// Where a session is kept: its [`Files`], with the content-addressed
72/// blobs its messages name under `blobs/`, and a log per agent plus the
73/// session's own, appended to and read whole. Log names are one part of a
74/// name ([`check_name`] without `/`).
75pub trait Store: Files {
76    /// The names of the logs, in no particular order.
77    fn logs(&self) -> io::Result<Vec<String>>;
78
79    /// The whole log `name`; [`io::ErrorKind::NotFound`] when there is none.
80    fn read_log(&self, name: &str) -> io::Result<Vec<u8>>;
81
82    /// Appends `bytes` to the log `name` if it is `len` bytes long now (0
83    /// when there is none, which it then starts); else
84    /// [`LogWriteError::Changed`], writing nothing. The check and the
85    /// append are one step for every writer of the store, other processes
86    /// included, so of two writers that read the log at the same length
87    /// one appends and the other is refused. A log that shrank and grew
88    /// back to exactly `len` bytes since it was read is not told apart;
89    /// rig shrinks a log only by setting a torn last line aside, or by
90    /// [`rewrite`](crate::rewrite) and [`Self::destroy`], which are for a
91    /// stopped session.
92    fn append_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError>;
93
94    /// Replaces the log `name` with `bytes`, under the condition
95    /// [`Self::append_log`] has, atomically and durably: a reader, and a
96    /// crash or power loss, leave the old log or the new, never a part of
97    /// either or an empty file; an append after it goes after the new
98    /// bytes. Starts the log when there is none.
99    fn replace_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError>;
100
101    /// Removes everything the store holds: logs and files.
102    /// Idempotent: calling it again after a crash midway finishes the job.
103    fn destroy(&self) -> io::Result<()>;
104}
105
106/// A session's storage reached asynchronously: the part of [`Store`] that
107/// [`load_async`](crate::load_async) and [`save_async`](crate::save_async)
108/// use, for a store behind a network or a JavaScript promise. Every
109/// [`Store`] is one, its calls done when made: a local store pays no thread
110/// hop and no allocation for it. A type that implements [`Store`] gets this
111/// from it and cannot have its own (the two impls would overlap): wrap it
112/// in a type of your own instead.
113///
114/// Each method keeps [`Store`]'s contract. A name is checked by
115/// [`check_name`] (a log name is one part) and may come from a log's
116/// content, so a store refuses one that does not pass. The two log writes
117/// check the log's length and write in one step, such as a conditional
118/// `UPDATE … WHERE octet_length(bytes) = $len` or one IndexedDB
119/// `readwrite` transaction, never a read followed by a write. An append
120/// adds its bytes whole, or at worst leaves a cut last line, which the next
121/// save sets aside: [`save_async`](crate::save_async) writes one turn with
122/// one append, so a cancelled or failed save never stores part of one.
123/// [`io::ErrorKind::NotFound`] means there is no such log or file.
124///
125/// Its futures are `Send` natively, and on browser wasm neither they nor
126/// the store need be. A holder that shares the store, such as an agent,
127/// asks for `WasmCompatSend + WasmCompatSync` itself. An async store that
128/// is shared is `Clone` over an inner `Arc`: `Arc<S>` is an `AsyncStore`
129/// only when `S` is a [`Store`].
130pub trait AsyncStore {
131    /// The whole log `name`.
132    fn read_log_async(
133        &self,
134        name: &str,
135    ) -> impl Future<Output = io::Result<Vec<u8>>> + WasmCompatSend;
136
137    /// Appends `bytes` to the log `name` if it is `len` bytes long now
138    /// ([`Store::append_log`]).
139    fn append_log_async(
140        &self,
141        name: &str,
142        len: u64,
143        bytes: &[u8],
144    ) -> impl Future<Output = Result<(), LogWriteError>> + WasmCompatSend;
145
146    /// Replaces the log `name` with `bytes` if it is `len` bytes long now,
147    /// atomically and durably ([`Store::replace_log`]).
148    fn replace_log_async(
149        &self,
150        name: &str,
151        len: u64,
152        bytes: &[u8],
153    ) -> impl Future<Output = Result<(), LogWriteError>> + WasmCompatSend;
154
155    /// The file `name`.
156    fn read_file_async(
157        &self,
158        name: &str,
159    ) -> impl Future<Output = io::Result<Vec<u8>>> + WasmCompatSend;
160
161    /// Replaces the file `name` with `bytes`, atomically.
162    fn write_file_async(
163        &self,
164        name: &str,
165        bytes: &[u8],
166    ) -> impl Future<Output = io::Result<()>> + WasmCompatSend;
167}
168
169/// A local store's calls, done when made.
170impl<S: Store + ?Sized> AsyncStore for S {
171    fn read_log_async(
172        &self,
173        name: &str,
174    ) -> impl Future<Output = io::Result<Vec<u8>>> + WasmCompatSend {
175        ready(self.read_log(name))
176    }
177
178    fn append_log_async(
179        &self,
180        name: &str,
181        len: u64,
182        bytes: &[u8],
183    ) -> impl Future<Output = Result<(), LogWriteError>> + WasmCompatSend {
184        ready(self.append_log(name, len, bytes))
185    }
186
187    fn replace_log_async(
188        &self,
189        name: &str,
190        len: u64,
191        bytes: &[u8],
192    ) -> impl Future<Output = Result<(), LogWriteError>> + WasmCompatSend {
193        ready(self.replace_log(name, len, bytes))
194    }
195
196    fn read_file_async(
197        &self,
198        name: &str,
199    ) -> impl Future<Output = io::Result<Vec<u8>>> + WasmCompatSend {
200        ready(self.read_file(name))
201    }
202
203    fn write_file_async(
204        &self,
205        name: &str,
206        bytes: &[u8],
207    ) -> impl Future<Output = io::Result<()>> + WasmCompatSend {
208        ready(self.write_file(name, bytes))
209    }
210}
211
212/// The store a pointer points at: `Arc<dyn Store>` and `Box<dyn Store>`
213/// are stores, so [`AsyncStore`]s too.
214macro_rules! forward {
215    ($($pointer:ident),*) => {$(
216        impl<S: Files + ?Sized> Files for $pointer<S> {
217            fn write_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
218                (**self).write_file(name, bytes)
219            }
220
221            fn append_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
222                (**self).append_file(name, bytes)
223            }
224
225            fn read_file(&self, name: &str) -> io::Result<Vec<u8>> {
226                (**self).read_file(name)
227            }
228
229            fn remove_file(&self, name: &str) -> io::Result<()> {
230                (**self).remove_file(name)
231            }
232
233            fn files(&self) -> io::Result<Vec<String>> {
234                (**self).files()
235            }
236        }
237
238        impl<S: Store + ?Sized> Store for $pointer<S> {
239            fn logs(&self) -> io::Result<Vec<String>> {
240                (**self).logs()
241            }
242
243            fn read_log(&self, name: &str) -> io::Result<Vec<u8>> {
244                (**self).read_log(name)
245            }
246
247            fn append_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError> {
248                (**self).append_log(name, len, bytes)
249            }
250
251            fn replace_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError> {
252                (**self).replace_log(name, len, bytes)
253            }
254
255            fn destroy(&self) -> io::Result<()> {
256                (**self).destroy()
257            }
258        }
259    )*};
260}
261
262forward!(Arc, Box);
263
264/// The output of `work`, polled once. A [`Store`]'s calls through
265/// [`AsyncStore`] are done when made, so its saves finish on the first
266/// poll: the synchronous API runs the one asynchronous implementation.
267pub(crate) fn now<T>(
268    work: impl Future<Output = Result<T, SessionError>>,
269) -> Result<T, SessionError> {
270    match pin!(work).poll(&mut Context::from_waker(Waker::noop())) {
271        Poll::Ready(output) => output,
272        Poll::Pending => Err(io::Error::other("a local store's call did not finish").into()),
273    }
274}
275
276/// Refuses `name` unless it is relative and `/`-separated, with no empty,
277/// `.` or `..` part and no `\`, so it names a place inside the store
278/// wherever the store keeps it.
279///
280/// ```
281/// use rig_memory::check_name;
282///
283/// assert!(check_name("plugins/notes.json").is_ok());
284/// assert!(check_name("../elsewhere").is_err());
285/// assert!(check_name("/etc/passwd").is_err());
286/// ```
287///
288/// # Errors
289///
290/// [`io::ErrorKind::InvalidInput`], naming it.
291pub fn check_name(name: &str) -> io::Result<()> {
292    let bad = |part: &str| matches!(part, "" | "." | "..") || part.contains('\\');
293    if name.split('/').any(bad) {
294        let why = format!("{name:?} is no name in a session");
295        return Err(io::Error::new(io::ErrorKind::InvalidInput, why));
296    }
297    Ok(())
298}
299
300/// Refuses `name` as a log name: [`check_name`], in one part.
301pub(crate) fn check_leaf(name: &str) -> io::Result<()> {
302    check_name(name)?;
303    if name.contains('/') {
304        let why = format!("{name:?} is no log name");
305        return Err(io::Error::new(io::ErrorKind::InvalidInput, why));
306    }
307    Ok(())
308}
309
310/// A session kept in memory, such as for tests or where there is no file
311/// system. Cloning shares the contents.
312#[derive(Clone, Default)]
313pub struct MemoryStore(Arc<Mutex<Memory>>);
314
315#[derive(Default)]
316struct Memory {
317    logs: BTreeMap<String, Vec<u8>>,
318    files: BTreeMap<String, Vec<u8>>,
319}
320
321impl MemoryStore {
322    fn memory(&self) -> MutexGuard<'_, Memory> {
323        self.0.lock().unwrap_or_else(PoisonError::into_inner)
324    }
325}
326
327fn not_found() -> io::Error {
328    io::ErrorKind::NotFound.into()
329}
330
331impl Files for MemoryStore {
332    fn write_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
333        check_name(name)?;
334        self.memory().files.insert(name.to_owned(), bytes.to_vec());
335        Ok(())
336    }
337
338    fn append_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
339        check_name(name)?;
340        let mut memory = self.memory();
341        let file = memory.files.entry(name.to_owned()).or_default();
342        file.extend_from_slice(bytes);
343        Ok(())
344    }
345
346    fn read_file(&self, name: &str) -> io::Result<Vec<u8>> {
347        check_name(name)?;
348        self.memory().files.get(name).cloned().ok_or_else(not_found)
349    }
350
351    fn remove_file(&self, name: &str) -> io::Result<()> {
352        check_name(name)?;
353        self.memory().files.remove(name);
354        Ok(())
355    }
356
357    fn files(&self) -> io::Result<Vec<String>> {
358        Ok(self.memory().files.keys().cloned().collect())
359    }
360}
361
362impl Store for MemoryStore {
363    fn logs(&self) -> io::Result<Vec<String>> {
364        Ok(self.memory().logs.keys().cloned().collect())
365    }
366
367    fn read_log(&self, name: &str) -> io::Result<Vec<u8>> {
368        self.memory().logs.get(name).cloned().ok_or_else(not_found)
369    }
370
371    fn append_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError> {
372        check_leaf(name)?;
373        let mut memory = self.memory();
374        if memory.logs.get(name).map_or(0, Vec::len) as u64 != len {
375            return Err(LogWriteError::Changed);
376        }
377        let log = memory.logs.entry(name.to_owned()).or_default();
378        log.extend_from_slice(bytes);
379        Ok(())
380    }
381
382    fn replace_log(&self, name: &str, len: u64, bytes: &[u8]) -> Result<(), LogWriteError> {
383        check_leaf(name)?;
384        let mut memory = self.memory();
385        if memory.logs.get(name).map_or(0, Vec::len) as u64 != len {
386            return Err(LogWriteError::Changed);
387        }
388        memory.logs.insert(name.to_owned(), bytes.to_vec());
389        Ok(())
390    }
391
392    fn destroy(&self) -> io::Result<()> {
393        *self.memory() = Memory::default();
394        Ok(())
395    }
396}
397
398/// Where plugins' files are in a session's store.
399pub const PLUGINS: &str = "plugins/";
400
401/// The files of a session's store under [`PLUGINS`], by their names there:
402/// what a plugin keeps for the session.
403pub struct PluginFiles(pub Arc<dyn Files>);
404
405impl Files for PluginFiles {
406    fn write_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
407        self.0.write_file(&format!("{PLUGINS}{name}"), bytes)
408    }
409
410    fn append_file(&self, name: &str, bytes: &[u8]) -> io::Result<()> {
411        self.0.append_file(&format!("{PLUGINS}{name}"), bytes)
412    }
413
414    fn read_file(&self, name: &str) -> io::Result<Vec<u8>> {
415        self.0.read_file(&format!("{PLUGINS}{name}"))
416    }
417
418    fn remove_file(&self, name: &str) -> io::Result<()> {
419        self.0.remove_file(&format!("{PLUGINS}{name}"))
420    }
421
422    fn files(&self) -> io::Result<Vec<String>> {
423        let files = self.0.files()?.into_iter();
424        let within = files.filter_map(|name| Some(name.strip_prefix(PLUGINS)?.to_owned()));
425        Ok(within.collect())
426    }
427}