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//!
18//! # Why the listing is the truth and the index is a cache
19//!
20//! Everything needed to re-open a document is derived from what is actually
21//! on disk: `list_keys` names the delta records, and their zero-padded hex
22//! sequence sorts into apply order. The space record only remembers document
23//! names, so a document created and never written still lists. Any crash
24//! therefore leaves a readable store rather than a bookkeeping claim that
25//! has to be reconciled against reality.
26
27use std::collections::{BTreeMap, BTreeSet};
28use std::sync::Arc;
29
30use offline_protocol_data::{
31    policy, CatchUp, DataDoc, DataError, DataValue, RemoteImport, VersionToken,
32};
33use serde::{Deserialize, Serialize};
34use tracing::{debug, warn};
35
36use crate::error::{Error, Result};
37use crate::events::Event;
38use crate::protocol::storage::StateRecord;
39use crate::protocol::types::{storage_keys, MAX_PROTOCOL_STATE_RECORD_BYTES};
40use crate::protocol::OfflineProtocol;
41use crate::protocol_state_storage::ProtocolStateStorage;
42
43/// The document index for one space.
44///
45/// Deliberately thin. Sizes and sequence numbers are derived from the
46/// records themselves at open, so this record can never disagree with the
47/// store about what exists; it only remembers names, which nothing else
48/// records for a document that has never been written.
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50struct SpaceRecord {
51    #[serde(default)]
52    docs: BTreeSet<String>,
53}
54
55/// A document held open in memory.
56struct LoadedDoc {
57    doc: DataDoc,
58    /// Sequence number the next persisted commit will use.
59    next_seq: u64,
60    /// Total size of the delta records currently on disk for this document.
61    log_bytes: usize,
62    /// Size of the last compacted record written for this document.
63    compacted_bytes: usize,
64    /// Set when the document's compacted encoding passed the cap.
65    ///
66    /// Growth is refused while it is set; deletions are not, so an
67    /// application that hit the cap can shrink its way back out. Cleared by
68    /// the next flush that measures under the cap.
69    over_cap: bool,
70    /// Whether every delta record on disk actually applied at open.
71    ///
72    /// False when a delta was unreadable, or when one imported but parked
73    /// behind a predecessor that never arrived. Both mean the document in
74    /// memory is missing changes the records still describe, and both make
75    /// compaction destructive: folding the log would write a snapshot without
76    /// those changes and then delete the records holding them. A growing log
77    /// is the cheaper failure, so compaction is simply switched off until an
78    /// open sees a complete log again.
79    history_complete: bool,
80}
81
82/// In-memory state for the data layer.
83#[derive(Default)]
84pub(crate) struct DataLayer {
85    docs: BTreeMap<(String, String), LoadedDoc>,
86    spaces: BTreeMap<String, SpaceRecord>,
87}
88
89impl std::fmt::Debug for DataLayer {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        f.debug_struct("DataLayer")
92            .field("open_docs", &self.docs.len())
93            .field("loaded_spaces", &self.spaces.len())
94            .finish()
95    }
96}
97
98fn map_data_error(err: DataError) -> Error {
99    match err {
100        DataError::DocTooLarge { actual, limit } => Error::DocTooLarge { actual, limit },
101        DataError::Corrupt(detail) | DataError::Engine(detail) => Error::DataCorrupted(detail),
102        DataError::Poisoned => {
103            Error::DataCorrupted("document handle poisoned by an earlier decode failure".into())
104        }
105        DataError::InvalidName { name, reason } => {
106            Error::InvalidArgument(format!("invalid name {name:?}: {reason}"))
107        }
108        DataError::ValueTooLarge { actual, limit } => Error::InvalidArgument(format!(
109            "value is {actual} bytes, over the {limit} byte limit"
110        )),
111        DataError::InvalidAttachment { reason } => {
112            Error::InvalidArgument(format!("invalid attachment: {reason}"))
113        }
114        DataError::OutOfRange { position, length } => Error::InvalidArgument(format!(
115            "position {position} is out of range for length {length}"
116        )),
117        // Forced by `#[non_exhaustive]` on `DataError`, which the data crate
118        // carries so a new variant is not a semver break for published
119        // consumers. That trade costs the compile error that would otherwise
120        // catch an unmapped variant, so it is replaced with a `warn!`: an
121        // unmapped variant shows up in logs instead of vanishing. When you add
122        // a variant, add its arm here — this is a tripwire, not a destination.
123        ref other => {
124            warn!(
125                error = %other,
126                "unmapped DataError variant reached the protocol boundary; \
127                 add an explicit arm in protocol/data.rs"
128            );
129            Error::Other(other.to_string())
130        }
131    }
132}
133
134/// Compose the record key for a document's compacted snapshot.
135fn doc_key(space: &str, doc: &str) -> String {
136    format!("{space}/{doc}")
137}
138
139/// Compose the record key for one persisted commit.
140///
141/// The sequence is zero-padded hex so that lexicographic order — which is
142/// the only order `list_keys` promises anything about — is apply order.
143fn delta_key(space: &str, doc: &str, seq: u64) -> String {
144    format!("{space}/{doc}/{seq:016x}")
145}
146
147/// Parse the sequence out of a delta record key belonging to this document.
148fn delta_seq(key: &str, prefix: &str) -> Option<u64> {
149    let suffix = key.strip_prefix(prefix)?;
150    u64::from_str_radix(suffix, 16).ok()
151}
152
153impl OfflineProtocol {
154    /// The backend documents are stored in.
155    ///
156    /// The application's override when it set one, and otherwise the store
157    /// protocol state already uses — which is what makes the layer work with
158    /// no storage configuration at all.
159    fn data_storage(&self) -> Option<Arc<dyn ProtocolStateStorage>> {
160        self.config
161            .data
162            .storage
163            .clone()
164            .or_else(|| self.protocol_state_storage.clone())
165    }
166
167    /// The backend, or the typed error explaining which precondition failed.
168    fn require_data_storage(&self) -> Result<Arc<dyn ProtocolStateStorage>> {
169        if !self.config.data.enabled {
170            return Err(Error::DataDisabled);
171        }
172        self.data_storage().ok_or(Error::DataStorageUnavailable)
173    }
174
175    fn validate_ids(space: &str, doc: &str) -> Result<()> {
176        offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
177        offline_protocol_data::validate_name(doc).map_err(map_data_error)?;
178        Ok(())
179    }
180
181    /// Read a space's document index, loading it on first touch.
182    fn load_space(&mut self, storage: &dyn ProtocolStateStorage, space: &str) -> Result<()> {
183        if self.data.spaces.contains_key(space) {
184            return Ok(());
185        }
186        let mut record = SpaceRecord::default();
187        match self.read_state_record(storage, storage_keys::DATA_SPACES, space) {
188            Ok(Some(bytes)) => match serde_json::from_slice::<SpaceRecord>(&bytes) {
189                Ok(parsed) => record = parsed,
190                Err(err) => {
191                    // The index is a cache: a damaged one costs the names of
192                    // documents that have no records yet, never a document.
193                    warn!(space, error = %err, "Unreadable data space index; rebuilding from records");
194                }
195            },
196            Ok(None) => {}
197            Err(err) => {
198                warn!(space, error = %err, "Failed to read data space index");
199            }
200        }
201
202        // Reconcile against what is actually stored, so the index can never
203        // hide a document that exists.
204        for key_type in [storage_keys::DATA_DOCS, storage_keys::DATA_DELTA_LOG] {
205            if let Ok(keys) = storage.list_keys(key_type) {
206                for key in keys {
207                    if let Some(rest) = key.strip_prefix(&format!("{space}/")) {
208                        let name = rest.split('/').next().unwrap_or(rest);
209                        if !name.is_empty() {
210                            record.docs.insert(name.to_string());
211                        }
212                    }
213                }
214            }
215        }
216
217        self.data.spaces.insert(space.to_string(), record);
218        Ok(())
219    }
220
221    fn persist_space(&mut self, storage: &dyn ProtocolStateStorage, space: &str) -> Result<()> {
222        let Some(record) = self.data.spaces.get(space) else {
223            return Ok(());
224        };
225        let bytes =
226            serde_json::to_vec(record).map_err(|err| Error::Serialization(err.to_string()))?;
227        self.write_state_record(storage, storage_keys::DATA_SPACES, space, &bytes)
228            .map_err(|err| Error::Other(format!("failed to persist data space index: {err}")))
229    }
230
231    /// Open a document, loading its compacted record and delta log.
232    ///
233    /// Applying the compacted record and then every delta is safe in any
234    /// order and safe to repeat: duplicates are absorbed by the merge, which
235    /// is exactly why a crash at any point during compaction leaves a
236    /// recoverable store.
237    fn open_doc(
238        &mut self,
239        storage: &dyn ProtocolStateStorage,
240        space: &str,
241        doc: &str,
242    ) -> Result<()> {
243        if self
244            .data
245            .docs
246            .contains_key(&(space.to_string(), doc.to_string()))
247        {
248            return Ok(());
249        }
250        self.load_space(storage, space)?;
251
252        let mut document = DataDoc::new();
253        let mut compacted_bytes = 0usize;
254        let mut history_complete = true;
255
256        match self.read_state_record_detailed(
257            storage,
258            storage_keys::DATA_DOCS,
259            &doc_key(space, doc),
260        ) {
261            Ok(StateRecord::Present(bytes)) => {
262                compacted_bytes = bytes.len();
263                // A snapshot is self-contained, so parking here should not be
264                // reachable. Recorded rather than assumed away: if it ever is,
265                // the document is missing its own base and compaction would
266                // write that gap over the delta log.
267                if document.import(&bytes).map_err(map_data_error)?.is_parked() {
268                    history_complete = false;
269                    warn!(space, doc, "Document snapshot did not fully apply");
270                }
271            }
272            // `Unavailable` means it is still on disk and may be fine, so
273            // refusing here keeps a transient failure from looking like data
274            // loss to the application.
275            Ok(StateRecord::Unavailable) => {
276                return Err(Error::Other(format!(
277                    "document {space}/{doc} could not be read this session"
278                )));
279            }
280            // `Unreadable` means the record was examined and is permanently
281            // gone; the delta log may still carry the document forward. The
282            // document has lost its base either way, so compaction stays off:
283            // folding now would write whatever partial state survived over the
284            // records that still hold the rest.
285            Ok(StateRecord::Unreadable) => {
286                history_complete = false;
287                warn!(space, doc, "Document snapshot is permanently unreadable");
288            }
289            Ok(StateRecord::Missing) => {}
290            Err(err) => {
291                return Err(Error::Other(format!("failed to read document: {err}")));
292            }
293        }
294
295        let prefix = format!("{space}/{doc}/");
296        let mut sequences: Vec<u64> = storage
297            .list_keys(storage_keys::DATA_DELTA_LOG)
298            .unwrap_or_default()
299            .iter()
300            .filter_map(|key| delta_seq(key, &prefix))
301            .collect();
302        sequences.sort_unstable();
303
304        let mut log_bytes = 0usize;
305        let mut next_seq = 0u64;
306        let mut replayed = 0u32;
307        for seq in sequences {
308            next_seq = next_seq.max(seq.saturating_add(1));
309            let key = delta_key(space, doc, seq);
310            match self.read_state_record_detailed(storage, storage_keys::DATA_DELTA_LOG, &key) {
311                Ok(StateRecord::Present(bytes)) => {
312                    log_bytes = log_bytes.saturating_add(bytes.len());
313                    replayed = replayed.saturating_add(1);
314                    match document.import(&bytes) {
315                        // The engine accepted the delta but cannot apply it:
316                        // its predecessor is missing, so it and everything
317                        // after it are invisible. Recording that is what stops
318                        // compaction from writing a snapshot without these
319                        // changes and then deleting the records holding them.
320                        Ok(outcome) if outcome.is_parked() => {
321                            history_complete = false;
322                            warn!(
323                                space,
324                                doc,
325                                seq,
326                                "Document delta is waiting on a predecessor that never \
327                                 arrived; compaction is disabled for this document"
328                            );
329                        }
330                        Ok(_) => {}
331                        Err(err) => {
332                            // A permanently undecodable delta is a hole. The
333                            // document still opens with everything that did
334                            // apply, but the log must not be folded away: F3
335                            // resend is what closes a hole, and compaction
336                            // would destroy what is left to resend against.
337                            history_complete = false;
338                            warn!(space, doc, seq, error = %err, "Skipping undecodable document delta");
339                        }
340                    }
341                }
342                // Still on disk and possibly perfectly good next launch.
343                // Refusing the open keeps a transient read failure from
344                // looking like data loss, exactly as the snapshot path above
345                // does, and keeps compaction away from records that
346                // everything after this delta depends on.
347                Ok(StateRecord::Unavailable) => {
348                    return Err(Error::Other(format!(
349                        "delta {seq} of document {space}/{doc} could not be read this session"
350                    )));
351                }
352                // Listed but gone or permanently unreadable: a hole, not a
353                // reason to refuse the document.
354                Ok(_) => {
355                    history_complete = false;
356                    warn!(space, doc, seq, "Document delta is missing or unreadable");
357                }
358                Err(err) => {
359                    return Err(Error::Other(format!(
360                        "failed to read delta {seq} of document {space}/{doc}: {err}"
361                    )));
362                }
363            }
364        }
365
366        document.mark_persisted();
367        if compacted_bytes == 0 {
368            compacted_bytes = document.measure().map_err(map_data_error)?;
369        }
370        // Hand the document what its own bookkeeping cannot know after a
371        // fresh open: how big the record it came from was, how much delta
372        // log sits on top, and how many commits that log represents. Skip
373        // this and a reopened document looks empty to the size check and to
374        // the compaction trigger, both of which then under-fire.
375        document.restore_bookkeeping(compacted_bytes, log_bytes, replayed);
376
377        if let Some(record) = self.data.spaces.get_mut(space) {
378            record.docs.insert(doc.to_string());
379        }
380        self.data.docs.insert(
381            (space.to_string(), doc.to_string()),
382            LoadedDoc {
383                doc: document,
384                next_seq,
385                log_bytes,
386                compacted_bytes,
387                over_cap: false,
388                history_complete,
389            },
390        );
391        Ok(())
392    }
393
394    /// Run an edit against an open document.
395    ///
396    /// `growth` marks operations that can make the document bigger. Those are
397    /// refused once the cap has been passed; deletions are not, so an
398    /// application that filled a document can still empty it.
399    fn with_doc<F>(&mut self, space: &str, doc: &str, growth: bool, edit: F) -> Result<()>
400    where
401        F: FnOnce(&mut DataDoc) -> std::result::Result<(), DataError>,
402    {
403        Self::validate_ids(space, doc)?;
404        let storage = self.require_data_storage()?;
405        self.open_doc(storage.as_ref(), space, doc)?;
406
407        let entry = self
408            .data
409            .docs
410            .get_mut(&(space.to_string(), doc.to_string()))
411            .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
412
413        if growth && entry.over_cap {
414            return Err(Error::DocTooLarge {
415                actual: entry.compacted_bytes,
416                limit: policy::MAX_DOC_BYTES,
417            });
418        }
419        edit(&mut entry.doc).map_err(map_data_error)
420    }
421
422    /// Read from an open document.
423    fn read_doc<F, T>(&mut self, space: &str, doc: &str, read: F) -> Result<T>
424    where
425        F: FnOnce(&DataDoc) -> std::result::Result<T, DataError>,
426    {
427        Self::validate_ids(space, doc)?;
428        let storage = self.require_data_storage()?;
429        self.open_doc(storage.as_ref(), space, doc)?;
430        let entry = self
431            .data
432            .docs
433            .get(&(space.to_string(), doc.to_string()))
434            .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
435        read(&entry.doc).map_err(map_data_error)
436    }
437
438    // ---- public surface ---------------------------------------------
439
440    /// Point documents at a backend the application supplies.
441    ///
442    /// Documents move; protocol secrets do not. Applied immediately, so a
443    /// store opened afterwards reads and writes through the new backend while
444    /// everything else stays where it is.
445    ///
446    /// Any document already open is written into the new backend as a
447    /// self-contained snapshot before this returns. That is not a
448    /// convenience, it is the whole correctness of a mid-session swap: a
449    /// delta record only describes the change *since* the previous one, so a
450    /// document that merely kept writing deltas into the new backend would
451    /// leave every earlier delta behind in the old one. The engine accepts
452    /// such an orphan delta and parks it, which means the document reads
453    /// **empty** from the new backend and the next compaction deletes the
454    /// orphans for good.
455    ///
456    /// If the migration cannot be written the swap does not happen at all and
457    /// the error is returned, because a half-swapped layer is precisely the
458    /// stranding this method exists to prevent.
459    ///
460    /// Documents that are not open are not migrated: they live wholly in the
461    /// old backend, which is what switching backends means.
462    pub fn set_data_storage(&mut self, storage: Arc<dyn ProtocolStateStorage>) -> Result<()> {
463        let previous = self.config.data.storage.take();
464        self.config.data.storage = Some(storage);
465
466        match self.migrate_open_docs_to_data_storage() {
467            Ok(()) => Ok(()),
468            Err(err) => {
469                self.config.data.storage = previous;
470                Err(err)
471            }
472        }
473    }
474
475    /// Write every open document into the current backend as a self-contained
476    /// snapshot, so nothing depends on history that lives somewhere else.
477    ///
478    /// Two phases, and the split is the correctness. Every write happens
479    /// first; only once all of them are durable does any document's
480    /// bookkeeping move. Interleaving the two would leave a document that
481    /// migrated before the failure claiming a fresh empty log, while the
482    /// caller rolls the swap back to the old backend: its next flush would
483    /// then write sequence zero over the delta already there and park
484    /// everything that followed it, which is permanent loss of exactly the
485    /// kind this migration exists to prevent.
486    fn migrate_open_docs_to_data_storage(&mut self) -> Result<()> {
487        let Some(storage) = self.data_storage() else {
488            return Ok(());
489        };
490
491        let keys: Vec<(String, String)> = self.data.docs.keys().cloned().collect();
492        let mut committed: Vec<(String, String)> = Vec::new();
493        let mut migrated: Vec<((String, String), usize)> = Vec::new();
494
495        if let Err(err) =
496            self.write_open_docs_into(storage.as_ref(), &keys, &mut committed, &mut migrated)
497        {
498            // Nothing durable is being claimed, so every export marker this
499            // advanced goes back. Leaving one advanced would drop the edits
500            // it covers from every future delta, which is the same silent
501            // loss a failed delta write already rewinds for.
502            for map_key in committed {
503                if let Some(entry) = self.data.docs.get_mut(&map_key) {
504                    entry.doc.rewind_last_commit();
505                }
506            }
507            return Err(err);
508        }
509
510        // Phase two. Every record above is durable, so the swap is going to
511        // stand and the bookkeeping can follow it.
512        for (map_key, compacted_bytes) in migrated {
513            if let Some(entry) = self.data.docs.get_mut(&map_key) {
514                // The new backend holds one snapshot and no delta log.
515                entry.next_seq = 0;
516                entry.log_bytes = 0;
517                entry.compacted_bytes = compacted_bytes;
518                entry.doc.mark_compacted(compacted_bytes);
519            }
520        }
521        Ok(())
522    }
523
524    /// Phase one of a backend swap: every open document and space index
525    /// written into the new backend, with nothing in memory moved yet.
526    ///
527    /// `committed` collects the documents whose export marker this advanced,
528    /// so a failure can put every one of them back. `migrated` collects the
529    /// bookkeeping to apply once every write has succeeded.
530    fn write_open_docs_into(
531        &mut self,
532        storage: &dyn ProtocolStateStorage,
533        keys: &[(String, String)],
534        committed: &mut Vec<(String, String)>,
535        migrated: &mut Vec<((String, String), usize)>,
536    ) -> Result<()> {
537        for (space, doc) in keys {
538            let map_key = (space.clone(), doc.clone());
539
540            // Fold pending edits in first: the snapshot below has to be the
541            // whole document, including what has not been flushed yet.
542            {
543                let Some(entry) = self.data.docs.get_mut(&map_key) else {
544                    continue;
545                };
546                if entry.doc.commit().map_err(map_data_error)?.is_some() {
547                    committed.push(map_key.clone());
548                }
549            }
550            let compacted = {
551                let Some(entry) = self.data.docs.get(&map_key) else {
552                    continue;
553                };
554                entry.doc.export_compacted().map_err(map_data_error)?
555            };
556
557            self.write_state_record(
558                storage,
559                storage_keys::DATA_DOCS,
560                &doc_key(space, doc),
561                &compacted,
562            )
563            .map_err(|err| {
564                Error::Other(format!(
565                    "failed to move document {space}/{doc} into the new backend: {err}"
566                ))
567            })?;
568
569            // Snapshot first, then drop the log, in that order and for the
570            // same reason as compaction. A delta record already sitting under
571            // this name in the new backend belongs to some earlier document
572            // that held it: the snapshot just written does not descend from
573            // it, so at the next open it parks and switches compaction off
574            // for good. Best-effort, again as compaction: the snapshot is
575            // already durable, and a leftover costs a growing log rather than
576            // a wrong document.
577            let prefix = format!("{space}/{doc}/");
578            if let Ok(stale) = storage.list_keys(storage_keys::DATA_DELTA_LOG) {
579                for key in stale.iter().filter(|key| key.starts_with(&prefix)) {
580                    let _ = storage.delete(storage_keys::DATA_DELTA_LOG, key);
581                }
582            }
583
584            migrated.push((map_key, compacted.len()));
585        }
586
587        // The space indexes carry document names that have no records of
588        // their own yet; without them the new backend forgets those names.
589        let spaces: Vec<String> = self.data.spaces.keys().cloned().collect();
590        for space in spaces {
591            self.persist_space(storage, &space)?;
592        }
593        Ok(())
594    }
595
596    /// Create a document, or do nothing if it already exists.
597    pub fn data_create_doc(&mut self, space: &str, doc: &str) -> Result<()> {
598        Self::validate_ids(space, doc)?;
599        let storage = self.require_data_storage()?;
600        self.open_doc(storage.as_ref(), space, doc)?;
601        self.persist_space(storage.as_ref(), space)
602    }
603
604    // ---- replication seams ------------------------------------------
605    //
606    // The sync module owns the frames and the peer bookkeeping; these are
607    // the four things it needs from the store, and they are here rather
608    // than there so nothing outside this module touches a document handle.
609
610    /// The backend documents live in, when the layer is on.
611    ///
612    /// Unlike `require_data_storage` this answers `None` rather than an
613    /// error: the sync path treats a switched-off layer as "nothing to do",
614    /// not as a failure to report.
615    pub(crate) fn data_storage_for_sync(&self) -> Option<Arc<dyn ProtocolStateStorage>> {
616        if !self.config.data.enabled {
617            return None;
618        }
619        self.data_storage()
620    }
621
622    /// Every document in a space with the version we hold of it.
623    ///
624    /// A document whose version cannot be read is left out rather than
625    /// failing the whole space. Propagating the error costs every other
626    /// document in the space its replication, and does it silently: one
627    /// unreadable record on either leg would leave the offer empty and both
628    /// replicas believing they had nothing to say to each other. Leaving it
629    /// out instead means the peer reads that document as one we have never
630    /// seen and offers it back, which is the recovery this layer already
631    /// has, and the rest of the space keeps converging meanwhile.
632    pub(crate) fn data_sync_versions(&mut self, space: &str) -> Result<BTreeMap<String, String>> {
633        let docs = self.data_list_docs(space)?;
634        let mut versions = BTreeMap::new();
635        for doc in docs {
636            match self.data_doc_version(space, &doc) {
637                Ok(encoded) => {
638                    versions.insert(doc, encoded);
639                }
640                Err(err) => {
641                    warn!(space, doc, error = %err, "Leaving a document out of the version offer: its version cannot be read");
642                }
643            }
644        }
645        Ok(versions)
646    }
647
648    /// The version we hold of one document, encoded as a frame carries it.
649    pub(crate) fn data_doc_version(&mut self, space: &str, doc: &str) -> Result<String> {
650        use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
651
652        let token = self.read_doc(space, doc, |document| Ok(document.version()))?;
653        Ok(BASE64.encode(token.as_bytes()))
654    }
655
656    /// The names a space holds, or `None` if there is nothing to read.
657    ///
658    /// Answered off the space index, which `load_space` has already
659    /// reconciled against the records themselves and cached. Opening a
660    /// document to find out would defeat both callers, whose whole question
661    /// is whether opening one is allowed.
662    fn space_docs(&mut self, space: &str) -> Option<&BTreeSet<String>> {
663        if offline_protocol_data::validate_space_name(space).is_err() {
664            return None;
665        }
666        let storage = self.data_storage_for_sync()?;
667        self.load_space(storage.as_ref(), space).ok()?;
668        self.data.spaces.get(space).map(|record| &record.docs)
669    }
670
671    /// Whether `space` already holds `doc`.
672    pub(crate) fn data_holds_doc(&mut self, space: &str, doc: &str) -> bool {
673        self.space_docs(space)
674            .is_some_and(|docs| docs.contains(doc))
675    }
676
677    /// Whether a document a peer named may be stored in `space`.
678    ///
679    /// One already held always may; a new one only while the space is under
680    /// `cap`. The cap is a parameter rather than a constant here because it
681    /// bounds what a *peer* can talk this device into storing, which is a
682    /// replication policy: an application creating its own documents is not
683    /// subject to it.
684    ///
685    /// A space that cannot be read admits, rather than refusing on a storage
686    /// hiccup: whatever follows fails on the same storage and reports it
687    /// where the failure actually is.
688    pub(crate) fn data_space_admits_doc(&mut self, space: &str, doc: &str, cap: usize) -> bool {
689        self.space_docs(space)
690            .is_none_or(|docs| docs.contains(doc) || docs.len() < cap)
691    }
692
693    /// What a replica at `theirs` is missing from a document.
694    pub(crate) fn data_catch_up(
695        &mut self,
696        space: &str,
697        doc: &str,
698        theirs: &VersionToken,
699    ) -> Result<CatchUp> {
700        self.read_doc(space, doc, |document| document.export_since(theirs))
701    }
702
703    /// A self-contained encoding of a document, for a peer no run of
704    /// changes can catch up.
705    pub(crate) fn data_export_snapshot(&mut self, space: &str, doc: &str) -> Result<Vec<u8>> {
706        self.read_doc(space, doc, |document| document.export_raw())
707    }
708
709    /// Apply a blob that arrived from a peer, and persist what it changed.
710    ///
711    /// The import goes through the engine's remote path, which judges the
712    /// blob before touching it. A change that applies is flushed here rather
713    /// than left in memory: an unflushed remote change would be lost to a
714    /// restart, and the sender has already been told (by the acknowledgement
715    /// the frame rides on) that it arrived.
716    ///
717    /// `Err` means the change is not durable. A document held over its cap
718    /// still answers `Applied`, because the cap refuses growth *after* the
719    /// change is on disk; the caller cannot tell the two apart from an error
720    /// alone, and one of them is a change it must not report as refused.
721    pub(crate) fn data_apply_remote(
722        &mut self,
723        space: &str,
724        doc: &str,
725        blob: &[u8],
726    ) -> Result<RemoteImport> {
727        Self::validate_ids(space, doc)?;
728        let storage = self.require_data_storage()?;
729        self.open_doc(storage.as_ref(), space, doc)?;
730
731        let map_key = (space.to_string(), doc.to_string());
732        let outcome = {
733            let entry = self
734                .data
735                .docs
736                .get_mut(&map_key)
737                .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
738            let outcome = entry.doc.import_remote(blob).map_err(map_data_error)?;
739            // A parked change is invisible to the engine's own version, so
740            // nothing downstream may fold history while one is outstanding:
741            // compaction would write a snapshot without it and then delete
742            // the records it is waiting for.
743            //
744            // It stays false for the rest of this document's time in memory,
745            // deliberately. The engine reports what one import left pending,
746            // never whether a document still holds anything parked, so a
747            // later import that applies cleanly is not evidence that the
748            // earlier gap closed. Clearing it on that evidence would re-arm
749            // compaction while a change is still waiting, which is the loss
750            // this flag exists to prevent; leaving it set costs a document
751            // its compaction until it is next opened, where the flag is
752            // recomputed from the records themselves.
753            if outcome == RemoteImport::Parked {
754                entry.history_complete = false;
755            }
756            outcome
757        };
758
759        if outcome == RemoteImport::Applied {
760            // Remote work is not this replica's to push onward, so the flush
761            // is told where the change came from.
762            //
763            // `DocTooLarge` is not an import failure and is not reported as
764            // one. It is raised after the delta record was written and the
765            // push decided, so the imported change is already durable; what
766            // failed is the size verdict that follows, and what it refuses is
767            // further *growth*. Propagating it would say the change was
768            // refused when it applied, skip the space record below, and cost
769            // the caller the one signal that a local edit may have been
770            // folded into this import and suppressed with it, because that
771            // compensation is gated on hearing `Applied`. A document over its
772            // cap is exactly where that matters: the pending edit it still
773            // accepts is a deletion, which is the route back under.
774            match self.flush_doc_from(storage.as_ref(), space, doc, Some(space)) {
775                Ok(()) => {}
776                Err(Error::DocTooLarge { .. }) => {
777                    debug!(
778                        space,
779                        doc, "Applied a remote change into a document that is over its cap"
780                    );
781                }
782                Err(err) => return Err(err),
783            }
784            self.persist_space(storage.as_ref(), space)?;
785        }
786        Ok(outcome)
787    }
788
789    /// Delete a document and every record belonging to it.
790    pub fn data_delete_doc(&mut self, space: &str, doc: &str) -> Result<()> {
791        Self::validate_ids(space, doc)?;
792        let storage = self.require_data_storage()?;
793        self.load_space(storage.as_ref(), space)?;
794
795        self.data.docs.remove(&(space.to_string(), doc.to_string()));
796
797        // Every record is attempted even after one fails, so a partial
798        // failure still removes as much as it can; the first error is what
799        // the caller hears. Reporting matters because a record that survives
800        // a delete is replayed into the *next* document of the same name,
801        // which resurrects the deleted incarnation's contents.
802        let mut first_error: Option<String> = None;
803        let mut record_error = |err: crate::protocol_state_storage::ProtocolStateError| {
804            if first_error.is_none() {
805                first_error = Some(err.to_string());
806            }
807        };
808
809        if let Err(err) = storage.delete(storage_keys::DATA_DOCS, &doc_key(space, doc)) {
810            record_error(err);
811        }
812        let prefix = format!("{space}/{doc}/");
813        match storage.list_keys(storage_keys::DATA_DELTA_LOG) {
814            Ok(keys) => {
815                for key in keys.iter().filter(|key| key.starts_with(&prefix)) {
816                    if let Err(err) = storage.delete(storage_keys::DATA_DELTA_LOG, key) {
817                        record_error(err);
818                    }
819                }
820            }
821            Err(err) => record_error(err),
822        }
823        if let Some(record) = self.data.spaces.get_mut(space) {
824            record.docs.remove(doc);
825        }
826        self.persist_space(storage.as_ref(), space)?;
827
828        match first_error {
829            Some(detail) => Err(Error::Other(format!(
830                "failed to delete every record of document {space}/{doc}: {detail}"
831            ))),
832            None => Ok(()),
833        }
834    }
835
836    /// The documents in a space.
837    pub fn data_list_docs(&mut self, space: &str) -> Result<Vec<String>> {
838        offline_protocol_data::validate_space_name(space).map_err(map_data_error)?;
839        let storage = self.require_data_storage()?;
840        self.load_space(storage.as_ref(), space)?;
841        Ok(self
842            .data
843            .spaces
844            .get(space)
845            .map(|record| record.docs.iter().cloned().collect())
846            .unwrap_or_default())
847    }
848
849    /// Every space the store knows of.
850    ///
851    /// Derived from what is on disk, so a space whose last document was
852    /// deleted still lists while its index record survives.
853    pub fn data_list_spaces(&mut self) -> Result<Vec<String>> {
854        let storage = self.require_data_storage()?;
855        let mut spaces = BTreeSet::new();
856        for key_type in [
857            storage_keys::DATA_SPACES,
858            storage_keys::DATA_DOCS,
859            storage_keys::DATA_DELTA_LOG,
860        ] {
861            if let Ok(keys) = storage.list_keys(key_type) {
862                for key in keys {
863                    let name = key.split('/').next().unwrap_or(&key);
864                    if !name.is_empty() {
865                        spaces.insert(name.to_string());
866                    }
867                }
868            }
869        }
870        for space in self.data.spaces.keys() {
871            spaces.insert(space.clone());
872        }
873        Ok(spaces.into_iter().collect())
874    }
875
876    /// Set a key in a map collection.
877    pub fn data_map_set(
878        &mut self,
879        space: &str,
880        doc: &str,
881        collection: &str,
882        key: &str,
883        value: DataValue,
884    ) -> Result<()> {
885        self.with_doc(space, doc, true, |document| {
886            document.map_set(collection, key, value)
887        })
888    }
889
890    /// Remove a key from a map collection.
891    pub fn data_map_delete(
892        &mut self,
893        space: &str,
894        doc: &str,
895        collection: &str,
896        key: &str,
897    ) -> Result<()> {
898        self.with_doc(space, doc, false, |document| {
899            document.map_delete(collection, key)
900        })
901    }
902
903    /// Read a key from a map collection.
904    pub fn data_map_get(
905        &mut self,
906        space: &str,
907        doc: &str,
908        collection: &str,
909        key: &str,
910    ) -> Result<Option<DataValue>> {
911        self.read_doc(space, doc, |document| document.map_get(collection, key))
912    }
913
914    /// Append to a list collection.
915    pub fn data_list_push(
916        &mut self,
917        space: &str,
918        doc: &str,
919        collection: &str,
920        value: DataValue,
921    ) -> Result<()> {
922        self.with_doc(space, doc, true, |document| {
923            document.list_push(collection, value)
924        })
925    }
926
927    /// Insert into a list collection.
928    pub fn data_list_insert(
929        &mut self,
930        space: &str,
931        doc: &str,
932        collection: &str,
933        index: u32,
934        value: DataValue,
935    ) -> Result<()> {
936        self.with_doc(space, doc, true, |document| {
937            document.list_insert(collection, index as usize, value)
938        })
939    }
940
941    /// Delete entries from a list collection.
942    pub fn data_list_delete(
943        &mut self,
944        space: &str,
945        doc: &str,
946        collection: &str,
947        index: u32,
948        count: u32,
949    ) -> Result<()> {
950        self.with_doc(space, doc, false, |document| {
951            document.list_delete(collection, index as usize, count as usize)
952        })
953    }
954
955    /// The number of entries in a list collection.
956    pub fn data_list_len(&mut self, space: &str, doc: &str, collection: &str) -> Result<u32> {
957        let len = self.read_doc(space, doc, |document| document.list_len(collection))?;
958        Ok(len as u32)
959    }
960
961    /// Insert into a text collection at a character position.
962    pub fn data_text_insert(
963        &mut self,
964        space: &str,
965        doc: &str,
966        collection: &str,
967        position: u32,
968        text: &str,
969    ) -> Result<()> {
970        self.with_doc(space, doc, true, |document| {
971            document.text_insert(collection, position as usize, text)
972        })
973    }
974
975    /// Delete characters from a text collection.
976    pub fn data_text_delete(
977        &mut self,
978        space: &str,
979        doc: &str,
980        collection: &str,
981        position: u32,
982        count: u32,
983    ) -> Result<()> {
984        self.with_doc(space, doc, false, |document| {
985            document.text_delete(collection, position as usize, count as usize)
986        })
987    }
988
989    /// The contents of a text collection.
990    pub fn data_text_value(&mut self, space: &str, doc: &str, collection: &str) -> Result<String> {
991        self.read_doc(space, doc, |document| document.text_value(collection))
992    }
993
994    /// Add to a counter collection. Negative values subtract.
995    pub fn data_counter_increment(
996        &mut self,
997        space: &str,
998        doc: &str,
999        collection: &str,
1000        amount: f64,
1001    ) -> Result<()> {
1002        self.with_doc(space, doc, true, |document| {
1003            document.counter_increment(collection, amount)
1004        })
1005    }
1006
1007    /// The value of a counter collection.
1008    pub fn data_counter_value(&mut self, space: &str, doc: &str, collection: &str) -> Result<f64> {
1009        self.read_doc(space, doc, |document| document.counter_value(collection))
1010    }
1011
1012    /// The document's current state as plain JSON.
1013    pub fn data_doc_json(&mut self, space: &str, doc: &str) -> Result<String> {
1014        self.read_doc(space, doc, |document| document.export_json())
1015    }
1016
1017    /// The document's full history, engine-encoded.
1018    ///
1019    /// Half of the escape hatch: this and [`Self::data_doc_json`] are what
1020    /// let an application take its data and leave.
1021    pub fn data_export_raw(&mut self, space: &str, doc: &str) -> Result<Vec<u8>> {
1022        self.read_doc(space, doc, |document| document.export_raw())
1023    }
1024
1025    /// Persist any pending edits to a document.
1026    ///
1027    /// Writes one delta record, then compacts if the log has outgrown the
1028    /// document. The order is load-bearing: the delta is durable before any
1029    /// bookkeeping assumes it, and at compaction the fresh snapshot is
1030    /// durable before the deltas it folded are deleted. A crash at any point
1031    /// leaves duplicate history, which the merge absorbs, rather than a gap,
1032    /// which it cannot.
1033    pub fn data_flush(&mut self, space: &str, doc: &str) -> Result<()> {
1034        Self::validate_ids(space, doc)?;
1035        let storage = self.require_data_storage()?;
1036        self.open_doc(storage.as_ref(), space, doc)?;
1037        self.flush_doc(storage.as_ref(), space, doc)
1038    }
1039
1040    fn flush_doc(
1041        &mut self,
1042        storage: &dyn ProtocolStateStorage,
1043        space: &str,
1044        doc: &str,
1045    ) -> Result<()> {
1046        self.flush_doc_from(storage, space, doc, None)
1047    }
1048
1049    /// [`Self::flush_doc`], told which peer the change came from.
1050    ///
1051    /// `origin` is `None` for a local edit and the peer's address for one
1052    /// applied from a sync frame. It exists so replication does not send a
1053    /// change straight back to whoever sent it: the merge would absorb the
1054    /// echo, but on a mesh it is a frame nobody needed.
1055    fn flush_doc_from(
1056        &mut self,
1057        storage: &dyn ProtocolStateStorage,
1058        space: &str,
1059        doc: &str,
1060        origin: Option<&str>,
1061    ) -> Result<()> {
1062        let map_key = (space.to_string(), doc.to_string());
1063
1064        let Some(entry) = self.data.docs.get_mut(&map_key) else {
1065            return Ok(());
1066        };
1067        let Some(delta) = entry.doc.commit().map_err(map_data_error)? else {
1068            return Ok(());
1069        };
1070        let seq = entry.next_seq;
1071        let delta_bytes = delta.bytes.len();
1072        let history_complete = entry.history_complete;
1073
1074        // A commit too large for one record can never be stored as a delta,
1075        // and refusing it is not a recovery: the rewind below puts the change
1076        // back in the pending set, so the *next* commit exports a strictly
1077        // larger delta and fails the same way forever. A compacted snapshot
1078        // is bounded by the document cap rather than by history, so it is the
1079        // route that still works, and the commit's changes are inside it.
1080        let oversized = delta_bytes > MAX_PROTOCOL_STATE_RECORD_BYTES;
1081
1082        let rewind = |protocol: &mut Self| {
1083            if let Some(entry) = protocol.data.docs.get_mut(&map_key) {
1084                entry.doc.rewind_last_commit();
1085            }
1086        };
1087
1088        if oversized && !history_complete {
1089            // Compaction is the only way out and it is unavailable: folding a
1090            // log with a hole in it would delete records the document still
1091            // needs. Say so rather than looping on an unwritable delta.
1092            rewind(self);
1093            return Err(Error::Other(format!(
1094                "commit for document {space}/{doc} is {delta_bytes} bytes, over the \
1095                 {MAX_PROTOCOL_STATE_RECORD_BYTES} byte record limit, and its delta log \
1096                 is incomplete so it cannot be compacted instead"
1097            )));
1098        }
1099
1100        if oversized {
1101            warn!(
1102                space,
1103                doc, delta_bytes, "Commit is too large for one delta record; compacting instead"
1104            );
1105            if let Err(err) = self.compact_doc(storage, space, doc) {
1106                rewind(self);
1107                return Err(err);
1108            }
1109        } else if let Err(err) = self.write_state_record(
1110            storage,
1111            storage_keys::DATA_DELTA_LOG,
1112            &delta_key(space, doc, seq),
1113            &delta.bytes,
1114        ) {
1115            // Put the export marker back. The commit already advanced it, so
1116            // leaving it advanced would mean this change is never handed out
1117            // again: the document keeps it in memory, the next commit exports
1118            // only what came after, and the change is simply gone at the next
1119            // restart. Re-exporting it later costs at worst a duplicate
1120            // record, which the merge absorbs.
1121            rewind(self);
1122            return Err(Error::Other(format!(
1123                "failed to persist document delta: {err}"
1124            )));
1125        }
1126
1127        let (verdict, measured, should_compact, compacted_before) = {
1128            let entry = self
1129                .data
1130                .docs
1131                .get_mut(&map_key)
1132                .ok_or_else(|| Error::Other("document vanished during flush".to_string()))?;
1133            if !oversized {
1134                entry.next_seq = seq.saturating_add(1);
1135                entry.log_bytes = entry.log_bytes.saturating_add(delta_bytes);
1136            }
1137
1138            let (verdict, measured) = entry.doc.check_size().map_err(map_data_error)?;
1139            if let Some(bytes) = measured {
1140                entry.compacted_bytes = bytes;
1141            }
1142            // Never fold a log that did not fully apply, and never compact
1143            // twice for one flush.
1144            let should_compact = !oversized
1145                && entry.history_complete
1146                && policy::should_compact(
1147                    entry.log_bytes,
1148                    entry.compacted_bytes,
1149                    entry.doc.commits_since_compaction(),
1150                );
1151            (verdict, measured, should_compact, entry.compacted_bytes)
1152        };
1153
1154        // Replicated here for the same reason the event fires here: the
1155        // claim being made is that the change is durable, and that became
1156        // true at the write above. Pushing before it would advertise a
1157        // change that a crash could still take back, and pushing after
1158        // compaction would let a compaction failure suppress replication of
1159        // something already on disk.
1160        //
1161        // The oversized branch wrote a compacted snapshot instead of a delta
1162        // record, so there is no delta to push. Offering versions is how the
1163        // peer finds the gap: waiting for its next offer would leave both
1164        // replicas believing they agree for as long as the link holds, since
1165        // a link that never drops never fires a trigger.
1166        if oversized {
1167            self.nudge_data_sync(space, origin, "oversized_commit");
1168        } else {
1169            self.push_data_delta(space, doc, &delta.bytes, origin);
1170        }
1171
1172        // Emitted here, before compaction: the event's claim is that the
1173        // change reached storage, and that became true at the write above.
1174        // Emitting after compaction would let a compaction failure suppress
1175        // the notification for a change that is already durable.
1176        self.emit_event(Event::DataChanged {
1177            space_id: space.to_string(),
1178            doc_id: doc.to_string(),
1179            delta_bytes: delta_bytes as u64,
1180        });
1181
1182        if should_compact {
1183            self.compact_doc(storage, space, doc)?;
1184        }
1185
1186        match verdict {
1187            policy::SizeVerdict::TooLarge => {
1188                let actual = measured.unwrap_or(compacted_before);
1189                if let Some(entry) = self.data.docs.get_mut(&map_key) {
1190                    entry.over_cap = true;
1191                }
1192                // The change is already durable: refusing to persist it would
1193                // lose work the application believed it had made. What is
1194                // refused is further growth, and deletions stay open so the
1195                // document can be brought back under the cap.
1196                Err(Error::DocTooLarge {
1197                    actual,
1198                    limit: policy::MAX_DOC_BYTES,
1199                })
1200            }
1201            policy::SizeVerdict::Warn => {
1202                if let Some(entry) = self.data.docs.get_mut(&map_key) {
1203                    entry.over_cap = false;
1204                }
1205                self.emit_event(Event::DataDocSizeWarning {
1206                    space_id: space.to_string(),
1207                    doc_id: doc.to_string(),
1208                    compacted_bytes: measured.unwrap_or(compacted_before) as u64,
1209                    cap_bytes: policy::MAX_DOC_BYTES as u64,
1210                });
1211                Ok(())
1212            }
1213            policy::SizeVerdict::Ok => {
1214                if let Some(entry) = self.data.docs.get_mut(&map_key) {
1215                    entry.over_cap = false;
1216                }
1217                Ok(())
1218            }
1219        }
1220    }
1221
1222    /// Fold a document's delta log into a fresh compacted record.
1223    fn compact_doc(
1224        &mut self,
1225        storage: &dyn ProtocolStateStorage,
1226        space: &str,
1227        doc: &str,
1228    ) -> Result<()> {
1229        let map_key = (space.to_string(), doc.to_string());
1230        let compacted = {
1231            let Some(entry) = self.data.docs.get(&map_key) else {
1232                return Ok(());
1233            };
1234            // Folding a log that did not fully apply would write a snapshot
1235            // missing those changes and then delete the records that still
1236            // hold them, turning a recoverable gap into permanent loss. An
1237            // oversized delta log is the cheaper failure.
1238            if !entry.history_complete {
1239                warn!(
1240                    space,
1241                    doc, "Skipping compaction: the document's delta log did not fully apply"
1242                );
1243                return Ok(());
1244            }
1245            entry.doc.export_compacted().map_err(map_data_error)?
1246        };
1247
1248        // Durable first. Everything after this point is cleanup, and a crash
1249        // in the middle of cleanup costs disk, not data.
1250        self.write_state_record(
1251            storage,
1252            storage_keys::DATA_DOCS,
1253            &doc_key(space, doc),
1254            &compacted,
1255        )
1256        .map_err(|err| Error::Other(format!("failed to persist compacted document: {err}")))?;
1257
1258        let prefix = format!("{space}/{doc}/");
1259        if let Ok(keys) = storage.list_keys(storage_keys::DATA_DELTA_LOG) {
1260            for key in keys.iter().filter(|key| key.starts_with(&prefix)) {
1261                let _ = storage.delete(storage_keys::DATA_DELTA_LOG, key);
1262            }
1263        }
1264
1265        if let Some(entry) = self.data.docs.get_mut(&map_key) {
1266            entry.log_bytes = 0;
1267            entry.compacted_bytes = compacted.len();
1268            entry.doc.mark_compacted(compacted.len());
1269        }
1270        debug!(space, doc, bytes = compacted.len(), "Compacted document");
1271        Ok(())
1272    }
1273
1274    /// Persist every open document with pending edits.
1275    ///
1276    /// Called on shutdown, so the debounce window between an edit and its
1277    /// record can never be the reason a change is lost.
1278    pub fn data_flush_all(&mut self) -> Result<()> {
1279        let Ok(storage) = self.require_data_storage() else {
1280            return Ok(());
1281        };
1282        let keys: Vec<(String, String)> = self.data.docs.keys().cloned().collect();
1283        for (space, doc) in keys {
1284            match self.flush_doc(storage.as_ref(), &space, &doc) {
1285                Ok(()) => {}
1286                // Not a failed flush, and not reported as one: the record was
1287                // written and only the size verdict after it refused further
1288                // growth. On a shutdown path the distinction is the whole
1289                // question an operator is asking, which is whether anything
1290                // was lost.
1291                Err(Error::DocTooLarge { .. }) => {
1292                    debug!(
1293                        space,
1294                        doc, "Flushed a document that is over its cap on shutdown"
1295                    );
1296                }
1297                Err(err) => {
1298                    warn!(space, doc, error = %err, "Failed to flush document");
1299                }
1300            }
1301        }
1302        Ok(())
1303    }
1304
1305    /// Delete every record the data layer owns.
1306    ///
1307    /// The logout path for an application that pointed documents at its own
1308    /// backend. `wipePersistedState` clears the account directory of the
1309    /// *default* provider, which a custom backend is not inside, so without
1310    /// this call those documents would outlive the account that made them.
1311    ///
1312    /// **Only durable once replication has stopped.** There are no deletion
1313    /// tombstones in this release, so nothing distinguishes a space this
1314    /// device wiped from one it has never seen. Called while the engine is
1315    /// running with live sessions, every document comes back: the peer's next
1316    /// version offer names them and they are recreated and refilled from the
1317    /// peer's copy, and an offer of our own naming nothing reads as a replica
1318    /// that has never seen the space. On the logout path the engine is being
1319    /// torn down anyway. Anywhere else, stop it first, and only for as long
1320    /// as it stays stopped: the peer still holds the documents, so they return
1321    /// when replication resumes with it. The call clears this device, it does
1322    /// not delete content.
1323    pub fn data_wipe_all(&mut self) -> Result<()> {
1324        let Some(storage) = self.data_storage() else {
1325            return Ok(());
1326        };
1327        self.data.docs.clear();
1328        self.data.spaces.clear();
1329        // Every window and every outstanding question goes with the content
1330        // they were about. Left behind, a window suppresses the first offer
1331        // or the first blob request made after the wipe, which is the same
1332        // shape [`Self::forget_data_sync_peer`] exists to prevent: a window
1333        // that survives the thing it was measuring.
1334        //
1335        // The pending fetches are not bookkeeping. An entry there is what
1336        // admits arriving attachment bytes, so one surviving a wipe would let
1337        // bytes land for a space this device no longer has, and hand them to
1338        // the application as an answer to a question the wipe erased.
1339        self.last_data_sync_offer.clear();
1340        self.pending_attachment_fetches.clear();
1341        self.blob_request_windows.clear();
1342
1343        // Attempt every record and report the first failure, as
1344        // [`Self::data_delete_doc`] does. Answering `Ok` for a wipe that left
1345        // records behind is the worst shape this call can take: it is the
1346        // logout path, so the records it failed to remove outlive the account
1347        // that made them, and the application has no symptom to notice it by.
1348        let mut first_error: Option<String> = None;
1349        let mut record_error = |err: crate::protocol_state_storage::ProtocolStateError| {
1350            if first_error.is_none() {
1351                first_error = Some(err.to_string());
1352            }
1353        };
1354
1355        // Every key type the layer owns, replication bookkeeping included.
1356        // A key type missing from this list is not a partial wipe that
1357        // reports itself, it is a silent one: the records are keyed by peer
1358        // address, so what survives a logout is who this account replicated
1359        // with.
1360        for key_type in [
1361            storage_keys::DATA_DOCS,
1362            storage_keys::DATA_DELTA_LOG,
1363            storage_keys::DATA_SPACES,
1364            storage_keys::DATA_SYNC,
1365        ] {
1366            match storage.list_keys(key_type) {
1367                Ok(keys) => {
1368                    for key in keys {
1369                        if let Err(err) = storage.delete(key_type, &key) {
1370                            record_error(err);
1371                        }
1372                    }
1373                }
1374                Err(err) => record_error(err),
1375            }
1376        }
1377
1378        match first_error {
1379            Some(detail) => Err(Error::Other(format!(
1380                "failed to delete every data record: {detail}"
1381            ))),
1382            None => Ok(()),
1383        }
1384    }
1385
1386    /// The compacted size of a document, in bytes.
1387    pub fn data_doc_size(&mut self, space: &str, doc: &str) -> Result<u64> {
1388        Self::validate_ids(space, doc)?;
1389        let storage = self.require_data_storage()?;
1390        self.open_doc(storage.as_ref(), space, doc)?;
1391        let entry = self
1392            .data
1393            .docs
1394            .get_mut(&(space.to_string(), doc.to_string()))
1395            .ok_or_else(|| Error::Other("document vanished after open".to_string()))?;
1396        let bytes = entry.doc.measure().map_err(map_data_error)?;
1397        entry.compacted_bytes = bytes;
1398        Ok(bytes as u64)
1399    }
1400}