Skip to main content

boarddown_sync/
crdt.rs

1use boarddown_core::{Board, Error, Task, TaskId, TaskOp, VectorClock, Operation, Status};
2
3pub struct CrdtEngine {
4    clock: VectorClock,
5}
6
7impl CrdtEngine {
8    pub fn new() -> Self {
9        Self {
10            clock: VectorClock::new(),
11        }
12    }
13
14    pub fn merge(&self, local: &Board, remote: &Board) -> Result<Board, Error> {
15        let mut merged = local.clone();
16        
17        for (task_id, remote_task) in &remote.tasks {
18            if let Some(local_task) = merged.tasks.get(task_id) {
19                merged.tasks.insert(task_id.clone(), self.merge_task(local_task, remote_task)?);
20            } else {
21                merged.tasks.insert(task_id.clone(), remote_task.clone());
22            }
23        }
24        
25        for col in &remote.columns {
26            if !merged.columns.iter().any(|c| c.name == col.name) {
27                merged.columns.push(col.clone());
28            }
29        }
30        
31        Ok(merged)
32    }
33
34    pub fn merge_task(&self, local: &Task, remote: &Task) -> Result<Task, Error> {
35        if local.updated_at > remote.updated_at {
36            return Ok(local.clone());
37        }
38        if remote.updated_at > local.updated_at {
39            return Ok(remote.clone());
40        }
41        
42        let mut merged = local.clone();
43        
44        if local.title != remote.title {
45            if remote.title.len() > local.title.len() {
46                merged.title = remote.title.clone();
47            }
48        }
49        
50        if local.status != remote.status {
51            let status_order = |s: &Status| match s {
52                Status::Urgent => 5,
53                Status::InProgress => 4,
54                Status::Ready => 3,
55                Status::Blocked => 2,
56                Status::Todo => 1,
57                Status::Done => 0,
58            };
59            if status_order(&remote.status) > status_order(&local.status) {
60                merged.status = remote.status;
61            }
62        }
63        
64        for dep in &remote.dependencies {
65            if !merged.dependencies.contains(dep) {
66                merged.dependencies.push(dep.clone());
67            }
68        }
69        
70        merged.metadata.merge(&remote.metadata);
71        
72        Ok(merged)
73    }
74
75    pub fn transform(&self, op1: &TaskOp, op2: &TaskOp) -> Result<(TaskOp, TaskOp), Error> {
76        if op1.timestamp >= op2.timestamp {
77            return Ok((op1.clone(), op2.clone()));
78        }
79        
80        let transformed_op2 = match (&op1.operation, &op2.operation) {
81            (
82                Operation::SetTitle { old: _, new: new1 },
83                Operation::SetTitle { old: _, new: new2 }
84            ) => {
85                TaskOp {
86                    task_id: op2.task_id.clone(),
87                    operation: Operation::SetTitle {
88                        old: new1.clone(),
89                        new: new2.clone(),
90                    },
91                    timestamp: op2.timestamp,
92                    client_id: op2.client_id,
93                }
94            }
95            (
96                Operation::SetStatus { old: _, new: new1 },
97                Operation::SetStatus { old: _, new: new2 }
98            ) => {
99                TaskOp {
100                    task_id: op2.task_id.clone(),
101                    operation: Operation::SetStatus {
102                        old: new1.clone(),
103                        new: new2.clone(),
104                    },
105                    timestamp: op2.timestamp,
106                    client_id: op2.client_id,
107                }
108            }
109            _ => op2.clone(),
110        };
111        
112        Ok((op1.clone(), transformed_op2))
113    }
114
115    pub fn apply(&self, board: &mut Board, op: TaskOp) -> Result<(), Error> {
116        match &op.operation {
117            Operation::Create { title, column } => {
118                let task = Task::builder()
119                    .id(op.task_id.clone())
120                    .title(title)
121                    .column(column)
122                    .build()
123                    .map_err(|e| Error::Validation(e))?;
124                board.tasks.insert(op.task_id, task);
125            }
126            Operation::Delete => {
127                board.tasks.remove(&op.task_id);
128            }
129            _ => {
130                if let Some(task) = board.tasks.get_mut(&op.task_id) {
131                    op.operation.apply_to_task(task);
132                }
133            }
134        }
135        Ok(())
136    }
137}
138
139impl Default for CrdtEngine {
140    fn default() -> Self {
141        Self::new()
142    }
143}