Skip to main content

blockworx_store/storage/
mod.rs

1//! Where one container's bytes live.
2//!
3//! The `.bwx` layout — which file, in what order, fsync'd before what — is
4//! the container's business and is written once, over this trait. What a
5//! [`Storage`] answers is narrower: read, write, append, truncate, list and
6//! remove, by container-relative [`Entry`] name, the three facts about the
7//! container itself (what it is called, where it is, how much of it is
8//! worth holding in memory), and the lock that says who may write it.
9//!
10//! **Async below, synchronous above.** The editor's read path is
11//! synchronous and stays so, while origin-private storage in a browser is
12//! not: every method answers a future, and a storage whose futures are
13//! ready the moment they are made ([`Native`], [`Memory`]) is resolved with
14//! one poll ([`ready_now`]). A storage whose futures are *not* ready is
15//! read into memory at open instead ([`Residency::Resident`]), so the reads
16//! the editor makes never reach it and the writes it makes are owed to the
17//! storage rather than made through it
18//! ([`Container::drain`](crate::container::Container::drain)).
19
20pub mod erased;
21pub mod memory;
22pub mod native;
23
24use core::future::Future;
25use std::borrow::Cow;
26use std::path::Path;
27
28use super::lock::Claim;
29use super::record::WallTime;
30
31pub use erased::Any;
32pub use memory::Memory;
33pub use native::Native;
34
35/// One file of a container, named relative to it: `manifest.jsonl`,
36/// `revs/000012.json.gz`. Never a platform path — the storage is what
37/// knows whether there is one.
38#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
39pub struct Entry(Cow<'static, str>);
40
41impl Entry {
42    /// The container itself, which is what [`Storage::list`] is asked for
43    /// when the question is what is in it.
44    pub const ROOT: Self = Self::fixed("");
45
46    pub const fn fixed(name: &'static str) -> Self {
47        Self(Cow::Borrowed(name))
48    }
49
50    /// The entry `name` spells, for a caller reading names back rather than
51    /// naming them in the source — a listing, an archive.
52    pub fn named(name: &str) -> Self {
53        Self(Cow::Owned(name.to_owned()))
54    }
55
56    /// `dir/name` — the one spelling of a nested entry, so no two callers
57    /// can build it differently.
58    pub fn under(dir: &str, name: &str) -> Self {
59        Self(Cow::Owned(format!("{dir}/{name}")))
60    }
61
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65}
66
67impl std::fmt::Display for Entry {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.write_str(&self.0)
70    }
71}
72
73/// What a container is called, wherever it is kept.
74///
75/// One segment, never a path: a rename gives the document another name and
76/// never another home, which is what a rename box is for. The container
77/// extension is part of it, because the container's own name *is* the
78/// document's.
79#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
80pub struct Name(String);
81
82/// The document format's extension, on a container.
83pub const CONTAINER_EXTENSION: &str = "bwx";
84
85impl Name {
86    /// The name as it is spelled, or `None` for one that is no name:
87    /// blank, hidden, or more than one segment.
88    pub fn new(name: &str) -> Option<Self> {
89        let name = name.trim();
90        let one_segment = Path::new(name).components().count() == 1;
91        if name.is_empty() || name.starts_with('.') || !one_segment {
92            return None;
93        }
94        Some(Self(name.to_owned()))
95    }
96
97    /// What the user typed, as a container is named: the suffix is
98    /// appended rather than substituted, since a document called `notes.v2`
99    /// should not become `notes.bwx`.
100    pub fn of_document(name: &str) -> Option<Self> {
101        let Self(name) = Self::new(name)?;
102        if named_as_a_container(&name) {
103            return Some(Self(name));
104        }
105        Some(Self(format!("{name}.{CONTAINER_EXTENSION}")))
106    }
107
108    /// The container an archive's file name says it carries: `engine.bwx.zip`
109    /// carries `engine.bwx`.
110    pub fn of_archive(file: &str) -> Option<Self> {
111        Self::of_document(file.strip_suffix(".zip").unwrap_or(file))
112    }
113
114    pub fn as_str(&self) -> &str {
115        &self.0
116    }
117}
118
119impl std::fmt::Display for Name {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        f.write_str(&self.0)
122    }
123}
124
125fn named_as_a_container(name: &str) -> bool {
126    Path::new(name)
127        .extension()
128        .is_some_and(|ext| ext.eq_ignore_ascii_case(CONTAINER_EXTENSION))
129}
130
131/// Which document a door names, as the shell that opens it spells one: a
132/// path on a desktop, a name in the library in a browser.
133///
134/// Opaque to everything in between. The kernel raises it and carries it
135/// back; only the shell that minted it resolves it, against its own
136/// storage — which is why "where the documents are" never has to become a
137/// fact of the crates under it.
138#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
139#[serde(transparent)]
140pub struct DocumentRef(String);
141
142impl DocumentRef {
143    pub fn new(named: impl Into<String>) -> Self {
144        Self(named.into())
145    }
146
147    pub fn as_str(&self) -> &str {
148        &self.0
149    }
150}
151
152impl std::fmt::Display for DocumentRef {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        f.write_str(&self.0)
155    }
156}
157
158impl From<&Path> for DocumentRef {
159    fn from(path: &Path) -> Self {
160        Self(path.to_string_lossy().into_owned())
161    }
162}
163
164impl From<&DocumentRef> for std::path::PathBuf {
165    fn from(named: &DocumentRef) -> Self {
166        Self::from(&named.0)
167    }
168}
169
170/// How much of a container is worth holding in memory.
171///
172/// The storage says which, because it is a fact about where the bytes are:
173/// a directory is read as it is asked for, and a storage whose reads are
174/// really asynchronous is read once at open so that every read the editor
175/// makes afterwards is a memory hit.
176#[derive(Clone, Copy, PartialEq, Eq, Debug)]
177pub enum Residency {
178    Lazy,
179    Resident,
180}
181
182/// Where one container's bytes live.
183pub trait Storage {
184    /// What this container is called.
185    fn name(&self) -> Name;
186
187    /// What a report calls `at` — a platform path where there is one. Not
188    /// a promise that anything is there.
189    fn names(&self, at: &Entry) -> String;
190
191    fn residency(&self) -> Residency;
192
193    /// Where these bytes sit on a filesystem, for a shell that can name
194    /// one. `None` for a storage that has no such thing.
195    fn disk_path(&self) -> Option<&Path> {
196        None
197    }
198
199    /// The bytes at `at`, or `None` where nothing is — which is not the
200    /// same as a read that did not work.
201    fn read<'a>(
202        &'a self,
203        at: &'a Entry,
204    ) -> impl Future<Output = std::io::Result<Option<Vec<u8>>>> + 'a;
205
206    /// Whether anything stands at `at`. Separate from [`Self::read`]
207    /// because a payload is filed under a content hash, so the answer is
208    /// all a write needs — and reading a megabyte to learn it is already
209    /// there is the one thing that would make every commit cost the
210    /// artwork on the page.
211    fn exists<'a>(&'a self, at: &'a Entry) -> impl Future<Output = std::io::Result<bool>> + 'a;
212
213    /// Whole file or nothing: a reader caught mid-write sees the previous
214    /// contents, never half of each.
215    fn write<'a>(
216        &'a self,
217        at: &'a Entry,
218        bytes: &'a [u8],
219    ) -> impl Future<Output = std::io::Result<()>> + 'a;
220
221    /// Add `bytes` to the end of `at`, durably before returning.
222    fn append<'a>(
223        &'a self,
224        at: &'a Entry,
225        bytes: &'a [u8],
226    ) -> impl Future<Output = std::io::Result<()>> + 'a;
227
228    fn truncate<'a>(
229        &'a self,
230        at: &'a Entry,
231        len: u64,
232    ) -> impl Future<Output = std::io::Result<()>> + 'a;
233
234    /// What `dir` holds, by name, in no particular order.
235    fn list<'a>(
236        &'a self,
237        dir: &'a Entry,
238    ) -> impl Future<Output = std::io::Result<Vec<String>>> + 'a;
239
240    fn remove<'a>(&'a self, at: &'a Entry) -> impl Future<Output = std::io::Result<()>> + 'a;
241
242    /// Make `dir` a place entries can be written.
243    fn create_dir<'a>(&'a self, dir: &'a Entry) -> impl Future<Output = std::io::Result<()>> + 'a;
244
245    /// Call this container something else, leaving everything in it where
246    /// it is — which is what renaming the document means.
247    fn rename<'a>(&'a mut self, to: &'a Name) -> impl Future<Output = std::io::Result<()>> + 'a;
248
249    /// Remove the container itself. Only ever asked of one that has been
250    /// emptied.
251    fn discard(&self) -> impl Future<Output = std::io::Result<()>> + '_;
252
253    /// Claim the right to write this container.
254    ///
255    /// The default is the [lock entry](super::lock) a container has always
256    /// carried: a row naming the process that holds it, broken when that
257    /// process is gone. A storage whose host keeps locks of its own
258    /// answers from that instead — which is not a nicety in a browser,
259    /// where nothing can be asked whether the tab that left an entry
260    /// behind is still there.
261    fn claim(&self, now: WallTime) -> impl Future<Output = std::io::Result<Claim>> + '_ {
262        super::lock::claim(self, now)
263    }
264
265    /// Give the lock back.
266    ///
267    /// Synchronous where the claim is not, because a container releases
268    /// its lock as it is dropped and a `Drop` cannot await. Which is why a
269    /// storage whose futures are not ready must hold its lock somewhere it
270    /// can let go of in one call — its host's lock manager — rather than
271    /// in the entry the default writes: an entry it could not remove here
272    /// would outlive every session that took it.
273    fn release(&self) {
274        super::lock::release(self);
275    }
276}
277
278/// Resolve a future that promised to be ready the moment it was made.
279///
280/// The editor's read path is synchronous, so a storage whose
281/// [`Storage::residency`] is [`Residency::Lazy`] is read through this: one
282/// poll, no executor, no waker. A storage that answers `Pending` here has
283/// broken that promise — a programming error, reported as one rather than
284/// blocking a frame on a spin.
285pub fn ready_now<T, E: From<std::io::Error>>(
286    future: impl Future<Output = Result<T, E>>,
287) -> Result<T, E> {
288    use core::task::{Context, Poll};
289    let mut future = core::pin::pin!(future);
290    match future
291        .as_mut()
292        .poll(&mut Context::from_waker(core::task::Waker::noop()))
293    {
294        Poll::Ready(done) => done,
295        Poll::Pending => Err(std::io::Error::new(
296            std::io::ErrorKind::WouldBlock,
297            "this storage is read as if it were ready, and it was not",
298        )
299        .into()),
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn a_name_is_one_segment_and_carries_the_container_extension() {
309        assert_eq!(
310            Name::of_document("motor-controller").map(|n| n.to_string()),
311            Some("motor-controller.bwx".to_owned()),
312        );
313        assert_eq!(
314            Name::of_document("  motor-controller  ").map(|n| n.to_string()),
315            Some("motor-controller.bwx".to_owned()),
316            "the box's whitespace is not part of the name",
317        );
318        assert_eq!(
319            Name::of_document("notes.v2").map(|n| n.to_string()),
320            Some("notes.v2.bwx".to_owned()),
321            "the suffix is appended, so a dotted name keeps all of itself",
322        );
323        assert_eq!(
324            Name::of_document("engine.bwx").map(|n| n.to_string()),
325            Some("engine.bwx".to_owned()),
326            "a name already carrying the extension does not gain a second",
327        );
328        for refused in [
329            "",
330            "   ",
331            ".hidden",
332            "..",
333            "sub/engine",
334            "/elsewhere/engine",
335        ] {
336            assert_eq!(
337                Name::of_document(refused),
338                None,
339                "{refused:?} was taken for a document name",
340            );
341        }
342    }
343
344    #[test]
345    fn an_entry_is_named_relative_to_its_container() {
346        assert_eq!(
347            Entry::under("revs", "000012.json.gz").as_str(),
348            "revs/000012.json.gz"
349        );
350        assert_eq!(Entry::ROOT.as_str(), "");
351    }
352
353    /// The promise the synchronous read path rests on, and what happens
354    /// when a storage breaks it.
355    #[test]
356    fn a_future_that_is_not_ready_is_reported_rather_than_waited_on() {
357        assert_eq!(
358            ready_now(core::future::ready(std::io::Result::Ok(7))).expect("ready"),
359            7
360        );
361        let refusal = ready_now(core::future::pending::<std::io::Result<u8>>())
362            .expect_err("a pending future cannot be resolved");
363        assert_eq!(refusal.kind(), std::io::ErrorKind::WouldBlock);
364    }
365}