Skip to main content

offline_protocol/protocol/
data.rs

1//! The replicated-document store: persistence, caps, and compaction.
2//!
3//! The engine lives in `offline-protocol-data` and knows nothing about
4//! storage. This module is the other half: it decides when a document is
5//! read, when its changes reach disk, and when history is folded away. It
6//! owns no cryptography of its own — records go through the same sealing
7//! chokepoint every other protocol-state category uses, which is what makes
8//! a swapped-in storage backend unable to change the at-rest posture.
9//!
10//! # Record layout
11//!
12//! | Category | Key id | Contents |
13//! |---|---|---|
14//! | `data_docs` | `{space}/{doc}` | The compacted document |
15//! | `data_delta_log` | `{space}/{doc}/{seq:016x}` | One persisted commit |
16//! | `data_spaces` | `{space}` | The document index for the space |
17//! | `data_doc_meta` | `{space}/{doc}` | What a name says after a removal |
18//!
19//! # Why the listing is the truth and the index is a cache
20//!
21//! Everything needed to re-open a document is derived from what is actually
22//! on disk: `list_keys` names the delta records, and their zero-padded hex
23//! sequence sorts into apply order. The space record only remembers document
24//! names, so a document created and never written still lists. Any crash
25//! therefore leaves a readable store rather than a bookkeeping claim that
26//! has to be reconciled against reality.
27//!
28//! Removals are the one thing the listing cannot express, and the reason
29//! `data_doc_meta` exists. A removed document has no records, and neither
30//! does a document nobody ever created: reading the truth off the listing
31//! would make those two the same state, which is exactly what lets a peer's
32//! next offer refill something this device deleted on purpose. The removal
33//! record is therefore written *before* the records it removes, so a crash
34//! in between leaves a name already marked removed rather than a name whose
35//! surviving records vote it back into existence.
36
37use std::collections::{BTreeMap, BTreeSet};
38use std::sync::Arc;
39
40use offline_protocol_data::{
41    policy, CatchUp, DataDoc, DataError, DataValue, RemoteImport, VersionToken,
42};
43use serde::{Deserialize, Serialize};
44use tracing::{debug, warn};
45
46use crate::error::{Error, Result};
47use crate::events::{DocRemovedBy, Event};
48use crate::protocol::data_sync::MAX_REMOVAL_FLOOR_BYTES;
49use crate::protocol::storage::StateRecord;
50use crate::protocol::types::{storage_keys, MAX_PROTOCOL_STATE_RECORD_BYTES};
51use crate::protocol::OfflineProtocol;
52use crate::protocol_state_storage::ProtocolStateStorage;
53
54/// How many interest patterns one space accepts.
55///
56/// A bound rather than a policy: the list is walked once per document per
57/// offer, so an unbounded one would let an application make its own peers
58/// expensive to answer.
59pub(crate) const MAX_INTEREST_PATTERNS: usize = 32;
60
61/// Whether one interest pattern matches a document name.
62///
63/// `*` alone is everything; a trailing `*` is a prefix; anything else is an
64/// exact name. The document charset (`A-Z a-z 0-9 . _ -`) has no `*` in it,
65/// so a wildcard is never ambiguous with a name that contains one.
66pub(crate) fn interest_matches(pattern: &str, doc: &str) -> bool {
67    match pattern.strip_suffix('*') {
68        Some(prefix) => doc.starts_with(prefix),
69        None => pattern == doc,
70    }
71}
72
73/// Reject a pattern that could never match, or that names something the
74/// document charset cannot express.
75///
76/// Checked where it is written rather than where it is used, so an
77/// application hears about a typo at the call that made it instead of
78/// silently receiving nothing.
79fn validate_interest_pattern(pattern: &str) -> Result<()> {
80    // `*` alone is the default expressed explicitly, and it is the one
81    // pattern whose body is empty.
82    if pattern == "*" {
83        return Ok(());
84    }
85    // Everything else is a document name with an optional trailing `*`, so
86    // the document validator judges it rather than a second copy of the
87    // charset. A copy would drift the day `validate_name` widens, and the
88    // symptom would be a pattern that cannot match a name the store accepts.
89    let body = pattern.strip_suffix('*').unwrap_or(pattern);
90    offline_protocol_data::validate_name(body).map_err(|err| {
91        Error::InvalidArgument(format!(
92            "an interest pattern is a document name with an optional \
93             trailing `*`: {pattern:?} {err}"
94        ))
95    })
96}
97
98/// The document index for one space.
99///
100/// Deliberately thin. Sizes and sequence numbers are derived from the
101/// records themselves at open, so this record can never disagree with the
102/// store about what exists; it only remembers names, which nothing else
103/// records for a document that has never been written.
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
105struct SpaceRecord {
106    #[serde(default)]
107    docs: BTreeSet<String>,
108}
109
110/// What a document's name says after a removal.
111///
112/// The `gone` field is the removal *floor*: the version the document stood
113/// at when it was removed, which is the whole of what a removal decided
114/// about. Content at or below it was removed; content beyond it is an edit
115/// made concurrently with the removal or after it, which the removal never
116/// saw. That is what makes an edit win over a removal without either side
117/// needing to know which happened first.
118///
119/// `alive` is separate from the floor on purpose, and is not derivable from
120/// whether records exist. A crash between the removal record and the
121/// deletes leaves records behind, and reading those as "alive" is precisely
122/// the resurrection this record prevents. The floor survives a document
123/// coming back, so a stale replica's copy of the removed content is still
124/// refused afterwards.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126struct DocMeta {
127    /// Base64 of the version the document was removed at.
128    gone: String,
129    /// Whether the name holds a document again.
130    ///
131    /// Defaults false so a record that fails to carry the field reads as
132    /// removed, which is the answer that loses no data: a document read as
133    /// removed comes back from any replica that still holds it, while one
134    /// wrongly read as live is a resurrection nothing reports.
135    #[serde(default)]
136    alive: bool,
137}
138
139/// What applying a peer's removal did to this device's copy.
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub(crate) enum TombstoneOutcome {
142    /// The copy held here was content the floor covered, and is gone.
143    Deleted,
144    /// The copy held here carries an edit the removal never saw, so it
145    /// stays and goes back to the peer through the ordinary exchange.
146    Kept,
147    /// Nothing was held under that name; the floor is recorded so a replica
148    /// that still holds the removed content cannot seed it back.
149    Recorded,
150    /// A floor already held here covers this one, so it carried nothing
151    /// new and nothing was written or opened. The common case: floors
152    /// travel on every offer for the life of the space.
153    Unchanged,
154}
155
156/// A document held open in memory.
157struct LoadedDoc {
158    doc: DataDoc,
159    /// Sequence number the next persisted commit will use.
160    next_seq: u64,
161    /// Total size of the delta records currently on disk for this document.
162    log_bytes: usize,
163    /// Size of the last compacted record written for this document.
164    compacted_bytes: usize,
165    /// Set when the document's compacted encoding passed the cap.
166    ///
167    /// Growth is refused while it is set; deletions are not, so an
168    /// application that hit the cap can shrink its way back out. Cleared by
169    /// the next flush that measures under the cap.
170    over_cap: bool,
171    /// Whether every delta record on disk actually applied at open.
172    ///
173    /// False when a delta was unreadable, or when one imported but parked
174    /// behind a predecessor that never arrived. Both mean the document in
175    /// memory is missing changes the records still describe, and both make
176    /// compaction destructive: folding the log would write a snapshot without
177    /// those changes and then delete the records holding them. A growing log
178    /// is the cheaper failure, so compaction is simply switched off until an
179    /// open sees a complete log again.
180    history_complete: bool,
181}
182
183/// In-memory state for the data layer.
184#[derive(Default)]
185pub(crate) struct DataLayer {
186    docs: BTreeMap<(String, String), LoadedDoc>,
187    spaces: BTreeMap<String, SpaceRecord>,
188    /// Removal state per document, loaded with the space that holds it.
189    meta: BTreeMap<(String, String), DocMeta>,
190    /// What this device wants from its peers, per space.
191    ///
192    /// Absent means everything, which is what a space nobody has narrowed
193    /// says and what every release before interest existed said. Deliberately
194    /// not persisted: it is application policy about this launch, not a fact
195    /// about the store, and a durable copy would be a second thing to
196    /// reconcile against an application that has changed its mind.
197    interest: BTreeMap<String, Vec<String>>,
198}
199
200impl std::fmt::Debug for DataLayer {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        f.debug_struct("DataLayer")
203            .field("open_docs", &self.docs.len())
204            .field("loaded_spaces", &self.spaces.len())
205            .field("removed_docs", &self.meta.len())
206            .field("narrowed_spaces", &self.interest.len())
207            .finish()
208    }
209}
210
211fn map_data_error(err: DataError) -> Error {
212    match err {
213        DataError::DocTooLarge { actual, limit } => Error::DocTooLarge { actual, limit },
214        DataError::Corrupt(detail) | DataError::Engine(detail) => Error::DataCorrupted(detail),
215        DataError::Poisoned => {
216            Error::DataCorrupted("document handle poisoned by an earlier decode failure".into())
217        }
218        DataError::InvalidName { name, reason } => {
219            Error::InvalidArgument(format!("invalid name {name:?}: {reason}"))
220        }
221        DataError::ValueTooLarge { actual, limit } => Error::InvalidArgument(format!(
222            "value is {actual} bytes, over the {limit} byte limit"
223        )),
224        DataError::InvalidAttachment { reason } => {
225            Error::InvalidArgument(format!("invalid attachment: {reason}"))
226        }
227        DataError::OutOfRange { position, length } => Error::InvalidArgument(format!(
228            "position {position} is out of range for length {length}"
229        )),
230        // Forced by `#[non_exhaustive]` on `DataError`, which the data crate
231        // carries so a new variant is not a semver break for published
232        // consumers. That trade costs the compile error that would otherwise
233        // catch an unmapped variant, so it is replaced with a `warn!`: an
234        // unmapped variant shows up in logs instead of vanishing. When you add
235        // a variant, add its arm here — this is a tripwire, not a destination.
236        ref other => {
237            warn!(
238                error = %other,
239                "unmapped DataError variant reached the protocol boundary; \
240                 add an explicit arm in protocol/data.rs"
241            );
242            Error::Other(other.to_string())
243        }
244    }
245}
246
247/// Compose the record key for a document's compacted snapshot.
248fn doc_key(space: &str, doc: &str) -> String {
249    format!("{space}/{doc}")
250}
251
252/// Compose the record key for one persisted commit.
253///
254/// The sequence is zero-padded hex so that lexicographic order — which is
255/// the only order `list_keys` promises anything about — is apply order.
256fn delta_key(space: &str, doc: &str, seq: u64) -> String {
257    format!("{space}/{doc}/{seq:016x}")
258}
259
260/// Parse the sequence out of a delta record key belonging to this document.
261fn delta_seq(key: &str, prefix: &str) -> Option<u64> {
262    let suffix = key.strip_prefix(prefix)?;
263    u64::from_str_radix(suffix, 16).ok()
264}
265
266impl OfflineProtocol {
267    /// The backend documents are stored in.
268    ///
269    /// The application's override when it set one, and otherwise the store
270    /// protocol state already uses — which is what makes the layer work with
271    /// no storage configuration at all.
272    fn data_storage(&self) -> Option<Arc<dyn ProtocolStateStorage>> {
273        self.config
274            .data
275            .storage
276            .clone()
277            .or_else(|| self.protocol_state_storage.clone())
278    }
279
280    /// The backend, or the typed error explaining which precondition failed.
281    fn require_data_storage(&self) -> Result<Arc<dyn ProtocolStateStorage>> {
282        if !self.config.data.enabled {
283            return Err(Error::DataDisabled);
284        }
285        self.data_storage().ok_or(Error::DataStorageUnavailable)
286    }
287
288    fn validate_ids(space: &str, doc: &str) -> Result<()> {
289        offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
290        offline_protocol_data::validate_name(doc).map_err(map_data_error)?;
291        Ok(())
292    }
293
294    /// Read a space's document index, loading it on first touch.
295    fn load_space(&mut self, storage: &dyn ProtocolStateStorage, space: &str) -> Result<()> {
296        if self.data.spaces.contains_key(space) {
297            return Ok(());
298        }
299        let mut record = SpaceRecord::default();
300        match self.read_state_record(storage, storage_keys::DATA_SPACES, space) {
301            Ok(Some(bytes)) => match serde_json::from_slice::<SpaceRecord>(&bytes) {
302                Ok(parsed) => record = parsed,
303                Err(err) => {
304                    // The index is a cache: a damaged one costs the names of
305                    // documents that have no records yet, never a document.
306                    warn!(space, error = %err, "Unreadable data space index; rebuilding from records");
307                }
308            },
309            Ok(None) => {}
310            Err(err) => {
311                warn!(space, error = %err, "Failed to read data space index");
312            }
313        }
314
315        // Reconcile against what is actually stored, so the index can never
316        // hide a document that exists.
317        for key_type in [storage_keys::DATA_DOCS, storage_keys::DATA_DELTA_LOG] {
318            if let Ok(keys) = storage.list_keys(key_type) {
319                for key in keys {
320                    if let Some(rest) = key.strip_prefix(&format!("{space}/")) {
321                        let name = rest.split('/').next().unwrap_or(rest);
322                        if !name.is_empty() {
323                            record.docs.insert(name.to_string());
324                        }
325                    }
326                }
327            }
328        }
329
330        // Removals last, because they overrule the listing. A name the
331        // reconciliation above just voted back into existence may be one a
332        // previous run removed and did not finish deleting, and the records
333        // it left are what that vote was counted from.
334        let mut half_removed = Vec::new();
335        if let Ok(keys) = storage.list_keys(storage_keys::DATA_DOC_META) {
336            let prefix = format!("{space}/");
337            for key in keys {
338                let Some(name) = key.strip_prefix(&prefix) else {
339                    continue;
340                };
341                if name.is_empty() || name.contains('/') {
342                    continue;
343                }
344                let meta = match self.read_state_record(storage, storage_keys::DATA_DOC_META, &key)
345                {
346                    Ok(Some(bytes)) => match serde_json::from_slice::<DocMeta>(&bytes) {
347                        Ok(meta) => meta,
348                        Err(err) => {
349                            // Unreadable here is not the index's "costs a
350                            // name" case: this record is the only thing that
351                            // says a document was removed, so a damaged one
352                            // is left in place and the name is left alone
353                            // rather than being silently revived.
354                            warn!(space, doc = name, error = %err, "Unreadable document removal record");
355                            continue;
356                        }
357                    },
358                    Ok(None) => continue,
359                    Err(err) => {
360                        warn!(space, doc = name, error = %err, "Failed to read a document removal record");
361                        continue;
362                    }
363                };
364                if !meta.alive && record.docs.remove(name) {
365                    half_removed.push(name.to_string());
366                }
367                self.data
368                    .meta
369                    .insert((space.to_string(), name.to_string()), meta);
370            }
371        }
372
373        self.data.spaces.insert(space.to_string(), record);
374
375        // Finish what a crash interrupted. Best effort, and repeated on
376        // every open until it succeeds: the name is already removed as far
377        // as every reader is concerned, so what is left is storage to
378        // reclaim rather than a decision to make.
379        for doc in half_removed {
380            debug!(
381                space,
382                doc, "Finishing a removal a previous run did not complete"
383            );
384            self.purge_doc_records(storage, space, &doc, None);
385        }
386        Ok(())
387    }
388
389    /// Delete every record belonging to one document, reporting the first
390    /// failure and attempting the rest regardless.
391    ///
392    /// A record that survives is replayed into the *next* document of the
393    /// same name, which resurrects the removed contents, so a partial
394    /// failure is worth hearing about even though every caller here can only
395    /// log it.
396    ///
397    /// `delta_keys` is the delta log's listing when the caller already holds
398    /// one, so a caller purging many documents lists once rather than once
399    /// per document. It has to postdate the document's last flush, or the
400    /// delta that flush wrote is exactly the record that survives.
401    fn purge_doc_records(
402        &mut self,
403        storage: &dyn ProtocolStateStorage,
404        space: &str,
405        doc: &str,
406        delta_keys: Option<&[String]>,
407    ) -> Option<String> {
408        fn note(
409            first_error: &mut Option<String>,
410            err: crate::protocol_state_storage::ProtocolStateError,
411        ) {
412            if first_error.is_none() {
413                *first_error = Some(err.to_string());
414            }
415        }
416        let mut first_error: Option<String> = None;
417
418        if let Err(err) = storage.delete(storage_keys::DATA_DOCS, &doc_key(space, doc)) {
419            note(&mut first_error, err);
420        }
421        let prefix = format!("{space}/{doc}/");
422        let listed;
423        let keys: &[String] = match delta_keys {
424            Some(keys) => keys,
425            None => match storage.list_keys(storage_keys::DATA_DELTA_LOG) {
426                Ok(keys) => {
427                    listed = keys;
428                    &listed
429                }
430                Err(err) => {
431                    note(&mut first_error, err);
432                    &[]
433                }
434            },
435        };
436        for key in keys.iter().filter(|key| key.starts_with(&prefix)) {
437            if let Err(err) = storage.delete(storage_keys::DATA_DELTA_LOG, key) {
438                note(&mut first_error, err);
439            }
440        }
441        first_error
442    }
443
444    /// Write one document's removal record.
445    fn persist_doc_meta(
446        &mut self,
447        storage: &dyn ProtocolStateStorage,
448        space: &str,
449        doc: &str,
450        meta: &DocMeta,
451    ) -> Result<()> {
452        let bytes =
453            serde_json::to_vec(meta).map_err(|err| Error::Serialization(err.to_string()))?;
454        self.write_state_record(
455            storage,
456            storage_keys::DATA_DOC_META,
457            &doc_key(space, doc),
458            &bytes,
459        )
460        .map_err(|err| {
461            Error::Other(format!(
462                "failed to record the removal of {space}/{doc}: {err}"
463            ))
464        })
465    }
466
467    /// Write one document's removal record and then remember it, in that
468    /// order: what memory says must never be ahead of what a relaunch will
469    /// read.
470    fn set_doc_meta(
471        &mut self,
472        storage: &dyn ProtocolStateStorage,
473        space: &str,
474        doc: &str,
475        meta: DocMeta,
476    ) -> Result<()> {
477        self.persist_doc_meta(storage, space, doc, &meta)?;
478        self.data
479            .meta
480            .insert((space.to_string(), doc.to_string()), meta);
481        Ok(())
482    }
483
484    /// Whether this name currently stands removed.
485    ///
486    /// The space must already be loaded.
487    fn doc_is_removed(&self, space: &str, doc: &str) -> bool {
488        self.data
489            .meta
490            .get(&(space.to_string(), doc.to_string()))
491            .is_some_and(|meta| !meta.alive)
492    }
493
494    /// Let a removed name hold a document again, keeping its floor.
495    ///
496    /// The floor outlives the removal it recorded, because a replica that
497    /// never heard about the removal still holds the old contents and will
498    /// offer them back. Dropping the floor here would let exactly that
499    /// offer refill the new document with the old one.
500    fn revive_doc(
501        &mut self,
502        storage: &dyn ProtocolStateStorage,
503        space: &str,
504        doc: &str,
505    ) -> Result<()> {
506        let key = (space.to_string(), doc.to_string());
507        let Some(meta) = self.data.meta.get(&key) else {
508            return Ok(());
509        };
510        if meta.alive {
511            return Ok(());
512        }
513        let revived = DocMeta {
514            gone: meta.gone.clone(),
515            alive: true,
516        };
517        self.set_doc_meta(storage, space, doc, revived)
518    }
519
520    /// The removal floor recorded for one document, if it has one.
521    ///
522    /// The space must already be loaded; every caller reaches this through a
523    /// path that loads it.
524    fn doc_floor(&self, space: &str, doc: &str) -> Option<VersionToken> {
525        use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
526
527        let meta = self.data.meta.get(&(space.to_string(), doc.to_string()))?;
528        match BASE64.decode(&meta.gone) {
529            Ok(bytes) => Some(VersionToken::from_bytes(bytes)),
530            Err(err) => {
531                warn!(space, doc, error = %err, "Undecodable removal floor; treating the document as never removed");
532                None
533            }
534        }
535    }
536
537    fn persist_space(&mut self, storage: &dyn ProtocolStateStorage, space: &str) -> Result<()> {
538        let Some(record) = self.data.spaces.get(space) else {
539            return Ok(());
540        };
541        let bytes =
542            serde_json::to_vec(record).map_err(|err| Error::Serialization(err.to_string()))?;
543        self.write_state_record(storage, storage_keys::DATA_SPACES, space, &bytes)
544            .map_err(|err| Error::Other(format!("failed to persist data space index: {err}")))
545    }
546
547    /// Open a document, loading its compacted record and delta log.
548    ///
549    /// Applying the compacted record and then every delta is safe in any
550    /// order and safe to repeat: duplicates are absorbed by the merge, which
551    /// is exactly why a crash at any point during compaction leaves a
552    /// recoverable store.
553    ///
554    /// A removed name is refused rather than opened empty. Opening it would
555    /// create a live document out of a removal, and every write to it would
556    /// then be a change to something no replica holds. Bringing the name
557    /// back is a decision with a caller: `data_create_doc` locally, or a
558    /// peer's change from beyond the removal floor.
559    fn open_doc(
560        &mut self,
561        storage: &dyn ProtocolStateStorage,
562        space: &str,
563        doc: &str,
564    ) -> Result<()> {
565        if self
566            .data
567            .docs
568            .contains_key(&(space.to_string(), doc.to_string()))
569        {
570            return Ok(());
571        }
572        self.load_space(storage, space)?;
573        if self.doc_is_removed(space, doc) {
574            return Err(Error::InvalidState(format!(
575                "document {space}/{doc} was removed; create it again to reuse the name"
576            )));
577        }
578
579        let mut document = DataDoc::new();
580        let mut compacted_bytes = 0usize;
581        let mut history_complete = true;
582
583        match self.read_state_record_detailed(
584            storage,
585            storage_keys::DATA_DOCS,
586            &doc_key(space, doc),
587        ) {
588            Ok(StateRecord::Present(bytes)) => {
589                compacted_bytes = bytes.len();
590                // A snapshot is self-contained, so parking here should not be
591                // reachable. Recorded rather than assumed away: if it ever is,
592                // the document is missing its own base and compaction would
593                // write that gap over the delta log.
594                if document.import(&bytes).map_err(map_data_error)?.is_parked() {
595                    history_complete = false;
596                    warn!(space, doc, "Document snapshot did not fully apply");
597                }
598            }
599            // `Unavailable` means it is still on disk and may be fine, so
600            // refusing here keeps a transient failure from looking like data
601            // loss to the application.
602            Ok(StateRecord::Unavailable) => {
603                return Err(Error::Other(format!(
604                    "document {space}/{doc} could not be read this session"
605                )));
606            }
607            // `Unreadable` means the record was examined and is permanently
608            // gone; the delta log may still carry the document forward. The
609            // document has lost its base either way, so compaction stays off:
610            // folding now would write whatever partial state survived over the
611            // records that still hold the rest.
612            Ok(StateRecord::Unreadable) => {
613                history_complete = false;
614                warn!(space, doc, "Document snapshot is permanently unreadable");
615            }
616            Ok(StateRecord::Missing) => {}
617            Err(err) => {
618                return Err(Error::Other(format!("failed to read document: {err}")));
619            }
620        }
621
622        let prefix = format!("{space}/{doc}/");
623        let mut sequences: Vec<u64> = storage
624            .list_keys(storage_keys::DATA_DELTA_LOG)
625            .unwrap_or_default()
626            .iter()
627            .filter_map(|key| delta_seq(key, &prefix))
628            .collect();
629        sequences.sort_unstable();
630
631        let mut log_bytes = 0usize;
632        let mut next_seq = 0u64;
633        let mut replayed = 0u32;
634        for seq in sequences {
635            next_seq = next_seq.max(seq.saturating_add(1));
636            let key = delta_key(space, doc, seq);
637            match self.read_state_record_detailed(storage, storage_keys::DATA_DELTA_LOG, &key) {
638                Ok(StateRecord::Present(bytes)) => {
639                    log_bytes = log_bytes.saturating_add(bytes.len());
640                    replayed = replayed.saturating_add(1);
641                    match document.import(&bytes) {
642                        // The engine accepted the delta but cannot apply it:
643                        // its predecessor is missing, so it and everything
644                        // after it are invisible. Recording that is what stops
645                        // compaction from writing a snapshot without these
646                        // changes and then deleting the records holding them.
647                        Ok(outcome) if outcome.is_parked() => {
648                            history_complete = false;
649                            warn!(
650                                space,
651                                doc,
652                                seq,
653                                "Document delta is waiting on a predecessor that never \
654                                 arrived; compaction is disabled for this document"
655                            );
656                        }
657                        Ok(_) => {}
658                        Err(err) => {
659                            // A permanently undecodable delta is a hole. The
660                            // document still opens with everything that did
661                            // apply, but the log must not be folded away: F3
662                            // resend is what closes a hole, and compaction
663                            // would destroy what is left to resend against.
664                            history_complete = false;
665                            warn!(space, doc, seq, error = %err, "Skipping undecodable document delta");
666                        }
667                    }
668                }
669                // Still on disk and possibly perfectly good next launch.
670                // Refusing the open keeps a transient read failure from
671                // looking like data loss, exactly as the snapshot path above
672                // does, and keeps compaction away from records that
673                // everything after this delta depends on.
674                Ok(StateRecord::Unavailable) => {
675                    return Err(Error::Other(format!(
676                        "delta {seq} of document {space}/{doc} could not be read this session"
677                    )));
678                }
679                // Listed but gone or permanently unreadable: a hole, not a
680                // reason to refuse the document.
681                Ok(_) => {
682                    history_complete = false;
683                    warn!(space, doc, seq, "Document delta is missing or unreadable");
684                }
685                Err(err) => {
686                    return Err(Error::Other(format!(
687                        "failed to read delta {seq} of document {space}/{doc}: {err}"
688                    )));
689                }
690            }
691        }
692
693        document.mark_persisted();
694        if compacted_bytes == 0 {
695            compacted_bytes = document.measure().map_err(map_data_error)?;
696        }
697        // Hand the document what its own bookkeeping cannot know after a
698        // fresh open: how big the record it came from was, how much delta
699        // log sits on top, and how many commits that log represents. Skip
700        // this and a reopened document looks empty to the size check and to
701        // the compaction trigger, both of which then under-fire.
702        document.restore_bookkeeping(compacted_bytes, log_bytes, replayed);
703
704        if let Some(record) = self.data.spaces.get_mut(space) {
705            record.docs.insert(doc.to_string());
706        }
707        self.data.docs.insert(
708            (space.to_string(), doc.to_string()),
709            LoadedDoc {
710                doc: document,
711                next_seq,
712                log_bytes,
713                compacted_bytes,
714                over_cap: false,
715                history_complete,
716            },
717        );
718        Ok(())
719    }
720
721    /// Run an edit against an open document.
722    ///
723    /// `growth` marks operations that can make the document bigger. Those are
724    /// refused once the cap has been passed; deletions are not, so an
725    /// application that filled a document can still empty it.
726    fn with_doc<F>(&mut self, space: &str, doc: &str, growth: bool, edit: F) -> Result<()>
727    where
728        F: FnOnce(&mut DataDoc) -> std::result::Result<(), DataError>,
729    {
730        Self::validate_ids(space, doc)?;
731        let storage = self.require_data_storage()?;
732        self.open_doc(storage.as_ref(), space, doc)?;
733
734        let entry = self
735            .data
736            .docs
737            .get_mut(&(space.to_string(), doc.to_string()))
738            .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
739
740        if growth && entry.over_cap {
741            return Err(Error::DocTooLarge {
742                actual: entry.compacted_bytes,
743                limit: policy::MAX_DOC_BYTES,
744            });
745        }
746        edit(&mut entry.doc).map_err(map_data_error)
747    }
748
749    /// Read from an open document.
750    fn read_doc<F, T>(&mut self, space: &str, doc: &str, read: F) -> Result<T>
751    where
752        F: FnOnce(&DataDoc) -> std::result::Result<T, DataError>,
753    {
754        Self::validate_ids(space, doc)?;
755        let storage = self.require_data_storage()?;
756        self.open_doc(storage.as_ref(), space, doc)?;
757        let entry = self
758            .data
759            .docs
760            .get(&(space.to_string(), doc.to_string()))
761            .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
762        read(&entry.doc).map_err(map_data_error)
763    }
764
765    // ---- public surface ---------------------------------------------
766
767    /// Point documents at a backend the application supplies.
768    ///
769    /// Documents move; protocol secrets do not. Applied immediately, so a
770    /// store opened afterwards reads and writes through the new backend while
771    /// everything else stays where it is.
772    ///
773    /// Any document already open is written into the new backend as a
774    /// self-contained snapshot before this returns. That is not a
775    /// convenience, it is the whole correctness of a mid-session swap: a
776    /// delta record only describes the change *since* the previous one, so a
777    /// document that merely kept writing deltas into the new backend would
778    /// leave every earlier delta behind in the old one. The engine accepts
779    /// such an orphan delta and parks it, which means the document reads
780    /// **empty** from the new backend and the next compaction deletes the
781    /// orphans for good.
782    ///
783    /// If the migration cannot be written the swap does not happen at all and
784    /// the error is returned, because a half-swapped layer is precisely the
785    /// stranding this method exists to prevent.
786    ///
787    /// Documents that are not open are not migrated: they live wholly in the
788    /// old backend, which is what switching backends means.
789    pub fn set_data_storage(&mut self, storage: Arc<dyn ProtocolStateStorage>) -> Result<()> {
790        let previous = self.config.data.storage.take();
791        self.config.data.storage = Some(storage);
792
793        match self.migrate_open_docs_to_data_storage() {
794            Ok(()) => Ok(()),
795            Err(err) => {
796                self.config.data.storage = previous;
797                Err(err)
798            }
799        }
800    }
801
802    /// Write every open document into the current backend as a self-contained
803    /// snapshot, so nothing depends on history that lives somewhere else.
804    ///
805    /// Two phases, and the split is the correctness. Every write happens
806    /// first; only once all of them are durable does any document's
807    /// bookkeeping move. Interleaving the two would leave a document that
808    /// migrated before the failure claiming a fresh empty log, while the
809    /// caller rolls the swap back to the old backend: its next flush would
810    /// then write sequence zero over the delta already there and park
811    /// everything that followed it, which is permanent loss of exactly the
812    /// kind this migration exists to prevent.
813    fn migrate_open_docs_to_data_storage(&mut self) -> Result<()> {
814        let Some(storage) = self.data_storage() else {
815            return Ok(());
816        };
817
818        let keys: Vec<(String, String)> = self.data.docs.keys().cloned().collect();
819        let mut committed: Vec<(String, String)> = Vec::new();
820        let mut migrated: Vec<((String, String), usize)> = Vec::new();
821
822        if let Err(err) =
823            self.write_open_docs_into(storage.as_ref(), &keys, &mut committed, &mut migrated)
824        {
825            // Nothing durable is being claimed, so every export marker this
826            // advanced goes back. Leaving one advanced would drop the edits
827            // it covers from every future delta, which is the same silent
828            // loss a failed delta write already rewinds for.
829            for map_key in committed {
830                if let Some(entry) = self.data.docs.get_mut(&map_key) {
831                    entry.doc.rewind_last_commit();
832                }
833            }
834            return Err(err);
835        }
836
837        // Phase two. Every record above is durable, so the swap is going to
838        // stand and the bookkeeping can follow it.
839        for (map_key, compacted_bytes) in migrated {
840            if let Some(entry) = self.data.docs.get_mut(&map_key) {
841                // The new backend holds one snapshot and no delta log.
842                entry.next_seq = 0;
843                entry.log_bytes = 0;
844                entry.compacted_bytes = compacted_bytes;
845                entry.doc.mark_compacted(compacted_bytes);
846            }
847        }
848        Ok(())
849    }
850
851    /// Phase one of a backend swap: every open document and space index
852    /// written into the new backend, with nothing in memory moved yet.
853    ///
854    /// `committed` collects the documents whose export marker this advanced,
855    /// so a failure can put every one of them back. `migrated` collects the
856    /// bookkeeping to apply once every write has succeeded.
857    fn write_open_docs_into(
858        &mut self,
859        storage: &dyn ProtocolStateStorage,
860        keys: &[(String, String)],
861        committed: &mut Vec<(String, String)>,
862        migrated: &mut Vec<((String, String), usize)>,
863    ) -> Result<()> {
864        for (space, doc) in keys {
865            let map_key = (space.clone(), doc.clone());
866
867            // Fold pending edits in first: the snapshot below has to be the
868            // whole document, including what has not been flushed yet.
869            {
870                let Some(entry) = self.data.docs.get_mut(&map_key) else {
871                    continue;
872                };
873                if entry.doc.commit().map_err(map_data_error)?.is_some() {
874                    committed.push(map_key.clone());
875                }
876            }
877            let compacted = {
878                let Some(entry) = self.data.docs.get(&map_key) else {
879                    continue;
880                };
881                entry.doc.export_compacted().map_err(map_data_error)?
882            };
883
884            self.write_state_record(
885                storage,
886                storage_keys::DATA_DOCS,
887                &doc_key(space, doc),
888                &compacted,
889            )
890            .map_err(|err| {
891                Error::Other(format!(
892                    "failed to move document {space}/{doc} into the new backend: {err}"
893                ))
894            })?;
895
896            // Snapshot first, then drop the log, in that order and for the
897            // same reason as compaction. A delta record already sitting under
898            // this name in the new backend belongs to some earlier document
899            // that held it: the snapshot just written does not descend from
900            // it, so at the next open it parks and switches compaction off
901            // for good. Best-effort, again as compaction: the snapshot is
902            // already durable, and a leftover costs a growing log rather than
903            // a wrong document.
904            let prefix = format!("{space}/{doc}/");
905            if let Ok(stale) = storage.list_keys(storage_keys::DATA_DELTA_LOG) {
906                for key in stale.iter().filter(|key| key.starts_with(&prefix)) {
907                    let _ = storage.delete(storage_keys::DATA_DELTA_LOG, key);
908                }
909            }
910
911            migrated.push((map_key, compacted.len()));
912        }
913
914        // The space indexes carry document names that have no records of
915        // their own yet; without them the new backend forgets those names.
916        let spaces: Vec<String> = self.data.spaces.keys().cloned().collect();
917        for space in spaces {
918            self.persist_space(storage, &space)?;
919        }
920
921        // Removal records have no document to carry them, so a backend swap
922        // that left them behind would move a space into a store where every
923        // removed name reads as one nobody ever created, and the next offer
924        // from any peer would refill all of them.
925        let removals: Vec<((String, String), DocMeta)> = self
926            .data
927            .meta
928            .iter()
929            .map(|(key, meta)| (key.clone(), meta.clone()))
930            .collect();
931        for ((space, doc), meta) in removals {
932            self.persist_doc_meta(storage, &space, &doc, &meta)?;
933        }
934        Ok(())
935    }
936
937    /// Create a document, or do nothing if it already exists.
938    ///
939    /// A name that was removed comes back empty, keeping its removal floor,
940    /// so the old contents cannot arrive from a replica that never heard
941    /// about the removal.
942    pub fn data_create_doc(&mut self, space: &str, doc: &str) -> Result<()> {
943        Self::validate_ids(space, doc)?;
944        let storage = self.require_data_storage()?;
945        self.load_space(storage.as_ref(), space)?;
946        self.revive_doc(storage.as_ref(), space, doc)?;
947        self.open_doc(storage.as_ref(), space, doc)?;
948        self.persist_space(storage.as_ref(), space)
949    }
950
951    // ---- replication seams ------------------------------------------
952    //
953    // The sync module owns the frames and the peer bookkeeping; these are
954    // the four things it needs from the store, and they are here rather
955    // than there so nothing outside this module touches a document handle.
956
957    /// The backend documents live in, when the layer is on.
958    ///
959    /// Unlike `require_data_storage` this answers `None` rather than an
960    /// error: the sync path treats a switched-off layer as "nothing to do",
961    /// not as a failure to report.
962    pub(crate) fn data_storage_for_sync(&self) -> Option<Arc<dyn ProtocolStateStorage>> {
963        if !self.config.data.enabled {
964            return None;
965        }
966        self.data_storage()
967    }
968
969    /// Every document in a space with the version we hold of it.
970    ///
971    /// A document whose version cannot be read is left out rather than
972    /// failing the whole space. Propagating the error costs every other
973    /// document in the space its replication, and does it silently: one
974    /// unreadable record on either leg would leave the offer empty and both
975    /// replicas believing they had nothing to say to each other. Leaving it
976    /// out instead means the peer reads that document as one we have never
977    /// seen and offers it back, which is the recovery this layer already
978    /// has, and the rest of the space keeps converging meanwhile.
979    pub(crate) fn data_sync_versions(&mut self, space: &str) -> Result<BTreeMap<String, String>> {
980        let docs = self.data_list_docs(space)?;
981        let mut versions = BTreeMap::new();
982        for doc in docs {
983            match self.data_doc_version(space, &doc) {
984                Ok(encoded) => {
985                    versions.insert(doc, encoded);
986                }
987                Err(err) => {
988                    warn!(space, doc, error = %err, "Leaving a document out of the version offer: its version cannot be read");
989                }
990            }
991        }
992        Ok(versions)
993    }
994
995    /// Every removal this space knows about, with the floor it recorded.
996    ///
997    /// Names that came back are included, because the floor still describes
998    /// content that was removed: a third replica that never heard about the
999    /// removal is holding exactly that content, and this is what tells it
1000    /// to drop it.
1001    pub(crate) fn data_sync_tombstones(&mut self, space: &str) -> Result<BTreeMap<String, String>> {
1002        let Some(storage) = self.data_storage_for_sync() else {
1003            return Ok(BTreeMap::new());
1004        };
1005        offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
1006        self.load_space(storage.as_ref(), space)?;
1007        Ok(self
1008            .data
1009            .meta
1010            .range((space.to_string(), String::new())..)
1011            .take_while(|((known, _), _)| known == space)
1012            .map(|((_, doc), meta)| (doc.clone(), meta.gone.clone()))
1013            .collect())
1014    }
1015
1016    /// Apply a removal a peer told us about.
1017    ///
1018    /// The floor decides, never the message: a copy the floor covers is
1019    /// removed, and a copy carrying anything beyond it is kept, because that
1020    /// is an edit the removal never saw. Either way the floor is recorded,
1021    /// including for a name this device has never held, so a third replica
1022    /// still holding the removed content cannot seed it back later.
1023    ///
1024    /// A floor this device already holds, or one a held floor covers, is
1025    /// answered from memory with no write and no open. Floors are carried on
1026    /// every offer for the life of the space, so that is the common case,
1027    /// and it has to be free for two reasons. The obvious one is cost: one
1028    /// sealed record per removed name per exchange, from every peer. The
1029    /// other is a name that was brought back on purpose and not yet written
1030    /// to: every version covers the empty one, so a floor that was already
1031    /// judged would read the fresh document as removed content and delete
1032    /// it. A floor decides once, when it first arrives.
1033    pub(crate) fn data_apply_tombstone(
1034        &mut self,
1035        space: &str,
1036        doc: &str,
1037        floor: &VersionToken,
1038    ) -> Result<TombstoneOutcome> {
1039        use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
1040
1041        Self::validate_ids(space, doc)?;
1042        // Checked once on the way in. A floor that does not decode is stored
1043        // for the life of the space and covers nothing, which costs a record
1044        // for no removal at all.
1045        floor.validate().map_err(map_data_error)?;
1046        let storage = self.require_data_storage()?;
1047        self.load_space(storage.as_ref(), space)?;
1048
1049        let held = self
1050            .data
1051            .spaces
1052            .get(space)
1053            .is_some_and(|record| record.docs.contains(doc));
1054
1055        let merged = match self.doc_floor(space, doc) {
1056            Some(existing) if existing.includes(floor).map_err(map_data_error)? => {
1057                return Ok(TombstoneOutcome::Unchanged);
1058            }
1059            Some(existing) => existing.union(floor).map_err(map_data_error)?,
1060            None => floor.clone(),
1061        };
1062        // The union only ever grows, and a peer can grow it by naming a
1063        // fresh replica in every offer. Bounded here rather than at the
1064        // frame, because the frame bounds one floor and this bounds the
1065        // sum of every floor ever merged into this name.
1066        if merged.as_bytes().len() > MAX_REMOVAL_FLOOR_BYTES {
1067            return Err(Error::Other(format!(
1068                "removal floor for {space}/{doc} would exceed {MAX_REMOVAL_FLOOR_BYTES} bytes"
1069            )));
1070        }
1071        let encoded = BASE64.encode(merged.as_bytes());
1072
1073        if !held {
1074            let alive = self
1075                .data
1076                .meta
1077                .get(&(space.to_string(), doc.to_string()))
1078                .is_some_and(|meta| meta.alive);
1079            self.set_doc_meta(
1080                storage.as_ref(),
1081                space,
1082                doc,
1083                DocMeta {
1084                    gone: encoded,
1085                    alive,
1086                },
1087            )?;
1088            return Ok(TombstoneOutcome::Recorded);
1089        }
1090
1091        // Pending edits count as ours and are flushed before the comparison,
1092        // or an edit made a moment ago would read as content the removal
1093        // covered and be deleted by it.
1094        if let Err(err) = self.data_flush(space, doc) {
1095            debug!(space, doc, error = %err, "Could not flush before judging a removal; judging the last durable version");
1096        }
1097        let ours =
1098            VersionToken::from_bytes(BASE64.decode(self.data_doc_version(space, doc)?).map_err(
1099                |err| Error::Serialization(format!("undecodable document version: {err}")),
1100            )?);
1101
1102        if !merged.includes(&ours).map_err(map_data_error)? {
1103            // Beyond the floor: an edit the removal never saw. The document
1104            // stays, the floor is kept, and the peer gets the edit back
1105            // through the ordinary exchange.
1106            self.set_doc_meta(
1107                storage.as_ref(),
1108                space,
1109                doc,
1110                DocMeta {
1111                    gone: encoded,
1112                    alive: true,
1113                },
1114            )?;
1115            return Ok(TombstoneOutcome::Kept);
1116        }
1117
1118        self.set_doc_meta(
1119            storage.as_ref(),
1120            space,
1121            doc,
1122            DocMeta {
1123                gone: encoded,
1124                alive: false,
1125            },
1126        )?;
1127
1128        self.data.docs.remove(&(space.to_string(), doc.to_string()));
1129        let first_error = self.purge_doc_records(storage.as_ref(), space, doc, None);
1130        if let Some(record) = self.data.spaces.get_mut(space) {
1131            record.docs.remove(doc);
1132        }
1133        self.persist_space(storage.as_ref(), space)?;
1134        if let Some(detail) = first_error {
1135            warn!(
1136                space,
1137                doc,
1138                detail,
1139                "Removed a document a peer removed, but not every record could be deleted"
1140            );
1141        }
1142
1143        self.emit_event(Event::DataDocRemoved {
1144            space_id: space.to_string(),
1145            doc_id: doc.to_string(),
1146            by: DocRemovedBy::Peer,
1147        });
1148        Ok(TombstoneOutcome::Deleted)
1149    }
1150
1151    /// The documents this device wants from its peers in one space.
1152    ///
1153    /// `patterns` is a list of document names, each of which MAY end in `*`
1154    /// to match a prefix. `["*"]` is everything and is the default; `[]` is
1155    /// nothing. The document charset excludes `*`, so the wildcard can never
1156    /// be part of a name it is matching.
1157    ///
1158    /// Two halves, and only one of them is a request. Toward a peer this is
1159    /// carried in every version offer, so the peer sends nothing else: that
1160    /// is the traffic saving, and it depends on the peer reading the field.
1161    /// Locally it is a refusal, so a document outside it is never created
1162    /// from an offer and never imported however it arrives. The refusal is
1163    /// what makes this safe against a peer that ignores the request, which
1164    /// includes every peer on a build that predates it.
1165    ///
1166    /// Narrowing does not delete what is already held. A document that falls
1167    /// outside the new patterns stops being updated and stays where it is,
1168    /// because deleting on a policy change would make a narrowing lose data
1169    /// no caller asked to lose. `data_delete_doc` is how it leaves.
1170    ///
1171    /// Widening asks: the newly wanted documents are absent from this
1172    /// device's next offer and inside its declared want, so the peer answers
1173    /// with them.
1174    ///
1175    /// The question that asks is skipped in the two cases where it would
1176    /// cost something and buy nothing, because both are the ordinary way an
1177    /// application calls this. Declaring the same patterns again asks for
1178    /// nothing new: the offer would carry exactly what the last one did.
1179    /// And declaring anything at all before `start()`, which is the
1180    /// documented order, cannot reach a peer: the transports are not up, so
1181    /// the frame is dropped for want of a session, and the offer path has
1182    /// already stamped the window that suppresses the next sweep. That stamp
1183    /// would then suppress the start-up sweep and the first rediscovery,
1184    /// delaying the exchange this call exists to bring forward by the length
1185    /// of the window. Both paths already carry the new interest: the sweep
1186    /// in `start` and every rediscovery build their offer from it.
1187    pub fn data_set_interest(&mut self, space: &str, patterns: Vec<String>) -> Result<()> {
1188        offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
1189        // The same door every other `DataStore` method goes through. Without
1190        // it this is the one call that reports success with the layer off,
1191        // and an application would read that as a narrowing it never got.
1192        self.require_data_storage()?;
1193        if patterns.len() > MAX_INTEREST_PATTERNS {
1194            return Err(Error::InvalidArgument(format!(
1195                "at most {MAX_INTEREST_PATTERNS} interest patterns per space, got {}",
1196                patterns.len()
1197            )));
1198        }
1199        for pattern in &patterns {
1200            validate_interest_pattern(pattern)?;
1201        }
1202        let previous = self.data.interest.get(space).cloned();
1203        let everything = patterns.len() == 1 && patterns[0] == "*";
1204        if everything {
1205            // The default, expressed explicitly. Held as absence so the wire
1206            // and every read below take the cheap path.
1207            self.data.interest.remove(space);
1208        } else {
1209            self.data.interest.insert(space.to_string(), patterns);
1210        }
1211        // Both guards are about the same thing: an offer that would carry
1212        // nothing new, or could not be carried at all, must not spend the
1213        // window that suppresses the offer which can. See the note above.
1214        if previous.as_ref() != self.data.interest.get(space) && self.protocol_is_running() {
1215            self.nudge_data_sync(space, None, "interest_changed");
1216        }
1217        Ok(())
1218    }
1219
1220    /// The patterns declared for a space, or `None` for the default.
1221    pub(crate) fn data_interest(&self, space: &str) -> Option<&Vec<String>> {
1222        self.data.interest.get(space)
1223    }
1224
1225    /// Whether this device wants `doc` in `space`.
1226    pub(crate) fn data_wants(&self, space: &str, doc: &str) -> bool {
1227        match self.data.interest.get(space) {
1228            None => true,
1229            Some(patterns) => patterns
1230                .iter()
1231                .any(|pattern| interest_matches(pattern, doc)),
1232        }
1233    }
1234
1235    /// Let a removed name hold a document again, because something beyond
1236    /// its floor is about to be imported into it.
1237    ///
1238    /// Separate from the refusal, and called only after it: content beyond a
1239    /// removal floor is an edit the removal never saw, and an edit beats a
1240    /// removal. The name comes back keeping its floor, so the *rest* of the
1241    /// removed content still cannot arrive from a replica that has not heard
1242    /// about the removal.
1243    pub(crate) fn data_revive_removed_doc(&mut self, space: &str, doc: &str) -> Result<()> {
1244        let storage = self.require_data_storage()?;
1245        self.load_space(storage.as_ref(), space)?;
1246        self.revive_doc(storage.as_ref(), space, doc)
1247    }
1248
1249    /// The removal floor for a document, with its space loaded first.
1250    ///
1251    /// [`Self::doc_floor`] answers off memory and needs the space loaded.
1252    /// The sync paths below can be the first thing to touch a space after
1253    /// a launch, on the media road in particular, and a floor missed there
1254    /// is a removed document refilled.
1255    fn loaded_doc_floor(&mut self, space: &str, doc: &str) -> Option<VersionToken> {
1256        let storage = self.data_storage_for_sync()?;
1257        self.load_space(storage.as_ref(), space).ok()?;
1258        self.doc_floor(space, doc)
1259    }
1260
1261    /// Whether a document has a removal floor at all.
1262    ///
1263    /// What decides whether a blob for it has to be judged against one.
1264    /// That judgement is an engine decode and has to run inside an
1265    /// in-flight marker, so a name with no floor, which is every name in a
1266    /// space nobody has removed anything from, skips the marker and the
1267    /// decode both.
1268    pub(crate) fn data_doc_has_floor(&mut self, space: &str, doc: &str) -> bool {
1269        self.loaded_doc_floor(space, doc).is_some()
1270    }
1271
1272    /// Whether a version a peer offers is content a removal already covered.
1273    ///
1274    /// Cheap and answered from memory: the overwhelming case is a space with
1275    /// no removals at all, where there is nothing to decode.
1276    pub(crate) fn data_version_below_floor(
1277        &mut self,
1278        space: &str,
1279        doc: &str,
1280        theirs: &VersionToken,
1281    ) -> bool {
1282        let Some(floor) = self.loaded_doc_floor(space, doc) else {
1283            return false;
1284        };
1285        floor.includes(theirs).unwrap_or(false)
1286    }
1287
1288    /// Whether a blob carries nothing beyond a removal floor.
1289    ///
1290    /// An engine decode, not a header read: the blob's changes are decoded
1291    /// into a scratch document to find the version they end at, with the
1292    /// same decoder an import runs. The caller owes it the containment an
1293    /// import gets, an in-flight marker written before and cleared after;
1294    /// see [`offline_protocol_data::blob_end_version`]. A document with no
1295    /// floor skips the decode entirely, which is every document in a space
1296    /// nobody has removed anything from.
1297    pub(crate) fn data_blob_below_floor(&mut self, space: &str, doc: &str, blob: &[u8]) -> bool {
1298        let Some(floor) = self.loaded_doc_floor(space, doc) else {
1299            return false;
1300        };
1301        match offline_protocol_data::blob_end_version(blob) {
1302            Ok(end) => floor.includes(&end).unwrap_or(false),
1303            Err(err) => {
1304                // Undecodable bytes are refused a few steps later by the
1305                // import itself, which reports properly. Not below the
1306                // floor, because nothing here established that.
1307                debug!(space, doc, error = %err, "Could not read a blob's version to judge it against a removal");
1308                false
1309            }
1310        }
1311    }
1312
1313    /// The version we hold of one document, encoded as a frame carries it.
1314    pub(crate) fn data_doc_version(&mut self, space: &str, doc: &str) -> Result<String> {
1315        use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
1316
1317        let token = self.read_doc(space, doc, |document| Ok(document.version()))?;
1318        Ok(BASE64.encode(token.as_bytes()))
1319    }
1320
1321    /// The names a space holds, or `None` if there is nothing to read.
1322    ///
1323    /// Answered off the space index, which `load_space` has already
1324    /// reconciled against the records themselves and cached. Opening a
1325    /// document to find out would defeat both callers, whose whole question
1326    /// is whether opening one is allowed.
1327    fn space_docs(&mut self, space: &str) -> Option<&BTreeSet<String>> {
1328        if offline_protocol_data::validate_space_name(space).is_err() {
1329            return None;
1330        }
1331        let storage = self.data_storage_for_sync()?;
1332        self.load_space(storage.as_ref(), space).ok()?;
1333        self.data.spaces.get(space).map(|record| &record.docs)
1334    }
1335
1336    /// Whether `space` already holds `doc`.
1337    pub(crate) fn data_holds_doc(&mut self, space: &str, doc: &str) -> bool {
1338        self.space_docs(space)
1339            .is_some_and(|docs| docs.contains(doc))
1340    }
1341
1342    /// Whether a document a peer named may be stored in `space`.
1343    ///
1344    /// One already held always may; a new one only while the space is under
1345    /// `cap`. The cap is a parameter rather than a constant here because it
1346    /// bounds what a *peer* can talk this device into storing, which is a
1347    /// replication policy: an application creating its own documents is not
1348    /// subject to it.
1349    ///
1350    /// A space that cannot be read admits, rather than refusing on a storage
1351    /// hiccup: whatever follows fails on the same storage and reports it
1352    /// where the failure actually is.
1353    /// Removed names count against the cap alongside held ones. Each one is
1354    /// a record a peer talked this device into storing, and the record
1355    /// outlives the document by design, so counting only what is held would
1356    /// let a peer name a fresh thousand documents after every removal
1357    /// sweep.
1358    ///
1359    /// A name already known here, held or removed, always admits: nothing a
1360    /// peer says about it costs a record that does not already exist. That
1361    /// matters at the ceiling, where a space stays for good once it gets
1362    /// there, because floors never expire. Refusing a known name there
1363    /// would refuse the floor a peer re-sends on every offer, and a removal
1364    /// for a held document sorted after it.
1365    pub(crate) fn data_space_admits_doc(&mut self, space: &str, doc: &str, cap: usize) -> bool {
1366        // Called once per name in an inbound frame, so it reads both fields
1367        // in place rather than cloning the index: a peer may send offers as
1368        // fast as the link carries them, and this is one of the things each
1369        // one pays for.
1370        if self.space_docs(space).is_none() {
1371            return true;
1372        }
1373        let Some(record) = self.data.spaces.get(space) else {
1374            return true;
1375        };
1376        if record.docs.contains(doc)
1377            || self
1378                .data
1379                .meta
1380                .contains_key(&(space.to_string(), doc.to_string()))
1381        {
1382            return true;
1383        }
1384        let removed = self
1385            .data
1386            .meta
1387            .range((space.to_string(), String::new())..)
1388            .take_while(|((known, _), _)| known == space)
1389            .filter(|((_, name), _)| !record.docs.contains(name.as_str()))
1390            .count();
1391        record.docs.len() + removed < cap
1392    }
1393
1394    /// What a replica at `theirs` is missing from a document.
1395    pub(crate) fn data_catch_up(
1396        &mut self,
1397        space: &str,
1398        doc: &str,
1399        theirs: &VersionToken,
1400    ) -> Result<CatchUp> {
1401        self.read_doc(space, doc, |document| document.export_since(theirs))
1402    }
1403
1404    /// A self-contained encoding of a document, for a peer no run of
1405    /// changes can catch up.
1406    pub(crate) fn data_export_snapshot(&mut self, space: &str, doc: &str) -> Result<Vec<u8>> {
1407        self.read_doc(space, doc, |document| document.export_raw())
1408    }
1409
1410    /// Apply a blob that arrived from a peer, and persist what it changed.
1411    ///
1412    /// The import goes through the engine's remote path, which judges the
1413    /// blob before touching it. A change that applies is flushed here rather
1414    /// than left in memory: an unflushed remote change would be lost to a
1415    /// restart, and the sender has already been told (by the acknowledgement
1416    /// the frame rides on) that it arrived.
1417    ///
1418    /// `Err` means the change is not durable. A document held over its cap
1419    /// still answers `Applied`, because the cap refuses growth *after* the
1420    /// change is on disk; the caller cannot tell the two apart from an error
1421    /// alone, and one of them is a change it must not report as refused.
1422    pub(crate) fn data_apply_remote(
1423        &mut self,
1424        space: &str,
1425        doc: &str,
1426        blob: &[u8],
1427    ) -> Result<RemoteImport> {
1428        Self::validate_ids(space, doc)?;
1429        let storage = self.require_data_storage()?;
1430        self.open_doc(storage.as_ref(), space, doc)?;
1431
1432        let map_key = (space.to_string(), doc.to_string());
1433        let outcome = {
1434            let entry = self
1435                .data
1436                .docs
1437                .get_mut(&map_key)
1438                .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
1439            let outcome = entry.doc.import_remote(blob).map_err(map_data_error)?;
1440            // A parked change is invisible to the engine's own version, so
1441            // nothing downstream may fold history while one is outstanding:
1442            // compaction would write a snapshot without it and then delete
1443            // the records it is waiting for.
1444            //
1445            // It stays false for the rest of this document's time in memory,
1446            // deliberately. The engine reports what one import left pending,
1447            // never whether a document still holds anything parked, so a
1448            // later import that applies cleanly is not evidence that the
1449            // earlier gap closed. Clearing it on that evidence would re-arm
1450            // compaction while a change is still waiting, which is the loss
1451            // this flag exists to prevent; leaving it set costs a document
1452            // its compaction until it is next opened, where the flag is
1453            // recomputed from the records themselves.
1454            if outcome == RemoteImport::Parked {
1455                entry.history_complete = false;
1456            }
1457            outcome
1458        };
1459
1460        if outcome == RemoteImport::Applied {
1461            // Remote work is not this replica's to push onward, so the flush
1462            // is told where the change came from.
1463            //
1464            // `DocTooLarge` is not an import failure and is not reported as
1465            // one. It is raised after the delta record was written and the
1466            // push decided, so the imported change is already durable; what
1467            // failed is the size verdict that follows, and what it refuses is
1468            // further *growth*. Propagating it would say the change was
1469            // refused when it applied, skip the space record below, and cost
1470            // the caller the one signal that a local edit may have been
1471            // folded into this import and suppressed with it, because that
1472            // compensation is gated on hearing `Applied`. A document over its
1473            // cap is exactly where that matters: the pending edit it still
1474            // accepts is a deletion, which is the route back under.
1475            match self.flush_doc_from(storage.as_ref(), space, doc, Some(space)) {
1476                Ok(()) => {}
1477                Err(Error::DocTooLarge { .. }) => {
1478                    debug!(
1479                        space,
1480                        doc, "Applied a remote change into a document that is over its cap"
1481                    );
1482                }
1483                Err(err) => return Err(err),
1484            }
1485            self.persist_space(storage.as_ref(), space)?;
1486        }
1487        Ok(outcome)
1488    }
1489
1490    /// Drop this device's copy of a document, leaving the peers' copies
1491    /// alone.
1492    ///
1493    /// Eviction, not removal: it reclaims the space a document occupies here
1494    /// and records nothing about the name, so the next version exchange with
1495    /// a replica that still holds it recreates and refills it. That is the
1496    /// right call for a cache and the wrong one for content somebody wants
1497    /// gone from the room, which is what [`Self::data_remove_doc`] is for.
1498    pub fn data_delete_doc(&mut self, space: &str, doc: &str) -> Result<()> {
1499        Self::validate_ids(space, doc)?;
1500        let storage = self.require_data_storage()?;
1501        self.load_space(storage.as_ref(), space)?;
1502
1503        self.data.docs.remove(&(space.to_string(), doc.to_string()));
1504        let first_error = self.purge_doc_records(storage.as_ref(), space, doc, None);
1505        if let Some(record) = self.data.spaces.get_mut(space) {
1506            record.docs.remove(doc);
1507        }
1508        self.persist_space(storage.as_ref(), space)?;
1509
1510        match first_error {
1511            Some(detail) => Err(Error::Other(format!(
1512                "failed to delete every record of document {space}/{doc}: {detail}"
1513            ))),
1514            None => Ok(()),
1515        }
1516    }
1517
1518    /// Remove a document from every replica of its space.
1519    ///
1520    /// Records the version the document stood at as its removal floor, then
1521    /// deletes it here and tells the space. A replica whose copy the floor
1522    /// covers deletes it too; a replica holding an edit the floor does not
1523    /// cover keeps its copy and hands the document back, contents and all,
1524    /// because that edit's history *is* those contents. An edit therefore
1525    /// beats a removal, whichever happened first, and the pair never
1526    /// produces a half-emptied document.
1527    ///
1528    /// Pending edits here are flushed first so the floor covers them:
1529    /// removing a document must not leave this device's own unsent work
1530    /// looking like somebody else's concurrent edit.
1531    ///
1532    /// Removing a name this device does not hold does nothing. There is no
1533    /// version to take a floor from, and an empty floor would name no
1534    /// content at all.
1535    ///
1536    /// Re-using the name afterwards starts a document whose history is
1537    /// disjoint from the floor. What it does not do is fence the old
1538    /// contents off: a replica that edited them concurrently keeps them and
1539    /// the ordinary exchange merges them into the new document, and a
1540    /// replica that never learned of the removal merges the new contents
1541    /// into its old copy. A name is cheap; a fresh one is the safe choice.
1542    pub fn data_remove_doc(&mut self, space: &str, doc: &str) -> Result<()> {
1543        Self::validate_ids(space, doc)?;
1544        let storage = self.require_data_storage()?;
1545        self.load_space(storage.as_ref(), space)?;
1546        if !self.removable(space, doc) {
1547            return Ok(());
1548        }
1549
1550        // Errors here are logged and stepped over, exactly as the import
1551        // path's pre-flush does. A document over its cap fails every flush
1552        // and must still be removable; refusing would leave the only way out
1553        // of the cap closed. The floor is taken from the version held in
1554        // memory either way, which already includes what the flush could
1555        // not write.
1556        if let Err(err) = self.data_flush(space, doc) {
1557            debug!(space, doc, error = %err, "Could not flush before removing; the floor is taken from the version held in memory");
1558        }
1559
1560        let first_error = self.record_removal(storage.as_ref(), space, doc, None)?;
1561        self.persist_space(storage.as_ref(), space)?;
1562        self.nudge_data_sync(space, None, "document_removed");
1563
1564        match first_error {
1565            Some(detail) => Err(Error::Other(format!(
1566                "document {space}/{doc} is removed, but not every record could be deleted: \
1567                 {detail}"
1568            ))),
1569            None => Ok(()),
1570        }
1571    }
1572
1573    /// Whether a removal has anything to act on: the document is held here
1574    /// and does not already stand removed.
1575    ///
1576    /// Removing a name this device does not hold does nothing. There is no
1577    /// version to take a floor from, and an empty floor would name no
1578    /// content at all. The space must already be loaded.
1579    fn removable(&self, space: &str, doc: &str) -> bool {
1580        if self.doc_is_removed(space, doc) {
1581            return false;
1582        }
1583        let held = self
1584            .data
1585            .spaces
1586            .get(space)
1587            .is_some_and(|record| record.docs.contains(doc));
1588        if !held {
1589            debug!(
1590                space,
1591                doc, "Nothing to remove: this device does not hold the document"
1592            );
1593        }
1594        held
1595    }
1596
1597    /// Record a document's removal floor, drop the copy held here and purge
1598    /// its records, reporting the first record that would not delete.
1599    ///
1600    /// What one removal writes, without what a batch of them shares: the
1601    /// space index is not persisted here and the space is not told, so a
1602    /// caller removing every document in a space does each once at the end
1603    /// rather than once per document.
1604    ///
1605    /// The floor is written before the records it removes. A crash in
1606    /// between leaves a name every reader already treats as removed, which
1607    /// the next open finishes; the reverse order leaves records that vote
1608    /// the document back into existence.
1609    ///
1610    /// `delta_keys` is the delta log's listing when the caller already has
1611    /// one. It has to postdate the document's last flush, or the delta that
1612    /// flush wrote is missed by the purge and replayed into the next
1613    /// document of the same name.
1614    fn record_removal(
1615        &mut self,
1616        storage: &dyn ProtocolStateStorage,
1617        space: &str,
1618        doc: &str,
1619        delta_keys: Option<&[String]>,
1620    ) -> Result<Option<String>> {
1621        let floor = self.data_doc_version(space, doc)?;
1622        let gone = match self.doc_floor(space, doc) {
1623            // Two removals of one name, ours and one we learned from a
1624            // peer, each cover changes the other does not.
1625            Some(existing) => {
1626                use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
1627                let taken = VersionToken::from_bytes(BASE64.decode(&floor).map_err(|err| {
1628                    Error::Serialization(format!("undecodable document version: {err}"))
1629                })?);
1630                BASE64.encode(existing.union(&taken).map_err(map_data_error)?.as_bytes())
1631            }
1632            None => floor,
1633        };
1634        self.set_doc_meta(storage, space, doc, DocMeta { gone, alive: false })?;
1635
1636        self.data.docs.remove(&(space.to_string(), doc.to_string()));
1637        let first_error = self.purge_doc_records(storage, space, doc, delta_keys);
1638        if let Some(record) = self.data.spaces.get_mut(space) {
1639            record.docs.remove(doc);
1640        }
1641
1642        self.emit_event(Event::DataDocRemoved {
1643            space_id: space.to_string(),
1644            doc_id: doc.to_string(),
1645            by: DocRemovedBy::Local,
1646        });
1647        Ok(first_error)
1648    }
1649
1650    /// Remove every document a space holds, from every replica of it.
1651    ///
1652    /// Each document is attempted even after one fails, and the first error
1653    /// is what the caller hears. What every removal shares is done once for
1654    /// the lot: the delta log is listed once, the space index is written
1655    /// once, and the space is told once, with one offer carrying every
1656    /// removal. Per document that is two full listings and one sealed index
1657    /// write fewer, which on a custom backend is three round trips under the
1658    /// engine lock, for each of up to a thousand names.
1659    pub fn data_remove_space(&mut self, space: &str) -> Result<()> {
1660        offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
1661        let storage = self.require_data_storage()?;
1662        self.load_space(storage.as_ref(), space)?;
1663        let docs: Vec<String> = self
1664            .data_list_docs(space)?
1665            .into_iter()
1666            .filter(|doc| self.removable(space, doc))
1667            .collect();
1668        if docs.is_empty() {
1669            return Ok(());
1670        }
1671
1672        // Every document is flushed before the delta log is listed, and
1673        // nothing writes to the log between the listing and the purges. A
1674        // flush after the listing would write a delta the listing does not
1675        // name, the purge would leave it behind, and the next document under
1676        // that name would replay it: the removed contents, back.
1677        for doc in &docs {
1678            if let Err(err) = self.data_flush(space, doc) {
1679                debug!(space, doc, error = %err, "Could not flush before removing; the floor is taken from the version held in memory");
1680            }
1681        }
1682        let delta_keys = match storage.list_keys(storage_keys::DATA_DELTA_LOG) {
1683            Ok(keys) => Some(keys),
1684            Err(err) => {
1685                // Each removal lists for itself, as a single one does.
1686                warn!(space, error = %err, "Could not list the delta log once for the space; listing per document");
1687                None
1688            }
1689        };
1690
1691        let mut first_error: Option<Error> = None;
1692        let mut note = |err: Error| {
1693            if first_error.is_none() {
1694                first_error = Some(err);
1695            }
1696        };
1697        for doc in &docs {
1698            match self.record_removal(storage.as_ref(), space, doc, delta_keys.as_deref()) {
1699                Ok(None) => {}
1700                Ok(Some(detail)) => note(Error::Other(format!(
1701                    "document {space}/{doc} is removed, but not every record could be deleted: \
1702                     {detail}"
1703                ))),
1704                Err(err) => note(err),
1705            }
1706        }
1707
1708        // The index once, at the end. A crash before it leaves an index
1709        // that still lists the removed names, and the next open reads the
1710        // removal records after it and drops them: the index is a cache,
1711        // and the removal record is what decides.
1712        if let Err(err) = self.persist_space(storage.as_ref(), space) {
1713            note(err);
1714        }
1715        // Told regardless of errors. An error past a removal record is
1716        // still a removal, and one before it costs at worst an offer that
1717        // says nothing new.
1718        self.nudge_data_sync(space, None, "space_removed");
1719
1720        match first_error {
1721            Some(err) => Err(err),
1722            None => Ok(()),
1723        }
1724    }
1725
1726    /// The documents in a space.
1727    pub fn data_list_docs(&mut self, space: &str) -> Result<Vec<String>> {
1728        offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
1729        let storage = self.require_data_storage()?;
1730        self.load_space(storage.as_ref(), space)?;
1731        Ok(self
1732            .data
1733            .spaces
1734            .get(space)
1735            .map(|record| record.docs.iter().cloned().collect())
1736            .unwrap_or_default())
1737    }
1738
1739    /// Every space the store knows of.
1740    ///
1741    /// Derived from what is on disk, so a space whose last document was
1742    /// deleted still lists while its index record survives, and a space
1743    /// whose documents were all removed lists for as long as its removal
1744    /// records do, which is the life of the space.
1745    pub fn data_list_spaces(&mut self) -> Result<Vec<String>> {
1746        let storage = self.require_data_storage()?;
1747        let mut spaces = BTreeSet::new();
1748        // Removal records count: a space whose documents were all removed
1749        // still has something to say to its peers, and leaving it out of
1750        // this list is what would stop the start-up sweep telling them.
1751        for key_type in [
1752            storage_keys::DATA_SPACES,
1753            storage_keys::DATA_DOCS,
1754            storage_keys::DATA_DELTA_LOG,
1755            storage_keys::DATA_DOC_META,
1756        ] {
1757            if let Ok(keys) = storage.list_keys(key_type) {
1758                for key in keys {
1759                    let name = key.split('/').next().unwrap_or(&key);
1760                    if !name.is_empty() {
1761                        spaces.insert(name.to_string());
1762                    }
1763                }
1764            }
1765        }
1766        for space in self.data.spaces.keys() {
1767            spaces.insert(space.clone());
1768        }
1769        Ok(spaces.into_iter().collect())
1770    }
1771
1772    /// Set a key in a map collection.
1773    pub fn data_map_set(
1774        &mut self,
1775        space: &str,
1776        doc: &str,
1777        collection: &str,
1778        key: &str,
1779        value: DataValue,
1780    ) -> Result<()> {
1781        self.with_doc(space, doc, true, |document| {
1782            document.map_set(collection, key, value)
1783        })
1784    }
1785
1786    /// Remove a key from a map collection.
1787    pub fn data_map_delete(
1788        &mut self,
1789        space: &str,
1790        doc: &str,
1791        collection: &str,
1792        key: &str,
1793    ) -> Result<()> {
1794        self.with_doc(space, doc, false, |document| {
1795            document.map_delete(collection, key)
1796        })
1797    }
1798
1799    /// Read a key from a map collection.
1800    pub fn data_map_get(
1801        &mut self,
1802        space: &str,
1803        doc: &str,
1804        collection: &str,
1805        key: &str,
1806    ) -> Result<Option<DataValue>> {
1807        self.read_doc(space, doc, |document| document.map_get(collection, key))
1808    }
1809
1810    /// Append to a list collection.
1811    pub fn data_list_push(
1812        &mut self,
1813        space: &str,
1814        doc: &str,
1815        collection: &str,
1816        value: DataValue,
1817    ) -> Result<()> {
1818        self.with_doc(space, doc, true, |document| {
1819            document.list_push(collection, value)
1820        })
1821    }
1822
1823    /// Insert into a list collection.
1824    pub fn data_list_insert(
1825        &mut self,
1826        space: &str,
1827        doc: &str,
1828        collection: &str,
1829        index: u32,
1830        value: DataValue,
1831    ) -> Result<()> {
1832        self.with_doc(space, doc, true, |document| {
1833            document.list_insert(collection, index as usize, value)
1834        })
1835    }
1836
1837    /// Delete entries from a list collection.
1838    pub fn data_list_delete(
1839        &mut self,
1840        space: &str,
1841        doc: &str,
1842        collection: &str,
1843        index: u32,
1844        count: u32,
1845    ) -> Result<()> {
1846        self.with_doc(space, doc, false, |document| {
1847            document.list_delete(collection, index as usize, count as usize)
1848        })
1849    }
1850
1851    /// The number of entries in a list collection.
1852    pub fn data_list_len(&mut self, space: &str, doc: &str, collection: &str) -> Result<u32> {
1853        let len = self.read_doc(space, doc, |document| document.list_len(collection))?;
1854        Ok(len as u32)
1855    }
1856
1857    /// Insert into a text collection at a character position.
1858    pub fn data_text_insert(
1859        &mut self,
1860        space: &str,
1861        doc: &str,
1862        collection: &str,
1863        position: u32,
1864        text: &str,
1865    ) -> Result<()> {
1866        self.with_doc(space, doc, true, |document| {
1867            document.text_insert(collection, position as usize, text)
1868        })
1869    }
1870
1871    /// Delete characters from a text collection.
1872    pub fn data_text_delete(
1873        &mut self,
1874        space: &str,
1875        doc: &str,
1876        collection: &str,
1877        position: u32,
1878        count: u32,
1879    ) -> Result<()> {
1880        self.with_doc(space, doc, false, |document| {
1881            document.text_delete(collection, position as usize, count as usize)
1882        })
1883    }
1884
1885    /// The contents of a text collection.
1886    pub fn data_text_value(&mut self, space: &str, doc: &str, collection: &str) -> Result<String> {
1887        self.read_doc(space, doc, |document| document.text_value(collection))
1888    }
1889
1890    /// Add to a counter collection. Negative values subtract.
1891    pub fn data_counter_increment(
1892        &mut self,
1893        space: &str,
1894        doc: &str,
1895        collection: &str,
1896        amount: f64,
1897    ) -> Result<()> {
1898        self.with_doc(space, doc, true, |document| {
1899            document.counter_increment(collection, amount)
1900        })
1901    }
1902
1903    /// The value of a counter collection.
1904    pub fn data_counter_value(&mut self, space: &str, doc: &str, collection: &str) -> Result<f64> {
1905        self.read_doc(space, doc, |document| document.counter_value(collection))
1906    }
1907
1908    /// The document's current state as plain JSON.
1909    pub fn data_doc_json(&mut self, space: &str, doc: &str) -> Result<String> {
1910        self.read_doc(space, doc, |document| document.export_json())
1911    }
1912
1913    /// The document's full history, engine-encoded.
1914    ///
1915    /// Half of the escape hatch: this and [`Self::data_doc_json`] are what
1916    /// let an application take its data and leave.
1917    pub fn data_export_raw(&mut self, space: &str, doc: &str) -> Result<Vec<u8>> {
1918        self.read_doc(space, doc, |document| document.export_raw())
1919    }
1920
1921    /// Persist any pending edits to a document.
1922    ///
1923    /// Writes one delta record, then compacts if the log has outgrown the
1924    /// document. The order is load-bearing: the delta is durable before any
1925    /// bookkeeping assumes it, and at compaction the fresh snapshot is
1926    /// durable before the deltas it folded are deleted. A crash at any point
1927    /// leaves duplicate history, which the merge absorbs, rather than a gap,
1928    /// which it cannot.
1929    pub fn data_flush(&mut self, space: &str, doc: &str) -> Result<()> {
1930        Self::validate_ids(space, doc)?;
1931        let storage = self.require_data_storage()?;
1932        self.open_doc(storage.as_ref(), space, doc)?;
1933        self.flush_doc(storage.as_ref(), space, doc)
1934    }
1935
1936    fn flush_doc(
1937        &mut self,
1938        storage: &dyn ProtocolStateStorage,
1939        space: &str,
1940        doc: &str,
1941    ) -> Result<()> {
1942        self.flush_doc_from(storage, space, doc, None)
1943    }
1944
1945    /// [`Self::flush_doc`], told which peer the change came from.
1946    ///
1947    /// `origin` is `None` for a local edit and the peer's address for one
1948    /// applied from a sync frame. It exists so replication does not send a
1949    /// change straight back to whoever sent it: the merge would absorb the
1950    /// echo, but on a mesh it is a frame nobody needed.
1951    fn flush_doc_from(
1952        &mut self,
1953        storage: &dyn ProtocolStateStorage,
1954        space: &str,
1955        doc: &str,
1956        origin: Option<&str>,
1957    ) -> Result<()> {
1958        let map_key = (space.to_string(), doc.to_string());
1959
1960        let Some(entry) = self.data.docs.get_mut(&map_key) else {
1961            return Ok(());
1962        };
1963        let Some(delta) = entry.doc.commit().map_err(map_data_error)? else {
1964            return Ok(());
1965        };
1966        let seq = entry.next_seq;
1967        let delta_bytes = delta.bytes.len();
1968        let history_complete = entry.history_complete;
1969
1970        // A commit too large for one record can never be stored as a delta,
1971        // and refusing it is not a recovery: the rewind below puts the change
1972        // back in the pending set, so the *next* commit exports a strictly
1973        // larger delta and fails the same way forever. A compacted snapshot
1974        // is bounded by the document cap rather than by history, so it is the
1975        // route that still works, and the commit's changes are inside it.
1976        let oversized = delta_bytes > MAX_PROTOCOL_STATE_RECORD_BYTES;
1977
1978        let rewind = |protocol: &mut Self| {
1979            if let Some(entry) = protocol.data.docs.get_mut(&map_key) {
1980                entry.doc.rewind_last_commit();
1981            }
1982        };
1983
1984        if oversized && !history_complete {
1985            // Compaction is the only way out and it is unavailable: folding a
1986            // log with a hole in it would delete records the document still
1987            // needs. Say so rather than looping on an unwritable delta.
1988            rewind(self);
1989            return Err(Error::Other(format!(
1990                "commit for document {space}/{doc} is {delta_bytes} bytes, over the \
1991                 {MAX_PROTOCOL_STATE_RECORD_BYTES} byte record limit, and its delta log \
1992                 is incomplete so it cannot be compacted instead"
1993            )));
1994        }
1995
1996        if oversized {
1997            warn!(
1998                space,
1999                doc, delta_bytes, "Commit is too large for one delta record; compacting instead"
2000            );
2001            if let Err(err) = self.compact_doc(storage, space, doc) {
2002                rewind(self);
2003                return Err(err);
2004            }
2005        } else if let Err(err) = self.write_state_record(
2006            storage,
2007            storage_keys::DATA_DELTA_LOG,
2008            &delta_key(space, doc, seq),
2009            &delta.bytes,
2010        ) {
2011            // Put the export marker back. The commit already advanced it, so
2012            // leaving it advanced would mean this change is never handed out
2013            // again: the document keeps it in memory, the next commit exports
2014            // only what came after, and the change is simply gone at the next
2015            // restart. Re-exporting it later costs at worst a duplicate
2016            // record, which the merge absorbs.
2017            rewind(self);
2018            return Err(Error::Other(format!(
2019                "failed to persist document delta: {err}"
2020            )));
2021        }
2022
2023        let (verdict, measured, should_compact, compacted_before) = {
2024            let entry = self
2025                .data
2026                .docs
2027                .get_mut(&map_key)
2028                .ok_or_else(|| Error::Other("document vanished during flush".to_string()))?;
2029            if !oversized {
2030                entry.next_seq = seq.saturating_add(1);
2031                entry.log_bytes = entry.log_bytes.saturating_add(delta_bytes);
2032            }
2033
2034            let (verdict, measured) = entry.doc.check_size().map_err(map_data_error)?;
2035            if let Some(bytes) = measured {
2036                entry.compacted_bytes = bytes;
2037            }
2038            // Never fold a log that did not fully apply, and never compact
2039            // twice for one flush.
2040            let should_compact = !oversized
2041                && entry.history_complete
2042                && policy::should_compact(
2043                    entry.log_bytes,
2044                    entry.compacted_bytes,
2045                    entry.doc.commits_since_compaction(),
2046                );
2047            (verdict, measured, should_compact, entry.compacted_bytes)
2048        };
2049
2050        // Replicated here for the same reason the event fires here: the
2051        // claim being made is that the change is durable, and that became
2052        // true at the write above. Pushing before it would advertise a
2053        // change that a crash could still take back, and pushing after
2054        // compaction would let a compaction failure suppress replication of
2055        // something already on disk.
2056        //
2057        // The oversized branch wrote a compacted snapshot instead of a delta
2058        // record, so there is no delta to push. Offering versions is how the
2059        // peer finds the gap: waiting for its next offer would leave both
2060        // replicas believing they agree for as long as the link holds, since
2061        // a link that never drops never fires a trigger.
2062        if oversized {
2063            self.nudge_data_sync(space, origin, "oversized_commit");
2064        } else {
2065            self.push_data_delta(space, doc, &delta.bytes, origin);
2066        }
2067
2068        // Emitted here, before compaction: the event's claim is that the
2069        // change reached storage, and that became true at the write above.
2070        // Emitting after compaction would let a compaction failure suppress
2071        // the notification for a change that is already durable.
2072        self.emit_event(Event::DataChanged {
2073            space_id: space.to_string(),
2074            doc_id: doc.to_string(),
2075            delta_bytes: delta_bytes as u64,
2076        });
2077
2078        if should_compact {
2079            self.compact_doc(storage, space, doc)?;
2080        }
2081
2082        match verdict {
2083            policy::SizeVerdict::TooLarge => {
2084                let actual = measured.unwrap_or(compacted_before);
2085                if let Some(entry) = self.data.docs.get_mut(&map_key) {
2086                    entry.over_cap = true;
2087                }
2088                // The change is already durable: refusing to persist it would
2089                // lose work the application believed it had made. What is
2090                // refused is further growth, and deletions stay open so the
2091                // document can be brought back under the cap.
2092                Err(Error::DocTooLarge {
2093                    actual,
2094                    limit: policy::MAX_DOC_BYTES,
2095                })
2096            }
2097            policy::SizeVerdict::Warn => {
2098                if let Some(entry) = self.data.docs.get_mut(&map_key) {
2099                    entry.over_cap = false;
2100                }
2101                self.emit_event(Event::DataDocSizeWarning {
2102                    space_id: space.to_string(),
2103                    doc_id: doc.to_string(),
2104                    compacted_bytes: measured.unwrap_or(compacted_before) as u64,
2105                    cap_bytes: policy::MAX_DOC_BYTES as u64,
2106                });
2107                Ok(())
2108            }
2109            policy::SizeVerdict::Ok => {
2110                if let Some(entry) = self.data.docs.get_mut(&map_key) {
2111                    entry.over_cap = false;
2112                }
2113                Ok(())
2114            }
2115        }
2116    }
2117
2118    /// Fold a document's delta log into a fresh compacted record.
2119    fn compact_doc(
2120        &mut self,
2121        storage: &dyn ProtocolStateStorage,
2122        space: &str,
2123        doc: &str,
2124    ) -> Result<()> {
2125        let map_key = (space.to_string(), doc.to_string());
2126        let compacted = {
2127            let Some(entry) = self.data.docs.get(&map_key) else {
2128                return Ok(());
2129            };
2130            // Folding a log that did not fully apply would write a snapshot
2131            // missing those changes and then delete the records that still
2132            // hold them, turning a recoverable gap into permanent loss. An
2133            // oversized delta log is the cheaper failure.
2134            if !entry.history_complete {
2135                warn!(
2136                    space,
2137                    doc, "Skipping compaction: the document's delta log did not fully apply"
2138                );
2139                return Ok(());
2140            }
2141            entry.doc.export_compacted().map_err(map_data_error)?
2142        };
2143
2144        // Durable first. Everything after this point is cleanup, and a crash
2145        // in the middle of cleanup costs disk, not data.
2146        self.write_state_record(
2147            storage,
2148            storage_keys::DATA_DOCS,
2149            &doc_key(space, doc),
2150            &compacted,
2151        )
2152        .map_err(|err| Error::Other(format!("failed to persist compacted document: {err}")))?;
2153
2154        let prefix = format!("{space}/{doc}/");
2155        if let Ok(keys) = storage.list_keys(storage_keys::DATA_DELTA_LOG) {
2156            for key in keys.iter().filter(|key| key.starts_with(&prefix)) {
2157                let _ = storage.delete(storage_keys::DATA_DELTA_LOG, key);
2158            }
2159        }
2160
2161        if let Some(entry) = self.data.docs.get_mut(&map_key) {
2162            entry.log_bytes = 0;
2163            entry.compacted_bytes = compacted.len();
2164            entry.doc.mark_compacted(compacted.len());
2165        }
2166        debug!(space, doc, bytes = compacted.len(), "Compacted document");
2167        Ok(())
2168    }
2169
2170    /// Persist every open document with pending edits.
2171    ///
2172    /// Called on shutdown, so the debounce window between an edit and its
2173    /// record can never be the reason a change is lost.
2174    pub fn data_flush_all(&mut self) -> Result<()> {
2175        let Ok(storage) = self.require_data_storage() else {
2176            return Ok(());
2177        };
2178        let keys: Vec<(String, String)> = self.data.docs.keys().cloned().collect();
2179        for (space, doc) in keys {
2180            match self.flush_doc(storage.as_ref(), &space, &doc) {
2181                Ok(()) => {}
2182                // Not a failed flush, and not reported as one: the record was
2183                // written and only the size verdict after it refused further
2184                // growth. On a shutdown path the distinction is the whole
2185                // question an operator is asking, which is whether anything
2186                // was lost.
2187                Err(Error::DocTooLarge { .. }) => {
2188                    debug!(
2189                        space,
2190                        doc, "Flushed a document that is over its cap on shutdown"
2191                    );
2192                }
2193                Err(err) => {
2194                    warn!(space, doc, error = %err, "Failed to flush document");
2195                }
2196            }
2197        }
2198        Ok(())
2199    }
2200
2201    /// Delete every record the data layer owns.
2202    ///
2203    /// The logout path for an application that pointed documents at its own
2204    /// backend. `wipePersistedState` clears the account directory of the
2205    /// *default* provider, which a custom backend is not inside, so without
2206    /// this call those documents would outlive the account that made them.
2207    ///
2208    /// **Only durable once replication has stopped.** There are no deletion
2209    /// tombstones in this release, so nothing distinguishes a space this
2210    /// device wiped from one it has never seen. Called while the engine is
2211    /// running with live sessions, every document comes back: the peer's next
2212    /// version offer names them and they are recreated and refilled from the
2213    /// peer's copy, and an offer of our own naming nothing reads as a replica
2214    /// that has never seen the space. On the logout path the engine is being
2215    /// torn down anyway. Anywhere else, stop it first, and only for as long
2216    /// as it stays stopped: the peer still holds the documents, so they return
2217    /// when replication resumes with it. The call clears this device, it does
2218    /// not delete content.
2219    pub fn data_wipe_all(&mut self) -> Result<()> {
2220        let Some(storage) = self.data_storage() else {
2221            return Ok(());
2222        };
2223        self.data.docs.clear();
2224        self.data.spaces.clear();
2225        self.data.meta.clear();
2226        // Interest goes with the content it was scoping, for the reason
2227        // stated above: nothing may distinguish a space this device wiped
2228        // from one it has never seen, and a surviving narrowing is exactly
2229        // such a distinction. It is also the logout path, so a narrowing
2230        // left behind would be one account's policy silently applied to the
2231        // next account that happens to replicate with the same peer.
2232        self.data.interest.clear();
2233        // Every window and every outstanding question goes with the content
2234        // they were about. Left behind, a window suppresses the first offer
2235        // or the first blob request made after the wipe, which is the same
2236        // shape [`Self::forget_data_sync_peer`] exists to prevent: a window
2237        // that survives the thing it was measuring.
2238        //
2239        // The pending fetches are not bookkeeping. An entry there is what
2240        // admits arriving attachment bytes, so one surviving a wipe would let
2241        // bytes land for a space this device no longer has, and hand them to
2242        // the application as an answer to a question the wipe erased.
2243        self.last_data_sync_offer.clear();
2244        self.data_sync_settle.clear();
2245        self.pending_attachment_fetches.clear();
2246        self.blob_request_windows.clear();
2247
2248        // Attempt every record and report the first failure, as
2249        // [`Self::data_delete_doc`] does. Answering `Ok` for a wipe that left
2250        // records behind is the worst shape this call can take: it is the
2251        // logout path, so the records it failed to remove outlive the account
2252        // that made them, and the application has no symptom to notice it by.
2253        let mut first_error: Option<String> = None;
2254
2255        // Custody goes with the documents. A custody store that survived a
2256        // logout would hold other people's traffic past the point the user
2257        // asked for erasure, and there is no global wipe for it to inherit
2258        // (`docs/spec/custody.md`, "Erase"), so this wipe calls its own.
2259        if let Err(err) = self.erase_custody() {
2260            first_error = Some(err.to_string());
2261        }
2262
2263        let mut record_error = |err: crate::protocol_state_storage::ProtocolStateError| {
2264            if first_error.is_none() {
2265                first_error = Some(err.to_string());
2266            }
2267        };
2268
2269        // Every key type the layer owns, replication bookkeeping included.
2270        // A key type missing from this list is not a partial wipe that
2271        // reports itself, it is a silent one: the records are keyed by peer
2272        // address, so what survives a logout is who this account replicated
2273        // with.
2274        for key_type in [
2275            storage_keys::DATA_DOCS,
2276            storage_keys::DATA_DELTA_LOG,
2277            storage_keys::DATA_SPACES,
2278            storage_keys::DATA_SYNC,
2279            storage_keys::DATA_DOC_META,
2280        ] {
2281            match storage.list_keys(key_type) {
2282                Ok(keys) => {
2283                    for key in keys {
2284                        if let Err(err) = storage.delete(key_type, &key) {
2285                            record_error(err);
2286                        }
2287                    }
2288                }
2289                Err(err) => record_error(err),
2290            }
2291        }
2292
2293        match first_error {
2294            Some(detail) => Err(Error::Other(format!(
2295                "failed to delete every data record: {detail}"
2296            ))),
2297            None => Ok(()),
2298        }
2299    }
2300
2301    /// The compacted size of a document, in bytes.
2302    pub fn data_doc_size(&mut self, space: &str, doc: &str) -> Result<u64> {
2303        Self::validate_ids(space, doc)?;
2304        let storage = self.require_data_storage()?;
2305        self.open_doc(storage.as_ref(), space, doc)?;
2306        let entry = self
2307            .data
2308            .docs
2309            .get_mut(&(space.to_string(), doc.to_string()))
2310            .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
2311        let bytes = entry.doc.measure().map_err(map_data_error)?;
2312        entry.compacted_bytes = bytes;
2313        Ok(bytes as u64)
2314    }
2315}