1mod crdt;
2mod ot;
3mod conflict;
4
5pub use crdt::CrdtEngine;
6pub use ot::OtEngine;
7pub use conflict::ConflictResolver;
8pub use conflict::Resolution;
9
10use boarddown_core::{BoardId, Changeset, ClientId, Error, Storage, Version, TaskOp};
11
12pub struct SyncEngine {
13 client_id: ClientId,
14 storage: Option<Box<dyn Storage>>,
15 #[allow(dead_code)]
16 remote_url: Option<String>,
17}
18
19impl SyncEngine {
20 pub fn new() -> Self {
21 Self {
22 client_id: ClientId::new(),
23 storage: None,
24 remote_url: None,
25 }
26 }
27
28 pub fn with_storage(mut self, storage: Box<dyn Storage>) -> Self {
29 self.storage = Some(storage);
30 self
31 }
32
33 #[doc(hidden)]
38 pub fn with_remote(mut self, url: &str) -> Self {
39 self.remote_url = Some(url.to_string());
40 self
41 }
42
43 pub fn client_id(&self) -> &ClientId {
44 &self.client_id
45 }
46
47 pub async fn sync(&self, board_id: &BoardId) -> Result<SyncResult, Error> {
48 if self.storage.is_none() {
49 return Err(Error::Sync("No storage configured".to_string()));
50 }
51
52 let storage = self.storage.as_ref().unwrap();
53
54 let local_board = storage.load_board(board_id).await?;
55
56 let local_version = Version::initial();
57
58 let remote_changeset = storage.get_changeset(board_id, local_version).await?;
59
60 let mut merged_board = local_board.clone();
61
62 remote_changeset.apply(&mut merged_board)?;
63
64 storage.save_board(&merged_board).await?;
65
66 Ok(SyncResult {
67 pushed: 0,
68 pulled: remote_changeset.operations.len(),
69 conflicts: vec![],
70 })
71 }
72
73 pub async fn push(&self, board_id: &BoardId, changes: &[TaskOp]) -> Result<usize, Error> {
74 if self.storage.is_none() {
75 return Err(Error::Sync("No storage configured".to_string()));
76 }
77
78 let storage = self.storage.as_ref().unwrap();
79
80 let mut board = storage.load_board(board_id).await?;
81
82 for op in changes {
83 let mut changeset = Changeset::new(board_id.clone(), Version::initial());
84 changeset.add_operation(op.clone());
85 changeset.apply(&mut board)?;
86 }
87
88 storage.save_board(&board).await?;
89
90 Ok(changes.len())
91 }
92
93 pub async fn pull(&self, board_id: &BoardId, since: Version) -> Result<Changeset, Error> {
94 if self.storage.is_none() {
95 return Err(Error::Sync("No storage configured".to_string()));
96 }
97
98 let storage = self.storage.as_ref().unwrap();
99 storage.get_changeset(board_id, since).await
100 }
101
102 pub async fn get_local_version(&self, board_id: &BoardId) -> Result<Version, Error> {
103 if self.storage.is_none() {
104 return Err(Error::Sync("No storage configured".to_string()));
105 }
106
107 let storage = self.storage.as_ref().unwrap();
108 let board = storage.load_board(board_id).await?;
109
110 Ok(Version(board.tasks.len() as u64))
111 }
112}
113
114impl Default for SyncEngine {
115 fn default() -> Self {
116 Self::new()
117 }
118}
119
120#[derive(Debug, Clone)]
121pub struct SyncResult {
122 pub pushed: usize,
123 pub pulled: usize,
124 pub conflicts: Vec<Conflict>,
125}
126
127#[derive(Debug, Clone)]
128pub struct Conflict {
129 pub task_id: boarddown_core::TaskId,
130 pub local: boarddown_core::TaskOp,
131 pub remote: boarddown_core::TaskOp,
132}