From 72be193fa1db221ecdc0c5e697643236f70d2f3d Mon Sep 17 00:00:00 2001 From: Josh Kingsley Date: Sat, 15 Nov 2025 22:04:16 +0200 Subject: feat(crdt): add Doc::merge method --- crdt/src/lib.rs | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 4 deletions(-) (limited to 'crdt') diff --git a/crdt/src/lib.rs b/crdt/src/lib.rs index 39c5f0e..3983692 100644 --- a/crdt/src/lib.rs +++ b/crdt/src/lib.rs @@ -1,6 +1,6 @@ mod vector_clock; -use std::fmt::Display; +use std::{collections::BTreeSet, fmt::Display}; use uuid::Uuid; @@ -14,12 +14,32 @@ pub enum Error { NotFound(DerivedId), } -#[derive(Default)] +#[derive(Default, Clone)] pub struct Doc { ops: Vec, } impl Doc { + pub fn from_ops(actor_id: &Uuid, payloads: &[OpPayload]) -> Self { + let mut clock = VectorClock::new(); + + let ops = payloads + .iter() + .cloned() + .map(|payload| { + clock = clock.inc(&actor_id); + + Op { + id: Uuid::now_v7(), + clock: clock.clone(), + payload, + } + }) + .collect(); + + Doc { ops } + } + pub fn append_op(&mut self, actor_id: &Uuid, payload: OpPayload) { // Increment the last clock for the provided actor let clock = self @@ -36,6 +56,18 @@ impl Doc { }); } + pub fn merge(&mut self, other: &Doc) { + let op_ids: BTreeSet = self.ops.iter().map(|op| op.id).collect(); + + for op in &other.ops { + if !op_ids.contains(&op.id) { + self.ops.push(op.clone()); + } + } + + self.ops.sort_by(|op1, op2| op1.clock.cmp(&op2.clock)); + } + pub fn realize(&self) -> Result { let mut realized = RealizedDoc::default(); @@ -47,7 +79,7 @@ impl Doc { } } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Op { id: Uuid, payload: OpPayload, @@ -132,7 +164,7 @@ impl Op { } } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum OpPayload { CreateGrid { rows: usize, @@ -202,6 +234,51 @@ impl DerivableId for Uuid { mod tests { use super::*; + #[test] + fn merge() { + let actor1 = Uuid::now_v7(); + let actor2 = Uuid::now_v7(); + + let mut doc1 = Doc::from_ops( + &actor1, + &[OpPayload::CreateGrid { + rows: 4, + base_cells_per_row: 16, + }], + ); + + let mut doc2 = Doc::from_ops( + &actor2, + &[OpPayload::CreateGrid { + rows: 4, + base_cells_per_row: 16, + }], + ); + + doc1.merge(&doc2); + + assert_eq!(doc1.ops.len(), 2); + assert_eq!(doc1.ops.last().unwrap(), doc2.ops.last().unwrap()); + + doc2.merge(&doc1); + + assert_eq!(doc2.ops.len(), 2); + } + + #[test] + fn concurrent_ops() { + let actor1 = Uuid::now_v7(); + let actor2 = Uuid::now_v7(); + + let doc = Doc::from_ops( + &actor1, + &[OpPayload::CreateGrid { + rows: 4, + base_cells_per_row: 16, + }], + ); + } + #[test] fn realize_doc() { let actor_id = Uuid::now_v7(); -- cgit v1.2.3