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}