Skip to main content

mdcs_sdk/
document.rs

1//! Document wrappers for collaborative editing.
2
3use mdcs_core::lattice::Lattice;
4use mdcs_db::{
5    json_crdt::{JsonCrdt, JsonPath, JsonValue},
6    rga_text::RGAText,
7    rich_text::{MarkType, RichText},
8};
9use tokio::sync::broadcast;
10
11/// Events emitted when a document changes.
12#[derive(Clone, Debug)]
13pub enum DocEvent {
14    /// Text was inserted.
15    Insert { position: usize, text: String },
16    /// Text was deleted.
17    Delete { position: usize, length: usize },
18    /// Remote changes were applied.
19    RemoteUpdate,
20}
21
22/// Trait for collaborative documents.
23pub trait CollaborativeDoc {
24    /// Get the document ID.
25    fn id(&self) -> &str;
26
27    /// Get the replica ID.
28    fn replica_id(&self) -> &str;
29
30    /// Subscribe to document events.
31    fn subscribe(&self) -> broadcast::Receiver<DocEvent>;
32
33    /// Take pending deltas for sync.
34    fn take_pending_deltas(&mut self) -> Vec<Vec<u8>>;
35
36    /// Apply a remote delta.
37    fn apply_remote(&mut self, delta: &[u8]);
38}
39
40/// A collaborative plain text document.
41#[derive(Clone)]
42pub struct TextDoc {
43    id: String,
44    replica_id: String,
45    text: RGAText,
46    #[allow(dead_code)]
47    event_tx: broadcast::Sender<DocEvent>,
48    pending_deltas: Vec<Vec<u8>>,
49}
50
51impl TextDoc {
52    /// Create a new text document.
53    pub fn new(id: impl Into<String>, replica_id: impl Into<String>) -> Self {
54        let replica_id = replica_id.into();
55        let (event_tx, _) = broadcast::channel(100);
56
57        Self {
58            id: id.into(),
59            replica_id: replica_id.clone(),
60            text: RGAText::new(&replica_id),
61            event_tx,
62            pending_deltas: Vec::new(),
63        }
64    }
65
66    /// Insert text at position.
67    pub fn insert(&mut self, position: usize, text: &str) {
68        self.text.insert(position, text);
69        let _ = self.event_tx.send(DocEvent::Insert {
70            position,
71            text: text.to_string(),
72        });
73    }
74
75    /// Delete text at position.
76    pub fn delete(&mut self, position: usize, length: usize) {
77        self.text.delete(position, length);
78        let _ = self.event_tx.send(DocEvent::Delete { position, length });
79    }
80
81    /// Get the current text content.
82    pub fn get_text(&self) -> String {
83        self.text.to_string()
84    }
85
86    /// Get the text length.
87    pub fn len(&self) -> usize {
88        self.text.len()
89    }
90
91    /// Check if the document is empty.
92    pub fn is_empty(&self) -> bool {
93        self.text.is_empty()
94    }
95
96    /// Merge another document's state into this one (CRDT merge).
97    /// This applies changes from the other document while preserving local changes.
98    pub fn merge(&mut self, other: &TextDoc) {
99        self.text = self.text.join(&other.text);
100        let _ = self.event_tx.send(DocEvent::RemoteUpdate);
101    }
102
103    /// Clone this document's state for syncing to another replica.
104    pub fn clone_state(&self) -> TextDoc {
105        TextDoc {
106            id: self.id.clone(),
107            replica_id: self.replica_id.clone(),
108            text: self.text.clone(),
109            event_tx: self.event_tx.clone(),
110            pending_deltas: Vec::new(),
111        }
112    }
113}
114
115impl CollaborativeDoc for TextDoc {
116    fn id(&self) -> &str {
117        &self.id
118    }
119
120    fn replica_id(&self) -> &str {
121        &self.replica_id
122    }
123
124    fn subscribe(&self) -> broadcast::Receiver<DocEvent> {
125        self.event_tx.subscribe()
126    }
127
128    fn take_pending_deltas(&mut self) -> Vec<Vec<u8>> {
129        std::mem::take(&mut self.pending_deltas)
130    }
131
132    fn apply_remote(&mut self, _delta: &[u8]) {
133        // In a real implementation, deserialize and apply delta
134        let _ = self.event_tx.send(DocEvent::RemoteUpdate);
135    }
136}
137
138/// A collaborative rich text document with formatting.
139#[derive(Clone)]
140pub struct RichTextDoc {
141    id: String,
142    replica_id: String,
143    text: RichText,
144    #[allow(dead_code)]
145    event_tx: broadcast::Sender<DocEvent>,
146    pending_deltas: Vec<Vec<u8>>,
147}
148
149impl RichTextDoc {
150    /// Create a new rich text document.
151    pub fn new(id: impl Into<String>, replica_id: impl Into<String>) -> Self {
152        let replica_id = replica_id.into();
153        let (event_tx, _) = broadcast::channel(100);
154
155        Self {
156            id: id.into(),
157            replica_id: replica_id.clone(),
158            text: RichText::new(&replica_id),
159            event_tx,
160            pending_deltas: Vec::new(),
161        }
162    }
163
164    /// Insert text at position.
165    pub fn insert(&mut self, position: usize, text: &str) {
166        self.text.insert(position, text);
167        let _ = self.event_tx.send(DocEvent::Insert {
168            position,
169            text: text.to_string(),
170        });
171    }
172
173    /// Delete text at position.
174    pub fn delete(&mut self, position: usize, length: usize) {
175        self.text.delete(position, length);
176        let _ = self.event_tx.send(DocEvent::Delete { position, length });
177    }
178
179    /// Apply formatting to a range.
180    pub fn format(&mut self, start: usize, end: usize, mark: MarkType) {
181        self.text.add_mark(start, end, mark);
182    }
183
184    /// Remove formatting by mark ID.
185    pub fn unformat_by_id(&mut self, mark_id: &mdcs_db::rich_text::MarkId) {
186        self.text.remove_mark(mark_id);
187    }
188
189    /// Get the plain text content.
190    pub fn get_text(&self) -> String {
191        self.text.to_string()
192    }
193
194    /// Get the plain text as spans with marks.
195    /// Note: For full mark information, use the underlying RichText directly.
196    pub fn get_content(&self) -> String {
197        self.text.to_string()
198    }
199
200    /// Get the text length.
201    pub fn len(&self) -> usize {
202        self.text.len()
203    }
204
205    /// Check if the document is empty.
206    pub fn is_empty(&self) -> bool {
207        self.text.is_empty()
208    }
209
210    /// Merge another document's state into this one (CRDT merge).
211    /// This applies changes from the other document while preserving local changes.
212    pub fn merge(&mut self, other: &RichTextDoc) {
213        self.text = self.text.join(&other.text);
214        let _ = self.event_tx.send(DocEvent::RemoteUpdate);
215    }
216
217    /// Clone this document's state for syncing to another replica.
218    pub fn clone_state(&self) -> RichTextDoc {
219        RichTextDoc {
220            id: self.id.clone(),
221            replica_id: self.replica_id.clone(),
222            text: self.text.clone(),
223            event_tx: self.event_tx.clone(),
224            pending_deltas: Vec::new(),
225        }
226    }
227}
228
229impl CollaborativeDoc for RichTextDoc {
230    fn id(&self) -> &str {
231        &self.id
232    }
233
234    fn replica_id(&self) -> &str {
235        &self.replica_id
236    }
237
238    fn subscribe(&self) -> broadcast::Receiver<DocEvent> {
239        self.event_tx.subscribe()
240    }
241
242    fn take_pending_deltas(&mut self) -> Vec<Vec<u8>> {
243        std::mem::take(&mut self.pending_deltas)
244    }
245
246    fn apply_remote(&mut self, _delta: &[u8]) {
247        let _ = self.event_tx.send(DocEvent::RemoteUpdate);
248    }
249}
250
251/// A collaborative JSON document.
252#[derive(Clone)]
253pub struct JsonDoc {
254    id: String,
255    replica_id: String,
256    doc: JsonCrdt,
257    #[allow(dead_code)]
258    event_tx: broadcast::Sender<DocEvent>,
259    pending_deltas: Vec<Vec<u8>>,
260}
261
262impl JsonDoc {
263    /// Create a new JSON document.
264    pub fn new(id: impl Into<String>, replica_id: impl Into<String>) -> Self {
265        let replica_id = replica_id.into();
266        let (event_tx, _) = broadcast::channel(100);
267
268        Self {
269            id: id.into(),
270            replica_id: replica_id.clone(),
271            doc: JsonCrdt::new(&replica_id),
272            event_tx,
273            pending_deltas: Vec::new(),
274        }
275    }
276
277    /// Set a value at a path.
278    pub fn set(&mut self, path: &str, value: JsonValue) {
279        let json_path = JsonPath::parse(path);
280        let _ = self.doc.set(&json_path, value);
281    }
282
283    /// Get a value at a path.
284    pub fn get(&self, path: &str) -> Option<JsonValue> {
285        let json_path = JsonPath::parse(path);
286        self.doc.get(&json_path).cloned()
287    }
288
289    /// Delete a value at a path.
290    pub fn delete(&mut self, path: &str) {
291        let json_path = JsonPath::parse(path);
292        let _ = self.doc.delete(&json_path);
293    }
294
295    /// Get the root value as a serde JSON Value.
296    pub fn root(&self) -> serde_json::Value {
297        self.doc.to_json()
298    }
299
300    /// Get keys at a path.
301    pub fn keys(&self) -> Vec<String> {
302        self.doc.keys()
303    }
304
305    /// Merge another document's state into this one (CRDT merge).
306    /// This applies changes from the other document while preserving local changes.
307    pub fn merge(&mut self, other: &JsonDoc) {
308        self.doc = self.doc.join(&other.doc);
309        let _ = self.event_tx.send(DocEvent::RemoteUpdate);
310    }
311
312    /// Clone this document's state for syncing to another replica.
313    pub fn clone_state(&self) -> JsonDoc {
314        JsonDoc {
315            id: self.id.clone(),
316            replica_id: self.replica_id.clone(),
317            doc: self.doc.clone(),
318            event_tx: self.event_tx.clone(),
319            pending_deltas: Vec::new(),
320        }
321    }
322}
323
324impl CollaborativeDoc for JsonDoc {
325    fn id(&self) -> &str {
326        &self.id
327    }
328
329    fn replica_id(&self) -> &str {
330        &self.replica_id
331    }
332
333    fn subscribe(&self) -> broadcast::Receiver<DocEvent> {
334        self.event_tx.subscribe()
335    }
336
337    fn take_pending_deltas(&mut self) -> Vec<Vec<u8>> {
338        std::mem::take(&mut self.pending_deltas)
339    }
340
341    fn apply_remote(&mut self, _delta: &[u8]) {
342        let _ = self.event_tx.send(DocEvent::RemoteUpdate);
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn test_text_doc() {
352        let mut doc = TextDoc::new("doc-1", "replica-1");
353        doc.insert(0, "Hello");
354        doc.insert(5, " World");
355
356        assert_eq!(doc.get_text(), "Hello World");
357        assert_eq!(doc.len(), 11);
358    }
359
360    #[test]
361    fn test_rich_text_doc() {
362        let mut doc = RichTextDoc::new("doc-1", "replica-1");
363        doc.insert(0, "Hello World");
364        doc.format(0, 5, MarkType::Bold);
365
366        assert_eq!(doc.get_text(), "Hello World");
367    }
368
369    #[test]
370    fn test_json_doc() {
371        let mut doc = JsonDoc::new("doc-1", "replica-1");
372        doc.set("name", JsonValue::String("Alice".to_string()));
373        doc.set("age", JsonValue::Float(30.0));
374
375        assert_eq!(
376            doc.get("name"),
377            Some(JsonValue::String("Alice".to_string()))
378        );
379    }
380}