ass_editor/sessions/
mod.rs

1//! Session management for multi-document editing
2//!
3//! Provides the `EditorSessionManager` for managing multiple documents
4//! with shared resources, arenas, and extension registries. Supports
5//! efficient session switching (<100µs target) and resource pooling.
6
7#[cfg(not(feature = "std"))]
8extern crate alloc;
9
10use crate::core::{EditorDocument, EditorError, Result};
11
12#[cfg(feature = "arena")]
13use bumpalo::Bump;
14
15#[cfg(feature = "std")]
16use std::collections::HashMap;
17
18#[cfg(not(feature = "std"))]
19use alloc::collections::BTreeMap as HashMap;
20
21#[cfg(not(feature = "std"))]
22use alloc::{
23    string::{String, ToString},
24    vec::Vec,
25};
26
27#[cfg(feature = "multi-thread")]
28use std::sync::Arc;
29
30#[cfg(all(feature = "plugins", not(feature = "multi-thread")))]
31use std::sync::Arc;
32
33#[cfg(not(feature = "multi-thread"))]
34use core::cell::RefCell;
35
36#[cfg(feature = "multi-thread")]
37use parking_lot::Mutex;
38
39/// Configuration for session management
40#[derive(Debug, Clone)]
41pub struct SessionConfig {
42    /// Maximum number of concurrent sessions
43    pub max_sessions: usize,
44
45    /// Maximum memory usage per session in bytes
46    pub max_memory_per_session: usize,
47
48    /// Total memory limit across all sessions
49    pub total_memory_limit: usize,
50
51    /// Whether to enable automatic cleanup of unused sessions
52    pub auto_cleanup: bool,
53
54    /// Interval for arena resets (0 = never reset)
55    pub arena_reset_interval: usize,
56
57    /// Whether to share extension registry across sessions
58    pub share_extensions: bool,
59}
60
61impl Default for SessionConfig {
62    fn default() -> Self {
63        Self {
64            max_sessions: 50,
65            max_memory_per_session: 100 * 1024 * 1024, // 100MB per session
66            total_memory_limit: 1024 * 1024 * 1024,    // 1GB total
67            auto_cleanup: true,
68            arena_reset_interval: 1000, // Reset every 1000 operations
69            share_extensions: true,
70        }
71    }
72}
73
74/// Statistics about session manager
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct SessionStats {
77    /// Number of active sessions
78    pub active_sessions: usize,
79
80    /// Total memory usage across all sessions
81    pub total_memory_usage: usize,
82
83    /// Number of operations since last cleanup
84    pub operations_since_cleanup: usize,
85
86    /// Number of arena resets performed
87    pub arena_resets: usize,
88}
89
90/// A single editing session containing a document and associated state
91#[derive(Debug)]
92pub struct EditorSession {
93    /// The document being edited
94    pub document: EditorDocument,
95
96    /// Session identifier
97    pub id: String,
98
99    /// Last access timestamp for cleanup purposes
100    #[cfg(feature = "std")]
101    pub last_accessed: std::time::Instant,
102
103    /// Memory usage of this session
104    pub memory_usage: usize,
105
106    /// Number of operations performed in this session
107    pub operation_count: usize,
108
109    /// Session-specific metadata
110    pub metadata: HashMap<String, String>,
111}
112
113impl EditorSession {
114    /// Create a new session with a document
115    pub fn new(id: String, document: EditorDocument) -> Self {
116        Self {
117            id,
118            document,
119            #[cfg(feature = "std")]
120            last_accessed: std::time::Instant::now(),
121            memory_usage: 0,
122            operation_count: 0,
123            metadata: HashMap::new(),
124        }
125    }
126
127    /// Update last accessed time
128    #[cfg(feature = "std")]
129    pub fn touch(&mut self) {
130        self.last_accessed = std::time::Instant::now();
131    }
132
133    /// Check if session is stale (for cleanup)
134    #[cfg(feature = "std")]
135    pub fn is_stale(&self, max_age: std::time::Duration) -> bool {
136        self.last_accessed.elapsed() > max_age
137    }
138
139    /// Get session metadata
140    #[must_use]
141    pub fn get_metadata(&self, key: &str) -> Option<&str> {
142        self.metadata.get(key).map(|s| s.as_str())
143    }
144
145    /// Set session metadata
146    pub fn set_metadata(&mut self, key: String, value: String) {
147        self.metadata.insert(key, value);
148    }
149
150    /// Increment operation counter
151    pub fn increment_operations(&mut self) {
152        self.operation_count += 1;
153    }
154}
155
156/// Multi-document session manager with resource sharing
157///
158/// Manages multiple editing sessions with shared resources like extension
159/// registries and arena allocators. Provides efficient session switching
160/// and automatic resource management.
161struct EditorSessionManagerInner {
162    /// Configuration for this manager
163    config: SessionConfig,
164
165    /// Active editing sessions
166    sessions: HashMap<String, EditorSession>,
167
168    /// Currently active session ID
169    active_session_id: Option<String>,
170
171    /// Shared arena allocator for temporary operations
172    #[cfg(feature = "arena")]
173    shared_arena: Bump,
174
175    /// Shared extension registry
176    #[cfg(feature = "plugins")]
177    extension_registry: Option<Arc<ass_core::plugin::ExtensionRegistry>>,
178
179    /// Statistics tracking
180    stats: SessionStats,
181
182    /// Operations since last arena reset
183    #[cfg(feature = "arena")]
184    ops_since_arena_reset: usize,
185}
186
187/// Multi-document session manager with built-in thread-safety
188pub struct EditorSessionManager {
189    #[cfg(feature = "multi-thread")]
190    inner: Arc<Mutex<EditorSessionManagerInner>>,
191    #[cfg(not(feature = "multi-thread"))]
192    inner: RefCell<EditorSessionManagerInner>,
193}
194
195// EditorSessionManager is cloneable when multi-thread feature is enabled
196#[cfg(feature = "multi-thread")]
197impl Clone for EditorSessionManager {
198    fn clone(&self) -> Self {
199        Self {
200            inner: self.inner.clone(),
201        }
202    }
203}
204
205// Note: EditorSessionManager does not implement Clone without multi-thread feature
206// This is intentional - cloning requires Arc<Mutex<T>> which needs multi-thread
207
208impl std::fmt::Debug for EditorSessionManager {
209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210        #[cfg(feature = "multi-thread")]
211        {
212            let inner = self.inner.lock();
213            f.debug_struct("EditorSessionManager")
214                .field("config", &inner.config)
215                .field("active_session_id", &inner.active_session_id)
216                .field("sessions", &inner.sessions.keys().collect::<Vec<_>>())
217                .field("stats", &inner.stats)
218                .finish()
219        }
220        #[cfg(not(feature = "multi-thread"))]
221        {
222            let inner = self.inner.borrow();
223            f.debug_struct("EditorSessionManager")
224                .field("config", &inner.config)
225                .field("active_session_id", &inner.active_session_id)
226                .field("sessions", &inner.sessions.keys().collect::<Vec<_>>())
227                .field("stats", &inner.stats)
228                .finish()
229        }
230    }
231}
232
233impl EditorSessionManagerInner {
234    /// Create a new inner session manager
235    fn new(config: SessionConfig) -> Self {
236        Self {
237            config,
238            sessions: HashMap::new(),
239            active_session_id: None,
240            #[cfg(feature = "arena")]
241            shared_arena: Bump::new(),
242            #[cfg(feature = "plugins")]
243            extension_registry: None,
244            stats: SessionStats {
245                active_sessions: 0,
246                total_memory_usage: 0,
247                operations_since_cleanup: 0,
248                arena_resets: 0,
249            },
250            #[cfg(feature = "arena")]
251            ops_since_arena_reset: 0,
252        }
253    }
254}
255
256impl EditorSessionManager {
257    /// Helper method for accessing inner data mutably
258    #[cfg(feature = "multi-thread")]
259    fn with_inner_mut<F, R>(&self, f: F) -> R
260    where
261        F: FnOnce(&mut EditorSessionManagerInner) -> R,
262    {
263        let mut inner = self.inner.lock();
264        f(&mut inner)
265    }
266
267    /// Helper method for accessing inner data immutably
268    #[cfg(feature = "multi-thread")]
269    fn with_inner<F, R>(&self, f: F) -> R
270    where
271        F: FnOnce(&EditorSessionManagerInner) -> R,
272    {
273        let inner = self.inner.lock();
274        f(&inner)
275    }
276
277    /// Helper method for accessing inner data mutably
278    #[cfg(not(feature = "multi-thread"))]
279    fn with_inner_mut<F, R>(&self, f: F) -> R
280    where
281        F: FnOnce(&mut EditorSessionManagerInner) -> R,
282    {
283        let mut inner = self.inner.borrow_mut();
284        f(&mut inner)
285    }
286
287    /// Helper method for accessing inner data immutably
288    #[cfg(not(feature = "multi-thread"))]
289    fn with_inner<F, R>(&self, f: F) -> R
290    where
291        F: FnOnce(&EditorSessionManagerInner) -> R,
292    {
293        let inner = self.inner.borrow();
294        f(&inner)
295    }
296
297    /// Create a new session manager
298    pub fn new() -> Self {
299        Self::with_config(SessionConfig::default())
300    }
301
302    /// Create a new session manager with custom configuration
303    pub fn with_config(config: SessionConfig) -> Self {
304        #[cfg(feature = "multi-thread")]
305        {
306            Self {
307                inner: Arc::new(Mutex::new(EditorSessionManagerInner::new(config))),
308            }
309        }
310        #[cfg(not(feature = "multi-thread"))]
311        {
312            Self {
313                inner: RefCell::new(EditorSessionManagerInner::new(config)),
314            }
315        }
316    }
317
318    /// Create a new session with an empty document
319    pub fn create_session(&mut self, session_id: String) -> Result<()> {
320        self.create_session_with_document(session_id, EditorDocument::new())
321    }
322
323    /// Create a new session with a specific document
324    pub fn create_session_with_document(
325        &mut self,
326        session_id: String,
327        document: EditorDocument,
328    ) -> Result<()> {
329        self.with_inner_mut(|inner| {
330            // Check session limits
331            if inner.sessions.len() >= inner.config.max_sessions {
332                return Err(EditorError::SessionLimitExceeded {
333                    current: inner.sessions.len(),
334                    limit: inner.config.max_sessions,
335                });
336            }
337
338            // Create new session
339            let session = EditorSession::new(session_id.clone(), document);
340
341            // Add to sessions map
342            inner.sessions.insert(session_id.clone(), session);
343
344            // Update stats
345            inner.stats.active_sessions += 1;
346
347            // Set as active if it's the first session
348            if inner.active_session_id.is_none() {
349                inner.active_session_id = Some(session_id);
350            }
351
352            Ok(())
353        })
354    }
355
356    /// Switch to a different session
357    pub fn switch_session(&mut self, session_id: &str) -> Result<()> {
358        self.with_inner_mut(|inner| {
359            // Check if session exists
360            if !inner.sessions.contains_key(session_id) {
361                return Err(EditorError::DocumentNotFound {
362                    id: session_id.to_string(),
363                });
364            }
365
366            // Switch active session
367            inner.active_session_id = Some(session_id.to_string());
368
369            // Touch the session to update access time
370            #[cfg(feature = "std")]
371            if let Some(session) = inner.sessions.get_mut(session_id) {
372                session.touch();
373            }
374
375            Ok(())
376        })
377    }
378
379    /// Get the currently active session
380    pub fn active_session(&self) -> Result<Option<String>> {
381        Ok(self.with_inner(|inner| inner.active_session_id.clone()))
382    }
383
384    /// Execute a function with a read-only reference to a session's document
385    pub fn with_document<F, R>(&self, session_id: &str, f: F) -> Result<R>
386    where
387        F: FnOnce(&EditorDocument) -> Result<R>,
388    {
389        self.with_inner(|inner| {
390            inner
391                .sessions
392                .get(session_id)
393                .ok_or_else(|| EditorError::DocumentNotFound {
394                    id: session_id.to_string(),
395                })
396                .and_then(|session| f(&session.document))
397        })
398    }
399
400    /// Execute a closure with mutable access to a session's document
401    pub fn with_document_mut<F, R>(&mut self, session_id: &str, f: F) -> Result<R>
402    where
403        F: FnOnce(&mut EditorDocument) -> Result<R>,
404    {
405        self.with_inner_mut(|inner| {
406            let session = inner.sessions.get_mut(session_id).ok_or_else(|| {
407                EditorError::DocumentNotFound {
408                    id: session_id.to_string(),
409                }
410            })?;
411
412            let result = f(&mut session.document)?;
413            session.increment_operations();
414
415            Ok(result)
416        })
417    }
418
419    /// Remove a session
420    pub fn remove_session(&mut self, session_id: &str) -> Result<EditorSession> {
421        self.with_inner_mut(|inner| {
422            let session =
423                inner
424                    .sessions
425                    .remove(session_id)
426                    .ok_or_else(|| EditorError::DocumentNotFound {
427                        id: session_id.to_string(),
428                    })?;
429
430            // Update stats
431            inner.stats.active_sessions -= 1;
432            inner.stats.total_memory_usage -= session.memory_usage;
433
434            // Clear active session if it was removed
435            if inner.active_session_id.as_ref() == Some(&session_id.to_string()) {
436                inner.active_session_id = None;
437            }
438
439            Ok(session)
440        })
441    }
442
443    /// List all session IDs
444    pub fn list_sessions(&self) -> Result<Vec<String>> {
445        Ok(self.with_inner(|inner| inner.sessions.keys().cloned().collect()))
446    }
447
448    /// Get session statistics
449    pub fn stats(&self) -> SessionStats {
450        self.with_inner(|inner| inner.stats.clone())
451    }
452
453    /// Perform cleanup of stale sessions
454    #[cfg(feature = "std")]
455    pub fn cleanup_stale_sessions(&mut self, max_age: std::time::Duration) -> Result<usize> {
456        // Get list of stale sessions
457        let sessions_to_remove = self.with_inner(|inner| {
458            if !inner.config.auto_cleanup {
459                return vec![];
460            }
461
462            inner
463                .sessions
464                .iter()
465                .filter(|(_, session)| session.is_stale(max_age))
466                .map(|(id, _)| id.clone())
467                .collect::<Vec<_>>()
468        });
469
470        // Remove stale sessions
471        let mut removed_count = 0;
472        for session_id in sessions_to_remove {
473            if self.remove_session(&session_id).is_ok() {
474                removed_count += 1;
475            }
476        }
477
478        Ok(removed_count)
479    }
480
481    /// Reset shared arena to reclaim memory
482    #[cfg(feature = "arena")]
483    pub fn reset_shared_arena(&mut self) {
484        self.with_inner_mut(|inner| {
485            inner.shared_arena.reset();
486            inner.stats.arena_resets += 1;
487            inner.ops_since_arena_reset = 0;
488        });
489    }
490
491    /// Set shared extension registry
492    #[cfg(feature = "plugins")]
493    pub fn set_extension_registry(&mut self, registry: Arc<ass_core::plugin::ExtensionRegistry>) {
494        self.with_inner_mut(|inner| {
495            inner.extension_registry = Some(registry);
496        });
497    }
498
499    /// Get shared extension registry
500    #[cfg(feature = "plugins")]
501    #[must_use]
502    pub fn extension_registry(&self) -> Option<Arc<ass_core::plugin::ExtensionRegistry>> {
503        self.with_inner(|inner| inner.extension_registry.clone())
504    }
505}
506
507impl Default for EditorSessionManager {
508    fn default() -> Self {
509        Self::new()
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    #[cfg(not(feature = "std"))]
517    use alloc::{string::ToString, vec};
518
519    #[test]
520    fn session_manager_creation() {
521        let manager = EditorSessionManager::new();
522        assert_eq!(manager.stats().active_sessions, 0);
523        assert!(manager.active_session().unwrap().is_none());
524    }
525
526    #[test]
527    fn session_creation_and_switching() {
528        let mut manager = EditorSessionManager::new();
529
530        // Create first session
531        manager.create_session("session1".to_string()).unwrap();
532        assert_eq!(manager.stats().active_sessions, 1);
533        assert_eq!(
534            manager.active_session().unwrap(),
535            Some("session1".to_string())
536        );
537
538        // Create second session
539        manager.create_session("session2".to_string()).unwrap();
540        assert_eq!(manager.stats().active_sessions, 2);
541
542        // Switch to second session
543        manager.switch_session("session2").unwrap();
544        assert_eq!(
545            manager.active_session().unwrap(),
546            Some("session2".to_string())
547        );
548
549        // List sessions
550        let sessions = manager.list_sessions().unwrap();
551        assert_eq!(sessions.len(), 2);
552        assert!(sessions.contains(&"session1".to_string()));
553        assert!(sessions.contains(&"session2".to_string()));
554    }
555
556    #[test]
557    fn session_document_access() {
558        let mut manager = EditorSessionManager::new();
559        let doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
560
561        manager
562            .create_session_with_document("test".to_string(), doc)
563            .unwrap();
564
565        // Test document access
566        manager
567            .with_document("test", |doc| {
568                assert!(doc.text().contains("Title: Test"));
569                Ok(())
570            })
571            .unwrap();
572
573        // Test document mutation
574        manager
575            .with_document_mut("test", |doc| {
576                doc.insert(crate::core::Position::new(0), "Hello ")?;
577                Ok(())
578            })
579            .unwrap();
580
581        manager
582            .with_document("test", |doc| {
583                assert!(doc.text().starts_with("Hello "));
584                Ok(())
585            })
586            .unwrap();
587    }
588
589    #[test]
590    fn session_removal() {
591        let mut manager = EditorSessionManager::new();
592
593        manager.create_session("test".to_string()).unwrap();
594        assert_eq!(manager.stats().active_sessions, 1);
595
596        let removed_session = manager.remove_session("test").unwrap();
597        assert_eq!(removed_session.id, "test");
598        assert_eq!(manager.stats().active_sessions, 0);
599        assert!(manager.active_session().unwrap().is_none());
600    }
601
602    #[test]
603    fn session_limits() {
604        let config = SessionConfig {
605            max_sessions: 2,
606            ..Default::default()
607        };
608        let mut manager = EditorSessionManager::with_config(config);
609
610        // Create maximum allowed sessions
611        manager.create_session("session1".to_string()).unwrap();
612        manager.create_session("session2".to_string()).unwrap();
613
614        // Try to create one more - should fail
615        let result = manager.create_session("session3".to_string());
616        assert!(matches!(
617            result,
618            Err(EditorError::SessionLimitExceeded { .. })
619        ));
620    }
621
622    #[test]
623    fn session_metadata() {
624        let mut session = EditorSession::new("test".to_string(), EditorDocument::new());
625
626        assert_eq!(session.get_metadata("key"), None);
627
628        session.set_metadata("key".to_string(), "value".to_string());
629        assert_eq!(session.get_metadata("key"), Some("value"));
630    }
631}