Skip to main content

safemesh_crdt/
replica.rs

1// SafeMesh — delta-state CRDT convergence, built on crdt-lean.
2// Copyright (C) 2026 Ben Cassie
3// SPDX-License-Identifier: Apache-2.0
4//! In-memory coordination, without filesystem durability or writer identity.
5use crate::{
6    Admission, AppendError, CollectionLimits, CollisionVerdict, Crdt, DecodeError, DecodeLimits,
7    EventLog, Record, RecordCollision, RecordId, VersionVector, WireDecode, WireEncode, WireError,
8    WireSchema,
9};
10use alloc::vec::Vec;
11
12/// The operation that failed; invalid admission remains a verdict.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum ReplicaError {
15    RecordDecode(WireError),
16    LogDecode(DecodeError),
17    LogEncode(WireError),
18    Append(AppendError),
19    /// A collision report was refused whole; no alarm changed.
20    ReportDecode(DecodeError),
21    ReportEncode(WireError),
22}
23impl core::fmt::Display for ReplicaError {
24    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
25        match self {
26            Self::RecordDecode(e) | Self::LogEncode(e) | Self::LogDecode(DecodeError::Wire(e)) => {
27                e.fmt(f)
28            }
29            Self::LogDecode(DecodeError::RecordLimitExceeded { max_records }) => {
30                write!(f, "RecordLimitExceeded: {max_records}")
31            }
32            Self::LogDecode(DecodeError::DefaultRecordLimitExceeded { max_records }) => {
33                write!(f, "RecordLimitExceeded: {max_records}; pass DecodeLimits::UNLIMITED to reopen")
34            }
35            Self::Append(e) => e.fmt(f),
36            Self::ReportDecode(DecodeError::Wire(e)) => {
37                write!(f, "failed to decode collision report: {e}")
38            }
39            Self::ReportDecode(DecodeError::RecordLimitExceeded { max_records }) => write!(
40                f,
41                "failed to decode collision report: RecordLimitExceeded: {max_records}"
42            ),
43            Self::ReportDecode(DecodeError::DefaultRecordLimitExceeded { max_records }) => write!(
44                f,
45                "failed to decode collision report: RecordLimitExceeded: {max_records}; pass DecodeLimits::UNLIMITED to reopen"
46            ),
47            Self::ReportEncode(e) => write!(f, "failed to encode collision report: {e}"),
48        }
49    }
50}
51impl core::error::Error for ReplicaError {
52    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
53        match self {
54            Self::RecordDecode(e) | Self::LogEncode(e) | Self::LogDecode(DecodeError::Wire(e)) => {
55                Some(e)
56            }
57            Self::ReportDecode(DecodeError::Wire(e)) | Self::ReportEncode(e) => Some(e),
58            Self::Append(e) => Some(e),
59            Self::LogDecode(DecodeError::RecordLimitExceeded { .. })
60            | Self::ReportDecode(DecodeError::RecordLimitExceeded { .. })
61            | Self::LogDecode(DecodeError::DefaultRecordLimitExceeded { .. })
62            | Self::ReportDecode(DecodeError::DefaultRecordLimitExceeded { .. }) => None,
63        }
64    }
65}
66/// Owned state and admitted history. A nonempty initial state need not be
67/// reproducible from its initially empty log.
68pub struct Replica<C: Crdt> {
69    state: C,
70    log: EventLog<C::Delta>,
71}
72impl<C: Crdt> Replica<C> {
73    pub fn new(state: C) -> Self {
74        let log = EventLog::for_crdt(&state);
75        Self { state, log }
76    }
77    pub fn state(&self) -> &C {
78        &self.state
79    }
80    pub fn log(&self) -> &EventLog<C::Delta> {
81        &self.log
82    }
83    pub fn version(&self) -> &VersionVector {
84        self.log.version()
85    }
86    pub fn since(&self, peer: &VersionVector) -> Vec<Record<C::Delta>>
87    where
88        C::Delta: Clone,
89    {
90        self.log.since(peer)
91    }
92    /// A Collision verdict also raises the collision alarm; see
93    /// [`Self::collisions`] and [`Self::collision_report_bytes`].
94    pub fn admit(&mut self, record: Record<C::Delta>) -> Admission
95    where
96        C::Delta: Clone + PartialEq,
97    {
98        self.log
99            .admit_with(&mut self.state, record, |state, delta| {
100                state.apply_delta(delta.clone())
101            })
102    }
103    pub fn append(&mut self, author: u64, delta: C::Delta) -> Result<Record<C::Delta>, ReplicaError>
104    where
105        C::Delta: Clone + PartialEq,
106    {
107        let id = self
108            .log
109            .append_with(&mut self.state, author, delta.clone(), |state, delta| {
110                state.apply_delta(delta.clone())
111            })
112            .map_err(ReplicaError::Append)?;
113        Ok(Record { id, delta })
114    }
115    /// Decode only; no admission validation or mutation.
116    pub fn inspect_record_bytes(
117        bytes: &[u8],
118        limits: CollectionLimits,
119    ) -> Result<Record<C::Delta>, ReplicaError>
120    where
121        C::Delta: WireDecode,
122    {
123        Record::from_wire_bytes_with_collection_limits(bytes, limits)
124            .map_err(ReplicaError::RecordDecode)
125    }
126    pub fn merge_record_bytes(
127        &mut self,
128        bytes: &[u8],
129        limits: CollectionLimits,
130    ) -> Result<Admission, ReplicaError>
131    where
132        C::Delta: Clone + PartialEq + WireDecode,
133    {
134        Ok(self.admit(Self::inspect_record_bytes(bytes, limits)?))
135    }
136    /// Validate the complete batch and preserve every input occurrence.
137    pub fn decode_log_bytes(
138        &self,
139        bytes: &[u8],
140        limits: DecodeLimits,
141    ) -> Result<Vec<Record<C::Delta>>, ReplicaError>
142    where
143        C::Delta: Clone + PartialEq + WireDecode + WireSchema,
144    {
145        EventLog::records_from_wire_bytes_for_with_limits(bytes, &self.state, limits)
146            .map_err(ReplicaError::LogDecode)
147    }
148    pub fn merge_log_bytes(
149        &mut self,
150        bytes: &[u8],
151        limits: DecodeLimits,
152    ) -> Result<Vec<Admission>, ReplicaError>
153    where
154        C::Delta: Clone + PartialEq + WireDecode + WireSchema,
155    {
156        let records = self.decode_log_bytes(bytes, limits)?;
157        Ok(records
158            .into_iter()
159            .map(|record| self.admit(record))
160            .collect())
161    }
162    pub fn log_bytes(&self) -> Result<Vec<u8>, ReplicaError>
163    where
164        C::Delta: WireEncode + WireSchema,
165    {
166        self.log.to_wire_bytes().map_err(ReplicaError::LogEncode)
167    }
168    /// Supply a matching empty/bottom state. Publish only a fully checked candidate.
169    pub fn restore(mut state: C, bytes: &[u8], limits: DecodeLimits) -> Result<Self, ReplicaError>
170    where
171        C::Delta: Clone + PartialEq + WireDecode + WireSchema,
172    {
173        let log = EventLog::from_wire_bytes_for_with_limits(bytes, &state, limits)
174            .map_err(ReplicaError::LogDecode)?;
175        for record in log.records() {
176            state.apply_delta(record.delta.clone());
177        }
178        Ok(Self { state, log })
179    }
180
181    /// Held record IDs a peer holds with a different payload, raised when a
182    /// peer's offer collided here or a peer's report named one. In memory
183    /// only; [`Self::restore`] starts with none.
184    pub fn collisions(&self) -> Vec<RecordCollision<C::Delta>>
185    where
186        C::Delta: Clone,
187    {
188        self.log.collisions()
189    }
190    /// The collision report to send back to a peer after merging its batch,
191    /// or `None` when no alarm is raised here.
192    pub fn collision_report_bytes(&self) -> Result<Option<Vec<u8>>, ReplicaError>
193    where
194        C::Delta: Clone + WireEncode + WireSchema,
195    {
196        self.log
197            .collision_report_bytes()
198            .map_err(ReplicaError::ReportEncode)
199    }
200    /// Merge a peer's collision report; one verdict per entry, in order.
201    /// Only the alarm can change: never the records, version or state.
202    pub fn merge_collision_report_bytes(
203        &mut self,
204        bytes: &[u8],
205        limits: DecodeLimits,
206    ) -> Result<Vec<(RecordId, CollisionVerdict)>, ReplicaError>
207    where
208        C::Delta: PartialEq + WireDecode + WireSchema,
209    {
210        self.log
211            .merge_collision_report_bytes(bytes, limits)
212            .map_err(ReplicaError::ReportDecode)
213    }
214}