Skip to main content

mdcs_sdk/
client.rs

1//! High-level client for the MDCS SDK.
2
3use crate::error::SdkError;
4use crate::network::{MemoryTransport, NetworkTransport, Peer, PeerId};
5use crate::session::Session;
6use parking_lot::RwLock;
7use std::collections::HashMap;
8use std::sync::Arc;
9
10/// Configuration for the MDCS client.
11#[derive(Clone, Debug)]
12pub struct ClientConfig {
13    /// User name for presence.
14    pub user_name: String,
15    /// Enable automatic reconnection.
16    pub auto_reconnect: bool,
17    /// Maximum reconnection attempts.
18    pub max_reconnect_attempts: u32,
19}
20
21impl Default for ClientConfig {
22    fn default() -> Self {
23        Self {
24            user_name: "Anonymous".to_string(),
25            auto_reconnect: true,
26            max_reconnect_attempts: 5,
27        }
28    }
29}
30
31/// Builder for client configuration.
32pub struct ClientConfigBuilder {
33    config: ClientConfig,
34}
35
36impl ClientConfigBuilder {
37    pub fn new() -> Self {
38        Self {
39            config: ClientConfig::default(),
40        }
41    }
42
43    pub fn user_name(mut self, name: impl Into<String>) -> Self {
44        self.config.user_name = name.into();
45        self
46    }
47
48    pub fn auto_reconnect(mut self, enabled: bool) -> Self {
49        self.config.auto_reconnect = enabled;
50        self
51    }
52
53    pub fn max_reconnect_attempts(mut self, attempts: u32) -> Self {
54        self.config.max_reconnect_attempts = attempts;
55        self
56    }
57
58    pub fn build(self) -> ClientConfig {
59        self.config
60    }
61}
62
63impl Default for ClientConfigBuilder {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69/// The main MDCS client for collaborative editing.
70///
71/// The client manages sessions, documents, and network connections.
72///
73/// # Example
74///
75/// ```rust
76/// use mdcs_sdk::{Client, ClientConfig};
77///
78/// // Create a client
79/// let config = ClientConfig {
80///     user_name: "Alice".to_string(),
81///     ..Default::default()
82/// };
83/// let client = Client::new_with_memory_transport(config);
84///
85/// // Create a session
86/// let session = client.create_session("my-session");
87///
88/// // Open a document
89/// let doc = session.open_text_doc("shared-doc");
90/// doc.write().insert(0, "Hello, world!");
91/// ```
92pub struct Client<T: NetworkTransport> {
93    peer_id: PeerId,
94    config: ClientConfig,
95    transport: Arc<T>,
96    sessions: Arc<RwLock<HashMap<String, Arc<Session<T>>>>>,
97}
98
99impl Client<MemoryTransport> {
100    /// Create a new client with an in-memory transport (for testing).
101    pub fn new_with_memory_transport(config: ClientConfig) -> Self {
102        let peer_id = PeerId::new(format!("peer-{}", uuid_simple()));
103        let transport = Arc::new(MemoryTransport::new(peer_id.clone()));
104
105        Self {
106            peer_id,
107            config,
108            transport,
109            sessions: Arc::new(RwLock::new(HashMap::new())),
110        }
111    }
112}
113
114impl<T: NetworkTransport> Client<T> {
115    /// Create a new client with a custom transport.
116    pub fn new(peer_id: PeerId, transport: Arc<T>, config: ClientConfig) -> Self {
117        Self {
118            peer_id,
119            config,
120            transport,
121            sessions: Arc::new(RwLock::new(HashMap::new())),
122        }
123    }
124
125    /// Get the local peer ID.
126    pub fn peer_id(&self) -> &PeerId {
127        &self.peer_id
128    }
129
130    /// Get the user name.
131    pub fn user_name(&self) -> &str {
132        &self.config.user_name
133    }
134
135    /// Get the transport.
136    pub fn transport(&self) -> &Arc<T> {
137        &self.transport
138    }
139
140    /// Create a new collaborative session.
141    pub fn create_session(&self, session_id: impl Into<String>) -> Arc<Session<T>> {
142        let session_id = session_id.into();
143        let mut sessions = self.sessions.write();
144
145        if let Some(session) = sessions.get(&session_id) {
146            session.clone()
147        } else {
148            let session = Arc::new(Session::new(
149                session_id.clone(),
150                self.peer_id.clone(),
151                self.config.user_name.clone(),
152                self.transport.clone(),
153            ));
154            sessions.insert(session_id, session.clone());
155            session
156        }
157    }
158
159    /// Get an existing session.
160    pub fn get_session(&self, session_id: &str) -> Option<Arc<Session<T>>> {
161        self.sessions.read().get(session_id).cloned()
162    }
163
164    /// Close a session.
165    pub fn close_session(&self, session_id: &str) {
166        self.sessions.write().remove(session_id);
167    }
168
169    /// List all active session IDs.
170    pub fn session_ids(&self) -> Vec<String> {
171        self.sessions.read().keys().cloned().collect()
172    }
173
174    /// Connect to a peer.
175    pub async fn connect_peer(&self, peer_id: &PeerId) -> Result<(), SdkError> {
176        self.transport
177            .connect(peer_id)
178            .await
179            .map_err(|e| SdkError::ConnectionFailed(e.to_string()))
180    }
181
182    /// Disconnect from a peer.
183    pub async fn disconnect_peer(&self, peer_id: &PeerId) -> Result<(), SdkError> {
184        self.transport
185            .disconnect(peer_id)
186            .await
187            .map_err(|e| SdkError::NetworkError(e.to_string()))
188    }
189
190    /// Get list of connected peers.
191    pub async fn connected_peers(&self) -> Vec<Peer> {
192        self.transport.connected_peers().await
193    }
194}
195
196/// Simple UUID-like string generator.
197fn uuid_simple() -> String {
198    use std::time::{SystemTime, UNIX_EPOCH};
199    let timestamp = SystemTime::now()
200        .duration_since(UNIX_EPOCH)
201        .unwrap()
202        .as_nanos();
203    format!("{:x}", timestamp)
204}
205
206/// Convenience functions for quickly creating collaborative sessions.
207pub mod quick {
208    use super::*;
209    use crate::network::create_network;
210
211    /// Create a simple collaborative setup with multiple clients.
212    ///
213    /// Returns a vector of clients with their connected memory transports.
214    pub fn create_collaborative_clients(user_names: &[&str]) -> Vec<Client<MemoryTransport>> {
215        let network = create_network(user_names.len());
216
217        user_names
218            .iter()
219            .zip(network)
220            .map(|(name, transport)| {
221                let peer_id = transport.local_id().clone();
222                let config = ClientConfig {
223                    user_name: name.to_string(),
224                    ..Default::default()
225                };
226                Client::new(peer_id, Arc::new(transport), config)
227            })
228            .collect()
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_client_creation() {
238        let config = ClientConfig {
239            user_name: "Alice".to_string(),
240            ..Default::default()
241        };
242        let client = Client::new_with_memory_transport(config);
243
244        assert_eq!(client.user_name(), "Alice");
245    }
246
247    #[test]
248    fn test_session_management() {
249        let config = ClientConfig::default();
250        let client = Client::new_with_memory_transport(config);
251
252        let session1 = client.create_session("session-1");
253        let _session2 = client.create_session("session-2");
254
255        assert_eq!(client.session_ids().len(), 2);
256
257        // Getting same session returns same instance
258        let session1_again = client.create_session("session-1");
259        assert!(Arc::ptr_eq(&session1, &session1_again));
260
261        // Close session
262        client.close_session("session-1");
263        assert_eq!(client.session_ids().len(), 1);
264    }
265
266    #[test]
267    fn test_config_builder() {
268        let config = ClientConfigBuilder::new()
269            .user_name("Bob")
270            .auto_reconnect(false)
271            .max_reconnect_attempts(3)
272            .build();
273
274        assert_eq!(config.user_name, "Bob");
275        assert!(!config.auto_reconnect);
276        assert_eq!(config.max_reconnect_attempts, 3);
277    }
278
279    #[test]
280    fn test_quick_collaborative_clients() {
281        let clients = quick::create_collaborative_clients(&["Alice", "Bob", "Charlie"]);
282
283        assert_eq!(clients.len(), 3);
284        assert_eq!(clients[0].user_name(), "Alice");
285        assert_eq!(clients[1].user_name(), "Bob");
286        assert_eq!(clients[2].user_name(), "Charlie");
287    }
288}