Skip to main content

spacedb_consistency/
causal.rs

1//! The Causal+ (session) tier — read-your-writes and monotonic reads, no consensus.
2//!
3//! A [`CausalSession`] carries a **causal token**: the state-vector frontier it has
4//! observed. A causal read of a local replica is served only if the replica has
5//! caught up to that frontier; otherwise it reports [`Outcome::Stale`] rather than
6//! silently serving older data. This gives the two session guarantees people
7//! actually want — *I see my own writes*, and *my reads never go backwards* —
8//! cheaply and partition-tolerantly, built directly on the convergent substrate's
9//! state vectors. No cross-node coordination is involved.
10
11use spacedb_crdt::CrdtDoc;
12
13use crate::outcome::Outcome;
14use crate::tier::Tier;
15
16/// A causal-consistency session over one or more replicas of a document.
17#[derive(Clone, Debug, Default)]
18pub struct CausalSession {
19    /// The most-advanced frontier this session has observed (empty = none yet).
20    token: Vec<u8>,
21}
22
23impl CausalSession {
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// The session's causal token (the observed state-vector frontier).
29    pub fn token(&self) -> &[u8] {
30        &self.token
31    }
32
33    /// Record a local write: the session has now observed everything in `doc`,
34    /// including the write just made (read-your-writes). Returns [`Outcome::Local`]
35    /// — written and offline-durable, propagation is asynchronous.
36    pub fn record_write(&mut self, doc: &CrdtDoc) -> Outcome {
37        self.token = doc.state_vector();
38        Outcome::Local
39    }
40
41    /// Attempt a causal read of `doc`. If `doc` has caught up to the session's
42    /// frontier, the read is served (`Committed(Causal)`) and the token advances
43    /// to `doc`'s (≥) frontier — so the session never regresses (monotonic reads).
44    /// If `doc` is behind, the read is honestly [`Outcome::Stale`] and the token
45    /// is *not* advanced.
46    pub fn read(&mut self, doc: &CrdtDoc) -> Outcome {
47        let lag = if self.token.is_empty() {
48            0
49        } else {
50            doc.ops_behind(&self.token).unwrap_or(0)
51        };
52        if lag == 0 {
53            self.token = doc.state_vector();
54            Outcome::Committed(Tier::Causal)
55        } else {
56            Outcome::Stale { lag_ops: lag }
57        }
58    }
59}