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}