Skip to main content

communitas_core/
message_sync.rs

1// Copyright (c) 2025 Saorsa Labs Limited
2//
3// This file is part of the Saorsa P2P network.
4//
5// Licensed under the AGPL-3.0 license:
6// <https://www.gnu.org/licenses/agpl-3.0.html>
7
8//! Message Synchronization Service
9//!
10//! Handles CRDT-based message synchronization:
11//! - get_all_messages() for full sync requests
12//! - Out-of-order message detection and queuing
13//! - Missing message reply mechanism
14//! - Causal consistency enforcement
15
16use crate::crdt::*;
17use crate::error::AppResult;
18use std::collections::HashMap;
19use std::sync::Arc;
20use tokio::sync::RwLock;
21use tracing::{debug, info, warn};
22
23/// Message synchronization service
24#[derive(Debug)]
25pub struct MessageSyncService {
26    /// Our peer ID (four-word address)
27    peer_id: String,
28
29    /// Vector clock per entity
30    entity_clocks: Arc<RwLock<HashMap<String, VectorClock>>>,
31
32    /// Message storage per entity
33    entity_messages: Arc<RwLock<HashMap<String, Vec<CRDTMessage>>>>,
34
35    /// Out-of-order messages waiting for dependencies
36    pending_messages: Arc<RwLock<HashMap<String, Vec<CRDTMessage>>>>,
37
38    /// Global Lamport clock for total ordering
39    lamport_clock: Arc<RwLock<u64>>,
40}
41
42impl MessageSyncService {
43    /// Create a new message sync service
44    pub fn new(peer_id: String) -> Self {
45        info!("🔄 MessageSyncService initialized for peer: {}", peer_id);
46
47        Self {
48            peer_id,
49            entity_clocks: Arc::new(RwLock::new(HashMap::new())),
50            entity_messages: Arc::new(RwLock::new(HashMap::new())),
51            pending_messages: Arc::new(RwLock::new(HashMap::new())),
52            lamport_clock: Arc::new(RwLock::new(0)),
53        }
54    }
55
56    /// Get all messages for an entity (contact, group, project, org, channel)
57    /// This is the entry point for sync requests from other peers
58    pub async fn get_all_messages(&self, entity_id: &str) -> AppResult<SyncResponse> {
59        let messages_map = self.entity_messages.read().await;
60        let clocks_map = self.entity_clocks.read().await;
61
62        let mut messages = messages_map.get(entity_id).cloned().unwrap_or_default();
63
64        let vector_clock = clocks_map.get(entity_id).cloned().unwrap_or_default();
65
66        // Sort in causal order before returning
67        sort_messages_causally(&mut messages);
68
69        info!(
70            "📤 get_all_messages for {}: {} messages",
71            entity_id,
72            messages.len()
73        );
74
75        Ok(SyncResponse {
76            entity_id: entity_id.to_string(),
77            entity_type: self.infer_entity_type(entity_id),
78            messages,
79            vector_clock,
80        })
81    }
82
83    /// Handle incoming message - detect out-of-order and missing dependencies
84    pub async fn receive_message(&self, message: CRDTMessage) -> AppResult<ReceiveResult> {
85        let entity_id = message.metadata.entity_id.clone();
86        let clocks_map = self.entity_clocks.read().await;
87
88        let local_clock = clocks_map.get(&entity_id).cloned().unwrap_or_default();
89
90        // Release read lock before potential writes
91        drop(clocks_map);
92
93        // Check if we have all causal dependencies
94        let has_deps = local_clock.has_dependencies(&message.metadata.vector_clock);
95
96        if !has_deps {
97            // Message is out of order - store in pending queue
98            warn!("⚠️  Out-of-order message detected: {}", message.metadata.id);
99
100            let mut pending_map = self.pending_messages.write().await;
101            let pending = pending_map.entry(entity_id.clone()).or_default();
102            pending.push(message.clone());
103
104            // Calculate what we're missing
105            let missing = local_clock.get_missing_ranges(&message.metadata.vector_clock);
106
107            return Ok(ReceiveResult {
108                accepted: false,
109                out_of_order: true,
110                missing_ranges: Some(missing),
111            });
112        }
113
114        // Message has all dependencies - accept it
115        self.add_message(message).await?;
116
117        // Try to process pending messages
118        self.process_pending_messages(&entity_id).await?;
119
120        Ok(ReceiveResult {
121            accepted: true,
122            out_of_order: false,
123            missing_ranges: None,
124        })
125    }
126
127    /// Send a new message - assigns vector clock and Lamport timestamp
128    pub async fn send_message(
129        &self,
130        entity_id: String,
131        entity_type: EntityType,
132        content: MessageContent,
133        reply_to_id: Option<String>,
134    ) -> AppResult<CRDTMessage> {
135        // Increment our vector clock
136        let mut clocks_map = self.entity_clocks.write().await;
137        let clock = clocks_map.entry(entity_id.clone()).or_default();
138        clock.increment(&self.peer_id);
139        let new_clock = clock.clone();
140        drop(clocks_map);
141
142        // Increment Lamport clock
143        let mut lamport = self.lamport_clock.write().await;
144        *lamport += 1;
145        let lamport_value = *lamport;
146        drop(lamport);
147
148        // Get previous message for causal chain
149        let messages_map = self.entity_messages.read().await;
150        let previous_id = messages_map
151            .get(&entity_id)
152            .and_then(|msgs| msgs.last())
153            .map(|msg| msg.metadata.id.clone());
154        drop(messages_map);
155
156        let metadata = MessageMetadata {
157            id: format!(
158                "{}-{}-{}",
159                self.peer_id,
160                new_clock.0.get(&self.peer_id).copied().unwrap_or(0),
161                chrono::Utc::now().timestamp_millis()
162            ),
163            entity_id: entity_id.clone(),
164            entity_type,
165            author_peer_id: self.peer_id.clone(),
166            vector_clock: new_clock,
167            lamport_clock: lamport_value,
168            timestamp: chrono::Utc::now().timestamp_millis() as u64,
169            previous_message_id: previous_id,
170            reply_to_id,
171        };
172
173        let message = CRDTMessage {
174            content,
175            metadata,
176            local_state: Some(LocalMessageState {
177                status: Some(MessageStatus::Sent),
178                reactions: Vec::new(),
179                thread_count: None,
180                latest_reply_by: None,
181            }),
182        };
183
184        self.add_message(message.clone()).await?;
185
186        Ok(message)
187    }
188
189    /// Request sync from a peer - send our vector clock and get missing messages
190    pub async fn request_sync(
191        &self,
192        entity_id: &str,
193        from_peer_id: &str,
194    ) -> AppResult<SyncRequest> {
195        let clocks_map = self.entity_clocks.read().await;
196        let local_clock = clocks_map.get(entity_id).cloned().unwrap_or_default();
197        drop(clocks_map);
198
199        // Check for specific missing messages
200        let pending_map = self.pending_messages.read().await;
201        let missing_ids = pending_map
202            .get(entity_id)
203            .map(|pending| pending.iter().map(|m| m.metadata.id.clone()).collect());
204        drop(pending_map);
205
206        debug!("🔄 Requesting sync for {} from {}", entity_id, from_peer_id);
207        debug!("   Local clock: {:?}", local_clock);
208        debug!("   Missing messages: {:?}", missing_ids);
209
210        Ok(SyncRequest {
211            entity_id: entity_id.to_string(),
212            entity_type: self.infer_entity_type(entity_id),
213            requester_peer_id: self.peer_id.clone(),
214            vector_clock: local_clock,
215            missing_message_ids: missing_ids,
216        })
217    }
218
219    /// Handle sync response - integrate received messages
220    pub async fn handle_sync_response(&self, response: SyncResponse) -> AppResult<SyncResult> {
221        let entity_id = &response.entity_id;
222        let mut added = 0;
223        let mut rejected = 0;
224
225        info!(
226            "📥 Handling sync response for {}: {} messages",
227            entity_id,
228            response.messages.len()
229        );
230
231        // Process each message
232        for message in response.messages {
233            let result = self.receive_message(message).await?;
234            if result.accepted {
235                added += 1;
236            } else {
237                rejected += 1;
238            }
239        }
240
241        // Merge remote clock into ours
242        let mut clocks_map = self.entity_clocks.write().await;
243        let local_clock = clocks_map.entry(entity_id.clone()).or_default();
244        local_clock.merge(&response.vector_clock);
245        let merged_clock = local_clock.clone();
246        drop(clocks_map);
247
248        info!("✅ Sync complete: {} added, {} rejected", added, rejected);
249        debug!("   Updated clock: {:?}", merged_clock);
250
251        Ok(SyncResult {
252            messages_added: added,
253            messages_rejected: rejected,
254        })
255    }
256
257    /// Get sync state for an entity
258    pub async fn get_sync_state(&self, entity_id: &str) -> AppResult<EntitySyncState> {
259        let messages_map = self.entity_messages.read().await;
260        let pending_map = self.pending_messages.read().await;
261        let clocks_map = self.entity_clocks.read().await;
262
263        let messages = messages_map.get(entity_id).cloned().unwrap_or_default();
264        let pending = pending_map.get(entity_id).cloned().unwrap_or_default();
265        let clock = clocks_map.get(entity_id).cloned().unwrap_or_default();
266
267        Ok(EntitySyncState {
268            entity_id: entity_id.to_string(),
269            entity_type: self.infer_entity_type(entity_id),
270            vector_clock: clock,
271            last_sync_time: chrono::Utc::now().timestamp_millis() as u64,
272            message_count: messages.len(),
273            missing_messages: pending.iter().map(|m| m.metadata.id.clone()).collect(),
274            out_of_order_messages: pending.iter().map(|m| m.metadata.id.clone()).collect(),
275        })
276    }
277
278    /// Get all messages in causal order for an entity
279    pub async fn get_messages(&self, entity_id: &str) -> AppResult<Vec<CRDTMessage>> {
280        let messages_map = self.entity_messages.read().await;
281        let mut messages = messages_map.get(entity_id).cloned().unwrap_or_default();
282
283        sort_messages_causally(&mut messages);
284
285        Ok(messages)
286    }
287
288    /// Check if we need to request a sync (missing messages)
289    pub async fn needs_sync(&self, entity_id: &str, remote_clock: &VectorClock) -> bool {
290        let clocks_map = self.entity_clocks.read().await;
291        let local_clock = clocks_map.get(entity_id).cloned().unwrap_or_default();
292
293        let missing = local_clock.get_missing_ranges(remote_clock);
294        !missing.is_empty()
295    }
296
297    pub async fn delete_message(&self, entity_id: &str, message_id: &str) -> AppResult<bool> {
298        let mut messages_map = self.entity_messages.write().await;
299        if let Some(messages) = messages_map.get_mut(entity_id) {
300            let original_len = messages.len();
301            messages.retain(|m| m.metadata.id != message_id);
302            let deleted = messages.len() < original_len;
303            if deleted {
304                info!("🗑️ Message deleted: {} (entity: {})", message_id, entity_id);
305            }
306            return Ok(deleted);
307        }
308        Ok(false)
309    }
310
311    pub async fn edit_message(
312        &self,
313        entity_id: &str,
314        message_id: &str,
315        new_text: String,
316    ) -> AppResult<u64> {
317        let mut messages_map = self.entity_messages.write().await;
318        if let Some(messages) = messages_map.get_mut(entity_id) {
319            for message in messages.iter_mut() {
320                if message.metadata.id == message_id {
321                    message.content.text = new_text;
322                    let edited_at = chrono::Utc::now().timestamp_millis() as u64;
323                    info!("✏️ Message edited: {} (entity: {})", message_id, entity_id);
324                    return Ok(edited_at);
325                }
326            }
327        }
328        Err(crate::error::AppError::NotFound(format!(
329            "Message not found: {}",
330            message_id
331        )))
332    }
333
334    pub async fn add_reaction(
335        &self,
336        entity_id: &str,
337        message_id: &str,
338        emoji: String,
339        peer_id: String,
340    ) -> AppResult<()> {
341        let mut messages_map = self.entity_messages.write().await;
342        if let Some(messages) = messages_map.get_mut(entity_id) {
343            for message in messages.iter_mut() {
344                if message.metadata.id == message_id {
345                    let local_state =
346                        message
347                            .local_state
348                            .get_or_insert_with(|| LocalMessageState {
349                                status: None,
350                                reactions: Vec::new(),
351                                thread_count: None,
352                                latest_reply_by: None,
353                            });
354
355                    if let Some(reaction) =
356                        local_state.reactions.iter_mut().find(|r| r.emoji == emoji)
357                    {
358                        if !reaction.peer_ids.contains(&peer_id) {
359                            reaction.peer_ids.push(peer_id.clone());
360                            reaction.count += 1;
361                        }
362                    } else {
363                        local_state.reactions.push(Reaction {
364                            emoji: emoji.clone(),
365                            count: 1,
366                            user_reacted: Some(true),
367                            peer_ids: vec![peer_id.clone()],
368                        });
369                    }
370
371                    info!(
372                        "👍 Reaction added: {} to {} (entity: {})",
373                        emoji, message_id, entity_id
374                    );
375                    return Ok(());
376                }
377            }
378        }
379        Err(crate::error::AppError::NotFound(format!(
380            "Message not found: {}",
381            message_id
382        )))
383    }
384
385    pub async fn remove_reaction(
386        &self,
387        entity_id: &str,
388        message_id: &str,
389        emoji: String,
390        peer_id: String,
391    ) -> AppResult<()> {
392        let mut messages_map = self.entity_messages.write().await;
393        if let Some(messages) = messages_map.get_mut(entity_id) {
394            for message in messages.iter_mut() {
395                if message.metadata.id == message_id {
396                    if let Some(ref mut local_state) = message.local_state
397                        && let Some(reaction) =
398                            local_state.reactions.iter_mut().find(|r| r.emoji == emoji)
399                    {
400                        reaction.peer_ids.retain(|p| p != &peer_id);
401                        reaction.count = reaction.count.saturating_sub(1);
402
403                        if reaction.count == 0 {
404                            local_state.reactions.retain(|r| r.emoji != emoji);
405                        }
406
407                        info!(
408                            "👎 Reaction removed: {} from {} (entity: {})",
409                            emoji, message_id, entity_id
410                        );
411                        return Ok(());
412                    }
413                    return Err(crate::error::AppError::NotFound(format!(
414                        "Reaction not found: {} on {}",
415                        emoji, message_id
416                    )));
417                }
418            }
419        }
420        Err(crate::error::AppError::NotFound(format!(
421            "Message not found: {}",
422            message_id
423        )))
424    }
425
426    pub async fn get_reactions(
427        &self,
428        entity_id: &str,
429        message_id: &str,
430    ) -> AppResult<Vec<Reaction>> {
431        let messages_map = self.entity_messages.read().await;
432        if let Some(messages) = messages_map.get(entity_id) {
433            for message in messages.iter() {
434                if message.metadata.id == message_id {
435                    return Ok(message
436                        .local_state
437                        .as_ref()
438                        .map(|ls| ls.reactions.clone())
439                        .unwrap_or_default());
440                }
441            }
442        }
443        Err(crate::error::AppError::NotFound(format!(
444            "Message not found: {}",
445            message_id
446        )))
447    }
448
449    // Private methods
450
451    async fn add_message(&self, message: CRDTMessage) -> AppResult<()> {
452        let entity_id = &message.metadata.entity_id;
453
454        let mut messages_map = self.entity_messages.write().await;
455        let messages = messages_map.entry(entity_id.clone()).or_default();
456
457        // Check for duplicates
458        if messages
459            .iter()
460            .any(|m| m.metadata.id == message.metadata.id)
461        {
462            warn!("⚠️  Duplicate message ignored: {}", message.metadata.id);
463            return Ok(());
464        }
465
466        messages.push(message.clone());
467        drop(messages_map);
468
469        // Update our vector clock
470        let mut clocks_map = self.entity_clocks.write().await;
471        let local_clock = clocks_map.entry(entity_id.clone()).or_default();
472        local_clock.merge(&message.metadata.vector_clock);
473        drop(clocks_map);
474
475        // Update Lamport clock
476        let mut lamport = self.lamport_clock.write().await;
477        *lamport = (*lamport).max(message.metadata.lamport_clock) + 1;
478        drop(lamport);
479
480        info!(
481            "📨 Message added: {} (entity: {})",
482            message.metadata.id, entity_id
483        );
484
485        Ok(())
486    }
487
488    async fn process_pending_messages(&self, entity_id: &str) -> AppResult<()> {
489        // Extract pending messages first
490        let mut pending_map = self.pending_messages.write().await;
491        let pending_messages = match pending_map.get_mut(entity_id) {
492            Some(p) if !p.is_empty() => std::mem::take(p),
493            _ => return Ok(()),
494        };
495        drop(pending_map);
496
497        let clocks_map = self.entity_clocks.read().await;
498        let local_clock = clocks_map.get(entity_id).cloned().unwrap_or_default();
499        drop(clocks_map);
500
501        let mut still_pending = Vec::new();
502
503        for message in pending_messages {
504            if local_clock.has_dependencies(&message.metadata.vector_clock) {
505                info!(
506                    "✅ Pending message now has dependencies: {}",
507                    message.metadata.id
508                );
509                self.add_message(message).await?;
510            } else {
511                still_pending.push(message);
512            }
513        }
514
515        // Update pending map with still-pending messages
516        let mut pending_map = self.pending_messages.write().await;
517        if !still_pending.is_empty() {
518            info!(
519                "⏳ Still pending: {} messages for {}",
520                still_pending.len(),
521                entity_id
522            );
523            pending_map.insert(entity_id.to_string(), still_pending);
524        } else {
525            pending_map.remove(entity_id);
526            info!("✨ All pending messages processed for {}", entity_id);
527        }
528
529        Ok(())
530    }
531
532    fn infer_entity_type(&self, entity_id: &str) -> EntityType {
533        // Infer from ID prefix
534        if entity_id.starts_with("contact-")
535            || entity_id.starts_with("ben-")
536            || entity_id.starts_with("lauren")
537        {
538            EntityType::Person
539        } else if entity_id.contains("-org") {
540            EntityType::Organisation
541        } else if entity_id.starts_with("project-") {
542            EntityType::Project
543        } else if entity_id.contains("general") || entity_id.contains("channel") {
544            EntityType::Channel
545        } else {
546            EntityType::Group
547        }
548    }
549}
550
551/// Result of receiving a message
552#[derive(Debug)]
553pub struct ReceiveResult {
554    pub accepted: bool,
555    pub out_of_order: bool,
556    pub missing_ranges: Option<Vec<MissingRange>>,
557}
558
559/// Result of syncing messages
560#[derive(Debug)]
561pub struct SyncResult {
562    pub messages_added: usize,
563    pub messages_rejected: usize,
564}