mod vector_clock; use std::{collections::BTreeSet, fmt::Display}; use num_rational::Ratio; use uuid::Uuid; use crate::vector_clock::VectorClock; use thiserror::Error; #[derive(Error, Debug)] pub enum Error { #[error("object with ID {0} not found")] NotFound(DerivedId), } #[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 .ops .last() .map(|Op { clock, .. }| clock.inc(actor_id)) // For an empty document, initialize a new clock .unwrap_or_else(|| VectorClock::default().inc(actor_id)); self.ops.push(Op { id: Uuid::now_v7(), clock, payload, }); } 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(); for op in &self.ops { op.apply(&mut realized)?; } Ok(realized) } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Op { id: Uuid, payload: OpPayload, clock: VectorClock, } impl Op { fn apply(&self, realized: &mut RealizedDoc) -> Result<(), Error> { match &self.payload { OpPayload::CreateGrid { rows, base_cells_per_row, } => { let duration: Ratio = Ratio::new(1, *base_cells_per_row as u32); let rows = (0..*rows) .map(|row_idx| { let cells = (0..*base_cells_per_row) .map(|cell_idx| Cell { id: self.id.derive_id("cell", cell_idx), duration, }) .collect(); Row { id: self.id.derive_id("row", row_idx), cells, } }) .collect(); realized.grids.push(Grid { id: self.id.derive_id("grid", 0), rows, }); } OpPayload::ChangeSubdivisions { grid_id, row_id, start_cell_id, end_cell_id, subdivisions, } => { let grid = realized .grids .iter_mut() .find(|g| g.id == *grid_id) .ok_or(Error::NotFound(grid_id.clone()))?; let row = grid .rows .iter_mut() .find(|r| r.id == *row_id) .ok_or(Error::NotFound(row_id.clone()))?; let start_cell_idx = row .cells .iter() .position(|c| c.id == *start_cell_id) .ok_or(Error::NotFound(start_cell_id.clone()))?; let end_cell_idx = row .cells .iter() .position(|c| c.id == *end_cell_id) .ok_or(Error::NotFound(end_cell_id.clone()))?; let (i, j) = if start_cell_idx <= end_cell_idx { (start_cell_idx, end_cell_idx) } else { (end_cell_idx, start_cell_idx) }; let span_duration: Ratio = row.cells[i..j + 1].iter().map(|cell| cell.duration).sum(); let duration: Ratio = span_duration / *subdivisions as u32; row.cells.splice( i..(j + 1), (0..*subdivisions).map(|subdivision_idx| Cell { id: self.id.derive_id("cell", subdivision_idx), duration, }), ); } } Ok(()) } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum OpPayload { CreateGrid { rows: usize, base_cells_per_row: usize, }, ChangeSubdivisions { grid_id: DerivedId, row_id: DerivedId, start_cell_id: DerivedId, end_cell_id: DerivedId, subdivisions: usize, }, } #[derive(Default, Debug)] pub struct RealizedDoc { grids: Vec, } #[derive(Debug)] pub struct Grid { id: DerivedId, rows: Vec, } #[derive(Debug)] pub struct Row { id: DerivedId, cells: Vec, } #[derive(Debug)] pub struct Cell { id: DerivedId, duration: Ratio, } #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub struct DerivedId { // TODO These IDs can be interned on the Doc id: Uuid, tag: &'static str, index: usize, } impl Display for DerivedId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}:{}", self.id, self.tag, self.index) } } trait DerivableId { fn derive_id(&self, tag: &'static str, index: usize) -> DerivedId; } impl DerivableId for Uuid { fn derive_id(&self, tag: &'static str, index: usize) -> DerivedId { DerivedId { id: self.clone(), tag, index, } } } #[cfg(test)] 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 mut doc1 = Doc::from_ops( &actor1, &[OpPayload::CreateGrid { rows: 4, base_cells_per_row: 16, }], ); let mut doc2 = doc1.clone(); { let realized = doc1.realize().unwrap(); doc1.append_op( &actor1, OpPayload::ChangeSubdivisions { grid_id: realized.grids[0].id, row_id: realized.grids[0].rows[0].id, start_cell_id: realized.grids[0].rows[0].cells[0].id, end_cell_id: realized.grids[0].rows[0].cells[3].id, subdivisions: 3, }, ); doc2.append_op( &actor2, OpPayload::ChangeSubdivisions { grid_id: realized.grids[0].id, row_id: realized.grids[0].rows[0].id, start_cell_id: realized.grids[0].rows[0].cells[0].id, end_cell_id: realized.grids[0].rows[0].cells[3].id, subdivisions: 3, }, ); } doc1.merge(&doc2); assert_eq!(doc1.ops.len(), 3); let realized = doc1.realize().unwrap(); let grid = &realized.grids[0]; let row = &grid.rows[0]; assert_eq!( row.cells .iter() .map(|cell| cell.duration) .sum::>(), Ratio::ONE ); } #[test] fn realize_doc() { let actor_id = Uuid::now_v7(); let mut doc = Doc::default(); doc.append_op( &actor_id, OpPayload::CreateGrid { rows: 4, base_cells_per_row: 16, }, ); { let realized = doc.realize().unwrap(); assert_eq!(realized.grids.len(), 1); let grid = realized.grids.first().unwrap(); assert_eq!(grid.rows.len(), 4); let row = grid.rows.first().unwrap(); assert_eq!(row.cells.len(), 16); assert_eq!( row.cells .iter() .map(|cell| cell.duration) .sum::>(), Ratio::ONE ); doc.append_op( &actor_id, OpPayload::ChangeSubdivisions { grid_id: grid.id.clone(), row_id: row.id.clone(), start_cell_id: row.cells[0].id.clone(), end_cell_id: row.cells[3].id.clone(), subdivisions: 3, }, ); } { let realized = doc.realize().unwrap(); assert_eq!(realized.grids[0].rows[0].cells.len(), 15); assert_eq!(realized.grids[0].rows[1].cells.len(), 16); let grid = &realized.grids[0]; let row = &grid.rows[0]; assert_eq!( row.cells .iter() .map(|cell| cell.duration) .sum::>(), Ratio::ONE ); doc.append_op( &actor_id, OpPayload::ChangeSubdivisions { grid_id: grid.id.clone(), row_id: row.id.clone(), start_cell_id: row.cells[0].id.clone(), end_cell_id: row.cells.last().unwrap().id.clone(), subdivisions: 12, }, ); } { let realized = doc.realize().unwrap(); let grid = &realized.grids[0]; let row = &grid.rows[0]; assert_eq!(row.cells.len(), 12); assert_eq!(realized.grids[0].rows[1].cells.len(), 16); assert_eq!( row.cells .iter() .map(|cell| cell.duration) .sum::>(), Ratio::ONE ); } } }