Skip to main content

boarddown_sync/
ot.rs

1use boarddown_core::{Error, Task, TaskOp, Operation};
2
3pub struct OtEngine;
4
5impl OtEngine {
6    pub fn new() -> Self {
7        Self
8    }
9
10    pub fn transform(&self, op1: &TaskOp, op2: &TaskOp) -> Result<(TaskOp, TaskOp), Error> {
11        if op1.timestamp <= op2.timestamp {
12            return Ok((op1.clone(), op2.clone()));
13        }
14        
15        let transformed_op2 = match (&op1.operation, &op2.operation) {
16            (
17                Operation::SetTitle { old: _, new: new1 },
18                Operation::SetTitle { old: _, new: new2 }
19            ) => {
20                TaskOp {
21                    task_id: op2.task_id.clone(),
22                    operation: Operation::SetTitle {
23                        old: new1.clone(),
24                        new: new2.clone(),
25                    },
26                    timestamp: op2.timestamp,
27                    client_id: op2.client_id,
28                }
29            }
30            (
31                Operation::SetStatus { old: old1, new: _ },
32                Operation::SetStatus { old: _, new: new2 }
33            ) => {
34                TaskOp {
35                    task_id: op2.task_id.clone(),
36                    operation: Operation::SetStatus {
37                        old: old1.clone(),
38                        new: new2.clone(),
39                    },
40                    timestamp: op2.timestamp,
41                    client_id: op2.client_id,
42                }
43            }
44            _ => op2.clone(),
45        };
46        
47        Ok((op1.clone(), transformed_op2))
48    }
49
50    pub fn compose(&self, op1: &TaskOp, op2: &TaskOp) -> Result<TaskOp, Error> {
51        if op1.task_id != op2.task_id {
52            return Err(Error::Conflict("Cannot compose operations on different tasks".to_string()));
53        }
54        
55        let composed = match (&op1.operation, &op2.operation) {
56            (Operation::SetTitle { old, new: _ }, Operation::SetTitle { old: _, new }) => {
57                Operation::SetTitle {
58                    old: old.clone(),
59                    new: new.clone(),
60                }
61            }
62            (Operation::SetStatus { old, new: _ }, Operation::SetStatus { old: _, new }) => {
63                Operation::SetStatus {
64                    old: old.clone(),
65                    new: new.clone(),
66                }
67            }
68            (Operation::AddDependency { dependency }, Operation::RemoveDependency { dependency: rem_dep }) => {
69                if dependency == rem_dep {
70                    return Err(Error::Conflict("Add and remove dependency cancel out".to_string()));
71                }
72                op2.operation.clone()
73            }
74            _ => op2.operation.clone(),
75        };
76        
77        Ok(TaskOp {
78            task_id: op2.task_id.clone(),
79            operation: composed,
80            timestamp: op2.timestamp,
81            client_id: op2.client_id,
82        })
83    }
84
85    pub fn invert(&self, op: &TaskOp) -> Result<TaskOp, Error> {
86        let inverted = match &op.operation {
87            Operation::SetTitle { old, new } => {
88                Operation::SetTitle {
89                    old: new.clone(),
90                    new: old.clone(),
91                }
92            }
93            Operation::SetStatus { old, new } => {
94                Operation::SetStatus {
95                    old: *new,
96                    new: *old,
97                }
98            }
99            Operation::SetMetadata { key, old, new } => {
100                Operation::SetMetadata {
101                    key: key.clone(),
102                    old: new.clone(),
103                    new: old.clone(),
104                }
105            }
106            Operation::AddDependency { dependency } => {
107                Operation::RemoveDependency {
108                    dependency: dependency.clone(),
109                }
110            }
111            Operation::RemoveDependency { dependency } => {
112                Operation::AddDependency {
113                    dependency: dependency.clone(),
114                }
115            }
116            Operation::Move { from_column, to_column } => {
117                Operation::Move {
118                    from_column: to_column.clone(),
119                    to_column: from_column.clone(),
120                }
121            }
122            Operation::Create { title, column } => {
123                Operation::Delete
124            }
125            Operation::Delete => {
126                return Err(Error::Conflict("Cannot invert delete operation".to_string()));
127            }
128        };
129        
130        Ok(TaskOp {
131            task_id: op.task_id.clone(),
132            operation: inverted,
133            timestamp: op.timestamp,
134            client_id: op.client_id,
135        })
136    }
137
138    pub fn apply(&self, task: &mut Task, op: &TaskOp) -> Result<(), Error> {
139        match &op.operation {
140            Operation::Delete => {
141                return Err(Error::Conflict("Use board-level delete, not task-level".to_string()));
142            }
143            Operation::Create { .. } => {
144                return Err(Error::Conflict("Cannot apply create operation via apply()".to_string()));
145            }
146            _ => {
147                op.operation.apply_to_task(task);
148            }
149        }
150        Ok(())
151    }
152}
153
154impl Default for OtEngine {
155    fn default() -> Self {
156        Self::new()
157    }
158}