bmux_sessions_plugin 0.0.1-alpha.1

Shipped sessions plugin for bmux
//! Session manager, owned by the sessions plugin.
//!
//! `SessionManager` tracks every session known to the host: its
//! identity, name, and client set. The sessions plugin owns and
//! constructs this type. The `Arc<RwLock<SessionManager>>` is
//! registered into [`bmux_plugin::PluginStateRegistry`] during the
//! plugin's `activate` callback; server and other plugins reach
//! session-manager state through the domain-agnostic
//! `bmux_session_state::SessionManagerHandle` trait object rather
//! than naming this concrete type.
//!
//! The heavier `SessionRuntimeManager` (pane PTY processes, snapshot
//! plumbing, event fan-out) remains in `packages/server` — it is too
//! entangled with server-specific runtime primitives (portable-pty,
//! tokio channels, recording runtimes) to relocate without a
//! dependency explosion.

use anyhow::Result;
use bmux_session_models::{Session, SessionId, SessionInfo};
use std::collections::BTreeMap;

/// Authoritative session roster. Owns every live session's static
/// identity and client-set metadata.
#[derive(Debug, Default)]
pub struct SessionManager {
    sessions: BTreeMap<SessionId, Session>,
}

impl SessionManager {
    /// Construct an empty session manager.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            sessions: BTreeMap::new(),
        }
    }

    /// Create a new session with an optional name. Returns the
    /// generated id.
    ///
    /// # Errors
    ///
    /// Today this method is infallible but keeps the `Result` return
    /// type for symmetry with [`Self::insert_session`] and for future
    /// capacity errors.
    pub fn create_session(&mut self, name: Option<String>) -> Result<SessionId> {
        let session = Session::new(name);
        let id = session.id;
        self.sessions.insert(id, session);
        Ok(id)
    }

    /// Insert a preconstructed session (used by restore paths).
    ///
    /// # Errors
    ///
    /// Returns an error when a session with the same id already exists.
    pub fn insert_session(&mut self, session: Session) -> Result<()> {
        let id = session.id;
        if self.sessions.contains_key(&id) {
            return Err(anyhow::anyhow!("Session already exists: {id}"));
        }
        self.sessions.insert(id, session);
        Ok(())
    }

    /// Reference to a session by id.
    #[must_use]
    pub fn get_session(&self, session_id: &SessionId) -> Option<&Session> {
        self.sessions.get(session_id)
    }

    /// Rename a session by id.
    ///
    /// # Errors
    ///
    /// Returns an error when no session has the given id.
    pub fn rename_session(&mut self, session_id: SessionId, name: String) -> Result<()> {
        let Some(session) = self.sessions.get_mut(&session_id) else {
            return Err(anyhow::anyhow!("Session not found: {session_id}"));
        };
        session.name = Some(name);
        Ok(())
    }

    /// Mutable reference to a session by id.
    pub fn get_session_mut(&mut self, session_id: &SessionId) -> Option<&mut Session> {
        self.sessions.get_mut(session_id)
    }

    /// List every session as a `SessionInfo` record.
    #[must_use]
    pub fn list_sessions(&self) -> Vec<SessionInfo> {
        self.sessions.values().map(Into::into).collect()
    }

    /// Remove a session by id.
    ///
    /// # Errors
    ///
    /// Returns an error if no session has the given id.
    pub fn remove_session(&mut self, session_id: &SessionId) -> Result<()> {
        if self.sessions.remove(session_id).is_some() {
            Ok(())
        } else {
            Err(anyhow::anyhow!("Session not found: {session_id}"))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::SessionManager;

    #[test]
    fn rename_session_updates_display_name() {
        let mut manager = SessionManager::new();
        let session_id = manager
            .create_session(Some("old".to_string()))
            .expect("create should succeed");

        manager
            .rename_session(session_id, "new".to_string())
            .expect("rename should succeed");

        let session = manager
            .get_session(&session_id)
            .expect("renamed session should exist");
        assert_eq!(session.name.as_deref(), Some("new"));
    }
}