Skip to main content

blockworx_store/
container.rs

1//! The `.bwx` container: a manifest, the revs it names, and the artwork
2//! they place.
3//!
4//! ```text
5//! doc.bwx/
6//!   revs/           the whole document at every rev, compressed — THE document
7//!   manifest.jsonl  one appended row per rev: who, when, where, what it named
8//!   assets/         content-addressed artwork
9//!   lock            advisory single-writer lock
10//!   .gitattributes  how git must treat them
11//! ```
12//!
13//! Which file, in what order, and fsync'd before what is written here and
14//! once, over a [`Storage`] that says only where the bytes go — a
15//! directory of real files on a desktop, a map in a test. On a filesystem
16//! that makes the container a directory rather than a packed file, because
17//! appends, content-addressed blobs and per-writer sidecars all want to be
18//! separate files, and because git and text tools then handle a container
19//! natively.
20
21use std::cell::RefCell;
22use std::collections::{BTreeMap, VecDeque};
23
24use super::assets::{AssetFormat, AssetSink, AssetSource};
25use super::lock::{Claim, Holding};
26use super::manifest::{self, BreakReport, Row};
27use super::record::{Digest, WallTime};
28use super::revs::{self, REVS};
29use super::storage::{Entry, Name, Residency, Storage, ready_now};
30
31use blockworx_doc::{block_model::Asset, document::Document, hash::AssetHash, rev::Rev};
32
33pub const MANIFEST: Entry = Entry::fixed("manifest.jsonl");
34pub const LOCK: Entry = Entry::fixed("lock");
35pub const GITATTRIBUTES: Entry = Entry::fixed(".gitattributes");
36/// The directory a container files its artwork in.
37pub const ASSETS: &str = "assets";
38
39/// Two divergent `manifest.jsonl`s are concurrent editing through the back door,
40/// which this design deliberately has no answer for, so the conflict is
41/// surfaced at merge time instead of waiting for a replay to refuse the
42/// result.
43const GITATTRIBUTES_TEMPLATE: &str = "\
44# A blockworx container. revs/ holds the document at every rev and
45# manifest.jsonl is the trail that names them.
46#
47# The merge driver named here is not built in. Define it once, globally:
48#     git config --global merge.conflict-always.name 'never auto-merge'
49#     git config --global merge.conflict-always.driver false
50# Without it git falls back to the text merge, and a merged manifest will
51# be refused by the integrity chain at load — loudly, but later.
52manifest.jsonl merge=conflict-always
53revs/** binary
54assets/** binary
55lock export-ignore
56";
57
58/// Why a container is open without the right to write it. Every arm is
59/// something the user must be told, which is why this is not a bool.
60#[derive(Debug)]
61pub enum ReadOnlyReason {
62    /// Another live process holds the advisory lock.
63    Locked(Holding),
64    /// Opened to be read rather than edited — someone else's document being
65    /// looked at. The lock was never claimed, which is the
66    /// whole point: a reader must not stand between an owner and their own
67    /// document.
68    Reading,
69    /// The manifest does not verify; only the prefix before the break is
70    /// real.
71    HistoryBroken(BreakReport),
72    /// A row could not be appended, so this session's document and the
73    /// files have parted ways. Nothing more is written until the container
74    /// is reopened.
75    WriteFailed(std::io::Error),
76}
77
78impl std::fmt::Display for ReadOnlyReason {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            ReadOnlyReason::Locked(held) => {
82                write!(f, "another blockworx ({held}) has this diagram open")
83            }
84            ReadOnlyReason::Reading => write!(f, "this container was opened to be read"),
85            ReadOnlyReason::HistoryBroken(report) => {
86                write!(f, "the manifest does not verify at {report}")
87            }
88            ReadOnlyReason::WriteFailed(error) => {
89                write!(f, "a row could not be appended: {error}")
90            }
91        }
92    }
93}
94
95/// Whether this handle may write. The writable arm *is* the lock: it is
96/// taken when the container is attached and given up when the handle
97/// closes or is demoted, so no caller has a guard to remember.
98pub enum Access {
99    Writable,
100    ReadOnly(ReadOnlyReason),
101}
102
103/// Why a container could not be opened at all — as opposed to opened
104/// read-only, which is an outcome rather than a failure.
105#[derive(Debug, thiserror::Error)]
106pub enum ContainerError {
107    #[error("{0} already holds a diagram")]
108    Exists(String),
109    #[error("{0} is not a blockworx diagram: it has no {MANIFEST}")]
110    NotAContainer(String),
111    #[error(transparent)]
112    Io(#[from] std::io::Error),
113}
114
115/// What of the container is kept in memory.
116///
117/// A storage read as it is asked for keeps nothing: every read is a file
118/// read, as it has always been, and every write is made before the call
119/// that made it returns. A storage whose reads are really asynchronous —
120/// origin-private storage in a browser — is read whole when it is
121/// attached, so the editor's synchronous read path never reaches it, and
122/// its writes are made to that memory and owed to the storage
123/// ([`Container::drain`]).
124enum Kept {
125    Nothing,
126    Everything(RefCell<Held>),
127}
128
129#[derive(Default)]
130struct Held {
131    bytes: BTreeMap<Entry, Vec<u8>>,
132    owed: VecDeque<(Entry, Op)>,
133}
134
135/// One write a container has made to its memory and not yet to its
136/// storage.
137#[derive(Clone, Debug)]
138enum Op {
139    Write(Vec<u8>),
140    Append(Vec<u8>),
141    Truncate(u64),
142}
143
144/// How many of the writes it owed a container has made.
145#[derive(Clone, Copy, PartialEq, Eq, Debug)]
146pub struct Drained(usize);
147
148impl Drained {
149    pub const NOTHING: Self = Self(0);
150
151    pub fn performed(self) -> usize {
152        self.0
153    }
154}
155
156pub struct Container<S: Storage> {
157    storage: S,
158    access: Access,
159    kept: Kept,
160}
161
162impl<S: Storage> Container<S> {
163    /// Lay out a new container and take its lock.
164    ///
165    /// # Errors
166    /// [`ContainerError::Exists`] if the storage already holds a manifest,
167    /// or the underlying I/O failure.
168    pub fn create(storage: S, now: WallTime) -> Result<Self, ContainerError> {
169        ready_now(Self::creating(storage, now))
170    }
171
172    /// [`Self::create`] for a storage whose futures are not ready when they
173    /// are made.
174    ///
175    /// The layout goes down through the storage itself rather than through
176    /// the journal: there is no container to owe a write to until this
177    /// returns.
178    ///
179    /// # Errors
180    /// As [`Self::create`].
181    pub async fn creating(storage: S, now: WallTime) -> Result<Self, ContainerError> {
182        Self::creating_holding(storage, now, b"").await
183    }
184
185    /// The same, with `rows` already in it — the prefix Save-as, which
186    /// copies an existing manifest's lines rather than re-writing them.
187    /// The bytes go down before the lock is taken, so nothing can append
188    /// into the middle of them.
189    ///
190    /// # Errors
191    /// As [`Self::create`].
192    pub fn create_holding(storage: S, now: WallTime, rows: &[u8]) -> Result<Self, ContainerError> {
193        ready_now(Self::creating_holding(storage, now, rows))
194    }
195
196    /// # Errors
197    /// As [`Self::create_holding`].
198    pub async fn creating_holding(
199        storage: S,
200        now: WallTime,
201        rows: &[u8],
202    ) -> Result<Self, ContainerError> {
203        if storage.exists(&MANIFEST).await? {
204            return Err(ContainerError::Exists(storage.names(&Entry::ROOT)));
205        }
206        storage.create_dir(&Entry::fixed(ASSETS)).await?;
207        storage.create_dir(&Entry::fixed(REVS)).await?;
208        storage
209            .write(&GITATTRIBUTES, GITATTRIBUTES_TEMPLATE.as_bytes())
210            .await?;
211        // Whole-file, so the entries themselves and not just their contents
212        // are down: a power cut between here and the first append must not
213        // leave a directory that has a manifest in it only sometimes.
214        storage.write(&MANIFEST, rows).await?;
215        Self::attach(storage, now).await
216    }
217
218    /// Open an existing container, taking its lock if nobody holds it.
219    ///
220    /// # Errors
221    /// [`ContainerError::NotAContainer`] when there is no manifest to open, or
222    /// the underlying I/O failure.
223    pub fn open(storage: S, now: WallTime) -> Result<Self, ContainerError> {
224        ready_now(Self::opening(storage, now))
225    }
226
227    /// [`Self::open`] for a storage whose futures are not ready when they
228    /// are made — which, being a resident one, is where the container is
229    /// read into memory.
230    ///
231    /// # Errors
232    /// As [`Self::open`].
233    pub async fn opening(storage: S, now: WallTime) -> Result<Self, ContainerError> {
234        Self::require_manifest(&storage).await?;
235        Self::attach(storage, now).await
236    }
237
238    /// Open an existing container to be read and never written, leaving the
239    /// lock untouched.
240    ///
241    /// The honest mode for a reader: claiming the lock merely to look
242    /// would either demote the session editing that container or hold a
243    /// lock against one nobody is writing, and a handle that will refuse
244    /// every write anyway has no business taking either.
245    ///
246    /// # Errors
247    /// As [`Self::open`].
248    pub fn reading(storage: S) -> Result<Self, ContainerError> {
249        ready_now(Self::opening_to_read(storage))
250    }
251
252    /// # Errors
253    /// As [`Self::reading`].
254    pub async fn opening_to_read(storage: S) -> Result<Self, ContainerError> {
255        Self::require_manifest(&storage).await?;
256        Ok(Self {
257            kept: Self::kept(&storage).await?,
258            storage,
259            access: Access::ReadOnly(ReadOnlyReason::Reading),
260        })
261    }
262
263    async fn require_manifest(storage: &S) -> Result<(), ContainerError> {
264        if storage.exists(&MANIFEST).await? {
265            Ok(())
266        } else {
267            Err(ContainerError::NotAContainer(storage.names(&Entry::ROOT)))
268        }
269    }
270
271    async fn attach(storage: S, now: WallTime) -> Result<Self, ContainerError> {
272        let access = match storage.claim(now).await? {
273            Claim::Held(held) => Access::ReadOnly(ReadOnlyReason::Locked(held)),
274            Claim::Taken => Access::Writable,
275        };
276        Ok(Self {
277            kept: Self::kept(&storage).await?,
278            storage,
279            access,
280        })
281    }
282
283    /// Read the container in, for a storage that says its reads are not
284    /// ready when they are made: the manifest, then every rev and every
285    /// payload.
286    async fn kept(storage: &S) -> Result<Kept, ContainerError> {
287        if let Residency::Lazy = storage.residency() {
288            return Ok(Kept::Nothing);
289        }
290        let mut held = Held::default();
291        if let Some(bytes) = storage.read(&MANIFEST).await? {
292            held.bytes.insert(MANIFEST, bytes);
293        }
294        for dir in [REVS, ASSETS] {
295            let within = storage.list(&Entry::fixed(dir)).await.unwrap_or_default();
296            for name in within {
297                let at = Entry::under(dir, &name);
298                if let Some(bytes) = storage.read(&at).await? {
299                    held.bytes.insert(at, bytes);
300                }
301            }
302        }
303        Ok(Kept::Everything(RefCell::new(held)))
304    }
305
306    pub fn storage(&self) -> &S {
307        &self.storage
308    }
309
310    /// What this container is called — the document's own name.
311    pub fn name(&self) -> Name {
312        self.storage.name()
313    }
314
315    pub fn access(&self) -> &Access {
316        &self.access
317    }
318
319    /// The bytes at `at`, off the shelf when the container is held in
320    /// memory and out of the storage when it is not.
321    ///
322    /// # Errors
323    /// The read that did not work — which is not the same as nothing being
324    /// there.
325    pub fn read(&self, at: &Entry) -> std::io::Result<Option<Vec<u8>>> {
326        match &self.kept {
327            Kept::Nothing => ready_now(self.storage.read(at)),
328            Kept::Everything(held) => Ok(held.borrow().bytes.get(at).cloned()),
329        }
330    }
331
332    /// # Errors
333    /// The read that did not work.
334    pub fn exists(&self, at: &Entry) -> std::io::Result<bool> {
335        match &self.kept {
336            Kept::Nothing => ready_now(self.storage.exists(at)),
337            Kept::Everything(held) => Ok(held.borrow().bytes.contains_key(at)),
338        }
339    }
340
341    /// # Errors
342    /// The write that did not land — which a held container's cannot,
343    /// since it has only reached memory when this returns.
344    pub fn write(&self, at: &Entry, bytes: &[u8]) -> std::io::Result<()> {
345        let Kept::Everything(held) = &self.kept else {
346            return ready_now(self.storage.write(at, bytes));
347        };
348        let mut held = held.borrow_mut();
349        held.bytes.insert(at.clone(), bytes.to_vec());
350        held.owed.push_back((at.clone(), Op::Write(bytes.to_vec())));
351        Ok(())
352    }
353
354    fn append(&self, at: &Entry, bytes: &[u8]) -> std::io::Result<()> {
355        let Kept::Everything(held) = &self.kept else {
356            return ready_now(self.storage.append(at, bytes));
357        };
358        let mut held = held.borrow_mut();
359        held.bytes
360            .entry(at.clone())
361            .or_default()
362            .extend_from_slice(bytes);
363        held.owed
364            .push_back((at.clone(), Op::Append(bytes.to_vec())));
365        Ok(())
366    }
367
368    fn truncate(&self, at: &Entry, len: u64) -> std::io::Result<()> {
369        let Kept::Everything(held) = &self.kept else {
370            return ready_now(self.storage.truncate(at, len));
371        };
372        let mut held = held.borrow_mut();
373        if let Some(bytes) = held.bytes.get_mut(at) {
374            bytes.truncate(len as usize);
375        }
376        held.owed.push_back((at.clone(), Op::Truncate(len)));
377        Ok(())
378    }
379
380    /// How many writes this container has made to its memory and not yet
381    /// to its storage. Zero for a container that writes as it goes.
382    pub fn pending(&self) -> usize {
383        match &self.kept {
384            Kept::Nothing => 0,
385            Kept::Everything(held) => held.borrow().owed.len(),
386        }
387    }
388
389    /// Make the writes this container owes, one at a time, in the order it
390    /// made them — the payload before the rev, the rev before the row that
391    /// names it, as a container that writes as it goes makes them.
392    ///
393    /// A write that does not land leaves this session's document ahead of
394    /// the files, so the container gives up its lock, as a failed append
395    /// does where there is no journal. What it had not yet written goes
396    /// with the lock: nothing more is written to a container this session
397    /// has parted from.
398    ///
399    /// # Errors
400    /// The write that did not land.
401    pub async fn drain(&mut self) -> std::io::Result<Drained> {
402        let mut performed = 0;
403        while let Some((at, op)) = self.next_owed() {
404            let done = match &op {
405                Op::Write(bytes) => self.storage.write(&at, bytes).await,
406                Op::Append(bytes) => self.storage.append(&at, bytes).await,
407                Op::Truncate(len) => self.storage.truncate(&at, *len).await,
408            };
409            if let Err(error) = done {
410                self.forget_what_is_owed();
411                let echo = std::io::Error::other(error.to_string());
412                self.demote(ReadOnlyReason::WriteFailed(error));
413                return Err(echo);
414            }
415            performed += 1;
416        }
417        Ok(Drained(performed))
418    }
419
420    fn next_owed(&self) -> Option<(Entry, Op)> {
421        match &self.kept {
422            Kept::Nothing => None,
423            Kept::Everything(held) => held.borrow_mut().owed.pop_front(),
424        }
425    }
426
427    fn forget_what_is_owed(&self) {
428        if let Kept::Everything(held) = &self.kept {
429            held.borrow_mut().owed.clear();
430        }
431    }
432
433    /// Rename this container — which renames the document, since the
434    /// container's name is what the document is called.
435    ///
436    /// The session carries on through it: every entry is named relative to
437    /// the container, so an edit made after the rename lands in the same
438    /// manifest the edits before it did, and the lock moves with what it
439    /// locks.
440    ///
441    /// # Errors
442    /// [`std::io::ErrorKind::AlreadyExists`] when something already stands
443    /// under that name, or the rename's own failure.
444    pub fn rename(&mut self, to: &Name) -> std::io::Result<()> {
445        ready_now(self.renaming_to(to))
446    }
447
448    /// [`Self::rename`] for a storage whose futures are not ready when they
449    /// are made.
450    ///
451    /// # Errors
452    /// As [`Self::rename`].
453    pub async fn renaming_to(&mut self, to: &Name) -> std::io::Result<()> {
454        self.storage.rename(to).await
455    }
456
457    /// Give up the right to write, releasing the lock with it. Called when
458    /// the manifest turns out not to verify, and when an append fails.
459    pub fn demote(&mut self, reason: ReadOnlyReason) {
460        self.release();
461        self.access = Access::ReadOnly(reason);
462    }
463
464    /// Let go of the lock, if this handle was holding it.
465    fn release(&mut self) {
466        if let Access::Writable = self.access {
467            self.storage.release();
468        }
469    }
470
471    /// # Errors
472    /// The underlying read failure.
473    pub fn read_manifest(&self) -> std::io::Result<String> {
474        let bytes = self.read(&MANIFEST)?.unwrap_or_default();
475        String::from_utf8(bytes)
476            .map_err(|why| std::io::Error::new(std::io::ErrorKind::InvalidData, why.to_string()))
477    }
478
479    /// Where this container's artwork is read and written.
480    pub fn assets(&self) -> Artwork<'_, S> {
481        Artwork(self)
482    }
483
484    /// Where this container keeps the document at every rev.
485    pub fn revs(&self) -> Revs<'_, S> {
486        Revs(self)
487    }
488
489    /// Write the document as it stands at `rev` into `revs/`, its payloads
490    /// into `assets/`, and hand back the digest of the rev's bytes.
491    ///
492    /// Called before the row that names the rev is appended, so the
493    /// manifest never names bytes a crash could still take away — and the
494    /// payloads land before the rev that references them, for the same
495    /// reason one level down.
496    ///
497    /// # Errors
498    /// [`WriteRefusal::ReadOnly`] if this handle does not hold the lock,
499    /// or the write that did not land.
500    pub fn write_rev(&self, rev: Rev, document: &Document) -> Result<Digest, WriteRefusal> {
501        let Access::Writable = &self.access else {
502            return Err(WriteRefusal::ReadOnly);
503        };
504        Ok(revs::write(
505            &mut self.revs(),
506            rev,
507            document,
508            &mut self.assets(),
509        )?)
510    }
511
512    /// The document this container holds at `rev`, payloads and all.
513    ///
514    /// # Errors
515    /// [`revs::RevFault`]: no file, one that is not a document this build
516    /// reads, or artwork the container cannot hand back.
517    pub fn read_rev(&self, rev: Rev) -> Result<Document, revs::RevFault> {
518        let document = revs::read(&self.revs(), rev)?;
519        Ok(revs::attached(document, &self.assets())?)
520    }
521
522    /// Append one row and make it durable before returning: the line and
523    /// its newline in one write, then an fsync. A crash anywhere in here
524    /// loses at most this row, and loses it *visibly* — the manifest then
525    /// ends without a newline, which is how load knows to drop it.
526    ///
527    /// # Errors
528    /// [`WriteRefusal::ReadOnly`] if this handle does not hold the lock,
529    /// or the underlying write failure.
530    pub fn append_row(&self, row: &Row) -> Result<(), WriteRefusal> {
531        let Access::Writable = &self.access else {
532            return Err(WriteRefusal::ReadOnly);
533        };
534        let mut line = row.canonical_bytes();
535        line.push(b'\n');
536        Ok(self.append(&MANIFEST, &line)?)
537    }
538
539    /// Cut a partial trailing row off, so the next append does not splice
540    /// itself onto half a line.
541    ///
542    /// # Errors
543    /// As [`Self::append_row`].
544    pub fn truncate_manifest(&self, bytes: u64) -> Result<(), WriteRefusal> {
545        let Access::Writable = &self.access else {
546            return Err(WriteRefusal::ReadOnly);
547        };
548        Ok(self.truncate(&MANIFEST, bytes)?)
549    }
550}
551
552/// What a library shows of a container without opening it: its newest rev,
553/// and when that rev was written.
554#[derive(Clone, Copy, PartialEq, Eq, Debug)]
555pub struct Glance {
556    pub rev: Rev,
557    pub written: WallTime,
558}
559
560impl<S: Storage> Container<S> {
561    /// A glance at the container in `storage`, read without touching its
562    /// lock. `None` for one no rev has been written into yet.
563    ///
564    /// # Errors
565    /// As [`Self::reading`], or the manifest that could not be read.
566    pub async fn glance(storage: S) -> Result<Option<Glance>, ContainerError> {
567        let container = Self::opening_to_read(storage).await?;
568        let text = container.read_manifest()?;
569        let history = manifest::history(&manifest::scan(&text).rows);
570        Ok(history.rows.last().map(|row| Glance {
571            rev: row.rev,
572            written: row.wall_time,
573        }))
574    }
575}
576
577impl<S: Storage> Drop for Container<S> {
578    /// The lock's lifetime is the writable handle's lifetime, rather than
579    /// something a caller has to remember.
580    fn drop(&mut self) {
581        self.release();
582    }
583}
584
585/// A container's rev files, as the shelf a rev is read off and written to.
586pub struct Revs<'a, S: Storage>(&'a Container<S>);
587
588impl<S: Storage> revs::Backing for Revs<'_, S> {
589    fn put(&mut self, at: Rev, bytes: &[u8]) -> std::io::Result<()> {
590        self.0.write(&revs::entry(at), bytes)
591    }
592
593    fn get(&self, at: Rev) -> std::io::Result<Option<Vec<u8>>> {
594        self.0.read(&revs::entry(at))
595    }
596
597    fn names(&self, at: Rev) -> String {
598        self.0.storage.names(&revs::entry(at))
599    }
600}
601
602/// A container's `assets/` directory.
603pub struct Artwork<'a, S: Storage>(&'a Container<S>);
604
605/// Where a payload of `hash` is filed, given the format that reads it.
606pub fn asset_entry(hash: AssetHash, format: AssetFormat) -> Entry {
607    Entry::under(ASSETS, &format.file_name(hash))
608}
609
610impl<S: Storage> AssetSource for Artwork<'_, S> {
611    fn names(&self, hash: AssetHash) -> String {
612        self.0
613            .storage
614            .names(&Entry::under(ASSETS, &hash.to_string()))
615    }
616
617    fn find(&self, hash: AssetHash) -> std::io::Result<(AssetFormat, Vec<u8>)> {
618        for format in AssetFormat::ALL {
619            if let Some(bytes) = self.0.read(&asset_entry(hash, format))? {
620                return Ok((format, bytes));
621            }
622        }
623        Err(std::io::Error::from(std::io::ErrorKind::NotFound))
624    }
625}
626
627impl<S: Storage> AssetSink for Artwork<'_, S> {
628    /// Create-only: a file already under a content hash's name already
629    /// holds those bytes, so there is nothing a second write could say.
630    fn put(&mut self, hash: AssetHash, asset: &Asset) -> std::io::Result<()> {
631        let at = asset_entry(hash, AssetFormat::of(asset));
632        if self.0.exists(&at)? {
633            return Ok(());
634        }
635        self.0.write(&at, asset.bytes())
636    }
637}
638
639#[derive(Debug, thiserror::Error)]
640pub enum WriteRefusal {
641    #[error("this diagram is open read-only")]
642    ReadOnly,
643    #[error(transparent)]
644    Io(#[from] std::io::Error),
645}
646
647/// What became of a container offered for [`discard_pristine`].
648#[derive(Clone, Copy, PartialEq, Eq, Debug)]
649pub enum Discarded {
650    Removed,
651    /// Something is in it, so it stays: a row in the manifest, artwork, or a
652    /// file this layout never writes.
653    Kept,
654}
655
656/// The files [`Container::create`] lays down. Anything else in a container
657/// belongs to whoever put it there.
658const LAID_DOWN: [Entry; 3] = [MANIFEST, LOCK, GITATTRIBUTES];
659
660/// The directories it lays down. Each must be empty for the container to
661/// count as pristine.
662const LAID_DOWN_DIRS: [&str; 2] = [ASSETS, REVS];
663
664/// Remove a container nothing was ever written into, so a launch and a quit
665/// leave the documents directory as they found it.
666///
667/// Deliberately timid: an empty manifest is necessary but not sufficient, and
668/// only a container holding exactly what [`Container::create`] laid down is
669/// removed. Anything else — a row, an asset, a file the user put beside
670/// the manifest — and the container stays, because deleting something we did
671/// not write every part of is not ours to decide. The caller narrows this
672/// further: only a container *this session created* is ever offered.
673///
674/// # Errors
675/// The read or the removal that did not work.
676pub async fn discard_pristine<S: Storage>(storage: &S) -> std::io::Result<Discarded> {
677    let manifest = storage
678        .read(&MANIFEST)
679        .await?
680        .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotFound))?;
681    if !manifest.is_empty() {
682        return Ok(Discarded::Kept);
683    }
684    let mut ours = Vec::new();
685    for name in storage.list(&Entry::ROOT).await? {
686        if let Some(dir) = LAID_DOWN_DIRS.into_iter().find(|dir| *dir == name) {
687            // A directory that cannot be listed is not one we emptied, so
688            // it counts as something in the container rather than as a
689            // reason to fail.
690            if !matches!(storage.list(&Entry::fixed(dir)).await.as_deref(), Ok([])) {
691                return Ok(Discarded::Kept);
692            }
693            ours.push(Entry::fixed(dir));
694        } else if let Some(at) = LAID_DOWN.into_iter().find(|at| at.as_str() == name) {
695            ours.push(at);
696        } else {
697            return Ok(Discarded::Kept);
698        }
699    }
700    for at in ours {
701        storage.remove(&at).await?;
702    }
703    storage.discard().await?;
704    Ok(Discarded::Removed)
705}
706
707/// The same cleanup, narrowed by the lock instead of by a caller's memory of
708/// what it made: a container nobody holds and nothing was ever written into.
709///
710/// This is how a host with no exit to sweep on asks the question — a browser
711/// tab that is closed runs nothing, so the containers a session left behind
712/// are judged the next time one starts, and the lock is what tells a newborn
713/// nobody kept from one another view has open.
714///
715/// # Errors
716/// The claim, the read or the removal that did not work.
717pub async fn discard_unclaimed<S: Storage>(
718    storage: &S,
719    now: WallTime,
720) -> std::io::Result<Discarded> {
721    if !matches!(storage.claim(now).await?, Claim::Taken) {
722        return Ok(Discarded::Kept);
723    }
724    let swept = discard_pristine(storage).await;
725    // A removed container took its lock with it; there is something to give
726    // back only where it stayed.
727    if !matches!(swept, Ok(Discarded::Removed)) {
728        storage.release();
729    }
730    swept
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736    use crate::fixture::{self, block_on};
737    use crate::storage::{Memory, Native};
738
739    fn native(dir: &crate::temp::TempDir, name: &str) -> Native {
740        Native::at(dir.join(name))
741    }
742
743    #[test]
744    fn a_created_container_holds_the_whole_template() {
745        let dir = fixture::dir("container-layout");
746        let root = dir.join("doc.bwx");
747        let container = Container::create(Native::at(&root), WallTime::EPOCH)
748            .expect("the container is laid out");
749
750        assert!(matches!(container.access(), Access::Writable));
751        assert_eq!(container.read_manifest().expect("an empty manifest"), "");
752        assert!(root.join(ASSETS).is_dir());
753        assert!(root.join(REVS).is_dir(), "a new container has no revs/");
754        assert!(
755            root.join(LOCK.as_str()).is_file(),
756            "a writable handle holds the lock"
757        );
758        let attributes =
759            std::fs::read_to_string(root.join(GITATTRIBUTES.as_str())).expect("the git template");
760        assert!(attributes.contains("manifest.jsonl merge=conflict-always"));
761        assert!(attributes.contains("revs/** binary"));
762        assert!(
763            !attributes.contains("document.json"),
764            "the template still names a file this layout does not write",
765        );
766    }
767
768    #[test]
769    fn creating_over_an_existing_container_is_refused() {
770        let dir = fixture::dir("container-exists");
771        let root = dir.join("doc.bwx");
772        let first =
773            Container::create(Native::at(&root), WallTime::EPOCH).expect("the first is laid out");
774        drop(first);
775
776        assert!(matches!(
777            Container::create(Native::at(&root), WallTime::EPOCH),
778            Err(ContainerError::Exists(_)),
779        ));
780        assert!(matches!(
781            Container::open(native(&dir, "nothing-here"), WallTime::EPOCH),
782            Err(ContainerError::NotAContainer(_)),
783        ));
784    }
785
786    /// The lock is the writable handle's lifetime, so closing a container
787    /// hands the next one the right to write.
788    #[test]
789    fn dropping_a_container_releases_its_lock() {
790        let dir = fixture::dir("container-lock-lifetime");
791        let root = dir.join("doc.bwx");
792        let container =
793            Container::create(Native::at(&root), WallTime::EPOCH).expect("the container");
794        assert!(matches!(
795            Container::open(Native::at(&root), WallTime::EPOCH)
796                .expect("a second handle opens")
797                .access(),
798            Access::ReadOnly(ReadOnlyReason::Locked(_)),
799        ));
800
801        drop(container);
802        assert!(matches!(
803            Container::open(Native::at(&root), WallTime::EPOCH)
804                .expect("a third handle opens")
805                .access(),
806            Access::Writable,
807        ));
808    }
809
810    /// The property renaming a document rests on: the session keeps the
811    /// container through the move, so an append after the rename lands in
812    /// the renamed manifest — and the lock, which sits inside what moved,
813    /// is released under its new name.
814    #[test]
815    fn a_renamed_container_keeps_its_lock_and_its_open_manifest() {
816        let dir = fixture::dir("container-rename");
817        let root = dir.join("doc.bwx");
818        let renamed = dir.join("engine.bwx");
819        let mut container =
820            Container::create(Native::at(&root), WallTime::EPOCH).expect("the container");
821        let row = fixture::rows(1).pop().expect("one row");
822        container.append_row(&row).expect("the first row lands");
823
824        container
825            .rename(&Name::of_document("engine").expect("a name"))
826            .expect("the container moves");
827
828        assert_eq!(container.name().to_string(), "engine.bwx");
829        assert!(!root.exists(), "the old name still stands");
830        assert!(
831            renamed.join(LOCK.as_str()).is_file(),
832            "the lock did not travel with the container",
833        );
834        container
835            .append_row(&row)
836            .expect("an append after the rename");
837        assert_eq!(
838            std::fs::read_to_string(renamed.join(MANIFEST.as_str()))
839                .expect("the renamed manifest")
840                .lines()
841                .count(),
842            2,
843            "the session kept appending to the file it had open",
844        );
845        assert!(
846            matches!(
847                Container::open(Native::at(&renamed), WallTime::EPOCH)
848                    .expect("a second handle")
849                    .access(),
850                Access::ReadOnly(ReadOnlyReason::Locked(_)),
851            ),
852            "the lock was dropped somewhere in the move",
853        );
854
855        drop(container);
856        assert!(
857            !renamed.join(LOCK.as_str()).exists(),
858            "releasing the lock removed the wrong path",
859        );
860    }
861
862    #[test]
863    fn a_rename_onto_something_that_exists_is_refused() {
864        let dir = fixture::dir("container-rename-onto");
865        let root = dir.join("doc.bwx");
866        let taken = dir.join("taken.bwx");
867        let mut container =
868            Container::create(Native::at(&root), WallTime::EPOCH).expect("the container");
869        Container::create(Native::at(&taken), WallTime::EPOCH).expect("the container in the way");
870
871        let refusal = container
872            .rename(&Name::of_document("taken").expect("a name"))
873            .expect_err("renaming over a container must be refused");
874        assert_eq!(refusal.kind(), std::io::ErrorKind::AlreadyExists);
875        assert_eq!(
876            container.name().to_string(),
877            "doc.bwx",
878            "the store moved anyway"
879        );
880        assert!(
881            taken.join(MANIFEST.as_str()).is_file(),
882            "the container in the way is gone"
883        );
884    }
885
886    /// The cleanup, and everything it refuses to do: only a container holding
887    /// exactly what `create` laid down, with an empty log, is removed.
888    #[test]
889    fn only_a_container_with_nothing_in_it_is_discarded() {
890        let dir = fixture::dir("container-discard");
891
892        let empty = dir.join("empty.bwx");
893        drop(Container::create(Native::at(&empty), WallTime::EPOCH).expect("the empty container"));
894        assert_eq!(
895            block_on(discard_pristine(&Native::at(&empty))).expect("the sweep answers"),
896            Discarded::Removed,
897        );
898        assert!(!empty.exists(), "a pristine container was left behind");
899
900        let written = dir.join("written.bwx");
901        let container =
902            Container::create(Native::at(&written), WallTime::EPOCH).expect("the container");
903        let row = fixture::rows(1).pop().expect("one row");
904        container.append_row(&row).expect("the row lands");
905        drop(container);
906        assert_eq!(
907            block_on(discard_pristine(&Native::at(&written))).expect("the sweep answers"),
908            Discarded::Kept,
909            "a container with a row in it was removed",
910        );
911        assert!(written.join(MANIFEST.as_str()).is_file());
912
913        let theirs = dir.join("theirs.bwx");
914        drop(Container::create(Native::at(&theirs), WallTime::EPOCH).expect("the container"));
915        std::fs::write(theirs.join("notes.txt"), b"mine").expect("a file of the user's own");
916        assert_eq!(
917            block_on(discard_pristine(&Native::at(&theirs))).expect("the sweep answers"),
918            Discarded::Kept,
919            "something we did not write was removed",
920        );
921        assert!(
922            theirs.join(MANIFEST.as_str()).is_file(),
923            "and it was half-removed"
924        );
925    }
926
927    /// What the sweep asks where there is no exit to run it on: the lock
928    /// stands in for the caller's memory of what it made, so a container
929    /// another view has open survives a sweep that would otherwise take it.
930    #[test]
931    fn a_held_container_survives_the_unclaimed_sweep() {
932        let held = Memory::deferred("held.bwx");
933        drop(block_on(Container::creating(held.clone(), WallTime::EPOCH)).expect("the container"));
934        let claimed = block_on(Storage::claim(&held, WallTime::EPOCH)).expect("the claim");
935        assert!(
936            matches!(claimed, Claim::Taken),
937            "precondition: the lock is ours to hold"
938        );
939
940        assert_eq!(
941            block_on(discard_unclaimed(&held, WallTime::EPOCH)).expect("the sweep answers"),
942            Discarded::Kept,
943            "a container someone holds was swept",
944        );
945        assert!(
946            block_on(held.exists(&MANIFEST)).expect("the read"),
947            "and it was half-removed",
948        );
949
950        held.release();
951        assert_eq!(
952            block_on(discard_unclaimed(&held, WallTime::EPOCH)).expect("the sweep answers"),
953            Discarded::Removed,
954            "a container nobody holds and nobody wrote to was left behind",
955        );
956        assert!(
957            !block_on(held.exists(&MANIFEST)).expect("the read"),
958            "and it was half-removed",
959        );
960    }
961
962    #[test]
963    fn a_read_only_handle_refuses_to_write() {
964        let dir = fixture::dir("container-read-only");
965        let root = dir.join("doc.bwx");
966        let _held = Container::create(Native::at(&root), WallTime::EPOCH).expect("the container");
967        let second = Container::open(Native::at(&root), WallTime::EPOCH).expect("a second handle");
968
969        let row = fixture::rows(1).pop().expect("one row");
970        assert!(matches!(
971            second.append_row(&row),
972            Err(WriteRefusal::ReadOnly)
973        ));
974        assert!(matches!(
975            second.truncate_manifest(0),
976            Err(WriteRefusal::ReadOnly)
977        ));
978    }
979
980    /// A container read whole at open answers out of memory, and every
981    /// write keeps that memory in step — the property the browser's
982    /// synchronous read path rests on.
983    #[test]
984    fn a_resident_container_reads_back_what_it_was_written() {
985        let document = fixture::documents(1).pop().expect("one document");
986        let held = Container::create(Memory::resident("doc.bwx"), WallTime::EPOCH)
987            .expect("the container is laid out");
988        let at = blockworx_doc::fixtures::rev(1);
989        held.write_rev(at, &document).expect("the rev lands");
990
991        assert!(
992            matches!(held.kept, Kept::Everything(_)),
993            "precondition: this container is held in memory",
994        );
995        assert_eq!(held.read_rev(at).expect("it reads back"), document);
996        let row = fixture::rows(1).pop().expect("one row");
997        held.append_row(&row).expect("the row lands");
998        assert_eq!(
999            held.read_manifest().expect("the manifest").lines().count(),
1000            1,
1001        );
1002    }
1003}