Skip to main content

pamoja_core/
store.rs

1//! Durable local storage backing the offline-first synchronization layer.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5use core::future::Future;
6
7use crate::error::Result;
8
9/// A durable, first-in first-out queue for store-and-forward buffering.
10///
11/// Records are appended while a device is offline and drained in order when a
12/// link becomes available, letting applications tolerate intermittent
13/// connectivity without losing data.
14///
15/// The returned futures are `Send`, for the same reason a [`Transport`]'s are: a
16/// transport ladder buffers into a store and is itself a transport, so a task on a
17/// multi-threaded runtime can drive it. An implementation written as `async fn`
18/// satisfies this as long as everything it holds across an await is `Send`.
19///
20/// [`Transport`]: crate::Transport
21pub trait Store {
22    /// Appends a record to the back of the queue.
23    ///
24    /// # Arguments
25    ///
26    /// * `record` - the raw bytes to persist.
27    ///
28    /// # Returns
29    ///
30    /// `Ok(())` once the record is durably stored.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`Error::Io`](crate::Error::Io) if the record cannot be written to
35    /// durable storage.
36    fn append(&mut self, record: &[u8]) -> impl Future<Output = Result<()>> + Send;
37
38    /// Appends text to the back of the queue: a reading or a line written out.
39    ///
40    /// This is [`append`](Self::append) with the text's UTF-8 bytes, so a queue of
41    /// readings needs no encoding step; [`peek_text`](Self::peek_text) and
42    /// [`pop_text`](Self::pop_text) read them back.
43    ///
44    /// # Arguments
45    ///
46    /// * `text` - the text to persist.
47    ///
48    /// # Returns
49    ///
50    /// `Ok(())` once the record is durably stored.
51    ///
52    /// # Errors
53    ///
54    /// Whatever [`append`](Self::append) returns.
55    fn append_text(&mut self, text: &str) -> impl Future<Output = Result<()>> + Send {
56        self.append(text.as_bytes())
57    }
58
59    /// Returns the oldest record without removing it.
60    ///
61    /// This lets a forwarder send a record before committing to its removal, so a
62    /// failed send can leave the record buffered in order rather than dropping it.
63    ///
64    /// # Returns
65    ///
66    /// `Some(record)` containing the oldest buffered bytes, or `None` if the queue
67    /// is empty.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`Error::Io`](crate::Error::Io) if the queue cannot be read.
72    fn peek(&self) -> impl Future<Output = Result<Option<Vec<u8>>>> + Send;
73
74    /// Returns the oldest record as text, without removing it.
75    ///
76    /// It borrows the store across an await, so it asks for `Sync`, which a store
77    /// whose own futures are `Send` already is.
78    ///
79    /// # Returns
80    ///
81    /// `Some(text)` for the oldest record, or `None` if the queue is empty.
82    ///
83    /// # Errors
84    ///
85    /// Whatever [`peek`](Self::peek) returns, or
86    /// [`Error::Codec`](crate::Error::Codec) if the record is not UTF-8 text.
87    fn peek_text(&self) -> impl Future<Output = Result<Option<String>>> + Send
88    where
89        Self: Sync,
90    {
91        async { as_text(self.peek().await?) }
92    }
93
94    /// Removes and returns the oldest record in the queue.
95    ///
96    /// # Returns
97    ///
98    /// `Some(record)` containing the oldest buffered bytes, or `None` if the queue
99    /// is empty.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`Error::Io`](crate::Error::Io) if the queue cannot be read.
104    fn pop(&mut self) -> impl Future<Output = Result<Option<Vec<u8>>>> + Send;
105
106    /// Removes and returns the oldest record as text.
107    ///
108    /// # Returns
109    ///
110    /// `Some(text)` for the oldest record, or `None` if the queue is empty.
111    ///
112    /// # Errors
113    ///
114    /// Whatever [`pop`](Self::pop) returns, or
115    /// [`Error::Codec`](crate::Error::Codec) if the record is not UTF-8 text.
116    fn pop_text(&mut self) -> impl Future<Output = Result<Option<String>>> + Send
117    where
118        Self: Send,
119    {
120        async { as_text(self.pop().await?) }
121    }
122
123    /// Returns the number of records currently buffered.
124    ///
125    /// # Returns
126    ///
127    /// The count of records waiting to be drained.
128    ///
129    /// # Errors
130    ///
131    /// Returns [`Error::Io`](crate::Error::Io) if the queue length cannot be
132    /// determined.
133    fn len(&self) -> impl Future<Output = Result<usize>> + Send;
134
135    /// Returns whether the queue currently holds no records.
136    ///
137    /// The default implementation reports whether [`len`](Self::len) is zero. It
138    /// borrows the store across an await, so it asks for `Sync`, which a store whose
139    /// own futures are `Send` already is.
140    ///
141    /// # Returns
142    ///
143    /// `true` if the queue is empty, `false` otherwise.
144    ///
145    /// # Errors
146    ///
147    /// Returns [`Error::Io`](crate::Error::Io) if the queue length cannot be
148    /// determined.
149    fn is_empty(&self) -> impl Future<Output = Result<bool>> + Send
150    where
151        Self: Sync,
152    {
153        async { Ok(self.len().await? == 0) }
154    }
155}
156
157// A buffered record read as text, for the `_text` helpers above.
158fn as_text(record: Option<Vec<u8>>) -> Result<Option<String>> {
159    match record {
160        Some(bytes) => String::from_utf8(bytes)
161            .map(Some)
162            .map_err(|_| crate::Error::Codec("the record is not UTF-8 text".into())),
163        None => Ok(None),
164    }
165}