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    /// Create a new builder initialized with [`ClientConfig::default`].
38    pub fn new() -> Self {
39        Self {
40            config: ClientConfig::default(),
41        }
42    }
43
44    /// Set the display name advertised to other collaborators.
45    pub fn user_name(mut self, name: impl Into<String>) -> Self {
46        self.config.user_name = name.into();
47        self
48    }
49
50    /// Enable or disable automatic reconnection attempts.
51    pub fn auto_reconnect(mut self, enabled: bool) -> Self {
52        self.config.auto_reconnect = enabled;
53        self
54    }
55
56    /// Set the maximum number of reconnect attempts.
57    pub fn max_reconnect_attempts(mut self, attempts: u32) -> Self {
58        self.config.max_reconnect_attempts = attempts;
59        self
60    }
61
62    /// Build and return the final [`ClientConfig`].
63    pub fn build(self) -> ClientConfig {
64        self.config
65    }
66}
67
68impl Default for ClientConfigBuilder {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74/// The main MDCS client for collaborative editing.
75///
76/// The client manages sessions, documents, and network connections.
77///
78/// # Example
79///
80/// ```rust
81/// use mdcs_sdk::{Client, ClientConfig};
82///
83/// // Create a client
84/// let config = ClientConfig {
85///     user_name: "Alice".to_string(),
86///     ..Default::default()
87/// };
88/// let client = Client::new_with_memory_transport(config);
89///
90/// // Create a session
91/// let session = client.create_session("my-session");
92///
93/// // Open a document
94/// let doc = session.open_text_doc("shared-doc");
95/// doc.write().insert(0, "Hello, world!");
96/// ```
97pub struct Client<T: NetworkTransport> {
98    peer_id: PeerId,
99    config: ClientConfig,
100    transport: Arc<T>,
101    sessions: Arc<RwLock<HashMap<String, Arc<Session<T>>>>>,
102}
103
104impl Client<MemoryTransport> {
105    /// Create a new client backed by [`MemoryTransport`].
106    ///
107    /// This constructor is ideal for tests, local demos, and examples where
108    /// all peers run in the same process.
109    pub fn new_with_memory_transport(config: ClientConfig) -> Self {
110        let peer_id = PeerId::new(format!("peer-{}", uuid_simple()));
111        let transport = Arc::new(MemoryTransport::new(peer_id.clone()));
112
113        Self {
114            peer_id,
115            config,
116            transport,
117            sessions: Arc::new(RwLock::new(HashMap::new())),
118        }
119    }
120}
121
122impl<T: NetworkTransport> Client<T> {
123    /// Create a new client with a custom transport implementation.
124    ///
125    /// Use this constructor when integrating with a real networking backend
126    /// (WebSocket, QUIC, custom RPC, etc.).
127    pub fn new(peer_id: PeerId, transport: Arc<T>, config: ClientConfig) -> Self {
128        Self {
129            peer_id,
130            config,
131            transport,
132            sessions: Arc::new(RwLock::new(HashMap::new())),
133        }
134    }
135
136    /// Return the unique identifier of this local peer.
137    pub fn peer_id(&self) -> &PeerId {
138        &self.peer_id
139    }
140
141    /// Return the configured local user name.
142    pub fn user_name(&self) -> &str {
143        &self.config.user_name
144    }
145
146    /// Return the transport used by this client.
147    pub fn transport(&self) -> &Arc<T> {
148        &self.transport
149    }
150
151    /// Create or fetch a collaborative session by ID.
152    ///
153    /// If the session already exists, this returns the same shared instance.
154    pub fn create_session(&self, session_id: impl Into<String>) -> Arc<Session<T>> {
155        let session_id = session_id.into();
156        let mut sessions = self.sessions.write();
157
158        if let Some(session) = sessions.get(&session_id) {
159            session.clone()
160        } else {
161            let session = Arc::new(Session::new(
162                session_id.clone(),
163                self.peer_id.clone(),
164                self.config.user_name.clone(),
165                self.transport.clone(),
166            ));
167            sessions.insert(session_id, session.clone());
168            session
169        }
170    }
171
172    /// Get an existing session if it has already been created on this client.
173    pub fn get_session(&self, session_id: &str) -> Option<Arc<Session<T>>> {
174        self.sessions.read().get(session_id).cloned()
175    }
176
177    /// Close a local session handle and remove it from the client cache.
178    ///
179    /// This does not notify remote peers directly; use higher-level app
180    /// signaling if your protocol requires explicit leave semantics.
181    pub fn close_session(&self, session_id: &str) {
182        self.sessions.write().remove(session_id);
183    }
184
185    /// List all currently active local session IDs.
186    pub fn session_ids(&self) -> Vec<String> {
187        self.sessions.read().keys().cloned().collect()
188    }
189
190    /// Establish a transport-level connection to a peer.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`SdkError::ConnectionFailed`] if the underlying transport
195    /// cannot connect to the target peer.
196    pub async fn connect_peer(&self, peer_id: &PeerId) -> Result<(), SdkError> {
197        self.transport
198            .connect(peer_id)
199            .await
200            .map_err(|e| SdkError::ConnectionFailed(e.to_string()))
201    }
202
203    /// Disconnect from a peer.
204    ///
205    /// # Errors
206    ///
207    /// Returns [`SdkError::NetworkError`] if the transport reports a failure
208    /// while disconnecting.
209    pub async fn disconnect_peer(&self, peer_id: &PeerId) -> Result<(), SdkError> {
210        self.transport
211            .disconnect(peer_id)
212            .await
213            .map_err(|e| SdkError::NetworkError(e.to_string()))
214    }
215
216    /// Return the current list of connected peers reported by the transport.
217    pub async fn connected_peers(&self) -> Vec<Peer> {
218        self.transport.connected_peers().await
219    }
220}
221
222/// Simple UUID-like string generator.
223fn uuid_simple() -> String {
224    use std::time::{SystemTime, UNIX_EPOCH};
225    let timestamp = SystemTime::now()
226        .duration_since(UNIX_EPOCH)
227        .unwrap()
228        .as_nanos();
229    format!("{:x}", timestamp)
230}
231
232/// Convenience functions for quickly creating collaborative sessions.
233pub mod quick {
234    use super::*;
235    use crate::network::create_network;
236
237    /// Create a fully connected in-memory set of collaborative clients.
238    ///
239    /// The returned clients can immediately open the same session/document IDs
240    /// and exchange updates in examples or tests.
241    ///
242    /// # Panics
243    ///
244    /// Panics if `user_names` and generated transports differ in length, which
245    /// should not happen for a correctly constructed in-memory network.
246    pub fn create_collaborative_clients(user_names: &[&str]) -> Vec<Client<MemoryTransport>> {
247        let network = create_network(user_names.len());
248
249        user_names
250            .iter()
251            .zip(network)
252            .map(|(name, transport)| {
253                let peer_id = transport.local_id().clone();
254                let config = ClientConfig {
255                    user_name: name.to_string(),
256                    ..Default::default()
257                };
258                Client::new(peer_id, Arc::new(transport), config)
259            })
260            .collect()
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_client_creation() {
270        let config = ClientConfig {
271            user_name: "Alice".to_string(),
272            ..Default::default()
273        };
274        let client = Client::new_with_memory_transport(config);
275
276        assert_eq!(client.user_name(), "Alice");
277    }
278
279    #[test]
280    fn test_session_management() {
281        let config = ClientConfig::default();
282        let client = Client::new_with_memory_transport(config);
283
284        let session1 = client.create_session("session-1");
285        let _session2 = client.create_session("session-2");
286
287        assert_eq!(client.session_ids().len(), 2);
288
289        // Getting same session returns same instance
290        let session1_again = client.create_session("session-1");
291        assert!(Arc::ptr_eq(&session1, &session1_again));
292
293        // Close session
294        client.close_session("session-1");
295        assert_eq!(client.session_ids().len(), 1);
296    }
297
298    #[test]
299    fn test_config_builder() {
300        let config = ClientConfigBuilder::new()
301            .user_name("Bob")
302            .auto_reconnect(false)
303            .max_reconnect_attempts(3)
304            .build();
305
306        assert_eq!(config.user_name, "Bob");
307        assert!(!config.auto_reconnect);
308        assert_eq!(config.max_reconnect_attempts, 3);
309    }
310
311    #[test]
312    fn test_quick_collaborative_clients() {
313        let clients = quick::create_collaborative_clients(&["Alice", "Bob", "Charlie"]);
314
315        assert_eq!(clients.len(), 3);
316        assert_eq!(clients[0].user_name(), "Alice");
317        assert_eq!(clients[1].user_name(), "Bob");
318        assert_eq!(clients[2].user_name(), "Charlie");
319    }
320}