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}