Skip to main content

boarddown_sync/
lib.rs

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    /// Configure remote sync server URL.
34    /// 
35    /// **Note**: Remote sync is not yet implemented. This stores the URL for future use
36    /// when WebSocket sync is fully implemented. Currently, sync only works with local storage.
37    #[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}