Login
4 branches 0 tags
Ben (Desktop/Arch) Code cleanup ada8ea6 11 days ago 251 Commits
rubhub / crates / state / src / state.rs
use std::{
    any::Any,
    fs,
    sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    },
    time::Instant,
};

use anyhow::Result;
use rubhub_auth_store::{AuthStore, RunnerToken};
use tokio::sync::{OnceCell, broadcast};

use super::{AppConfig, RepoEvent};
use super::repo_service::RepoService;
use super::runner::RunnerRegistry;

/// Type-erased CI job sender (allows main crate to set specific sender type)
type CiJobSender = Box<dyn Any + Send + Sync>;

/// Global application state
#[derive(Debug, Clone)]
pub struct GlobalState {
    pub auth: Arc<AuthStore>,
    pub config: Arc<AppConfig>,
    pub repo: Arc<RepoService>,
    pub process_start: Instant,
    pub event_tx: broadcast::Sender<RepoEvent>,
    pub local_podman_ci_available: Arc<AtomicBool>,
    ci_job_tx: Arc<OnceCell<CiJobSender>>,
    /// Registry of connected remote CI runners
    pub runner_registry: Arc<RunnerRegistry>,
}

impl GlobalState {
    /// Build a full URI from a path using the configured base URL
    pub fn uri(&self, path: &str) -> String {
        format!("{}{}", self.config.base_url, path)
    }

    /// Check if any CI backend is available and enabled
    pub fn ci_available(&self) -> bool {
        // CI is available if we have local podman OR connected remote runners OR configured runner tokens
        self.local_podman_ci_available.load(Ordering::Relaxed)
            || self.runner_registry.count() > 0
            || !self.config.runner_tokens.is_empty()
    }

    /// Create a new GlobalState instance
    pub fn new(config: AppConfig, process_start: Instant) -> Result<Self> {
        fs::create_dir_all(&config.dir_root)?;
        fs::create_dir_all(&config.git_root)?;
        fs::create_dir_all(&config.session_root)?;
        fs::create_dir_all(&config.ci_root)?;

        let auth = AuthStore::new(config.dir_root.clone());

        // Register runner tokens from config
        for token_config in &config.runner_tokens {
            let runner_token = RunnerToken::new(&token_config.token, token_config.label.clone());
            if let Err(e) = runner_token.save(&auth) {
                eprintln!("Failed to register runner token '{}': {}", token_config.label, e);
            }
        }

        let auth = Arc::new(auth);

        // Create broadcast channel for SSE events
        // 256 buffer - lagging receivers will drop old events
        let (event_tx, _) = broadcast::channel(256);

        // Create repository service
        let repo = Arc::new(RepoService::new(
            config.git_root.clone(),
            event_tx.clone(),
            Arc::clone(&auth),
        ));

        // Create runner registry
        let runner_registry = Arc::new(RunnerRegistry::new());

        Ok(Self {
            auth,
            repo,
            process_start,
            config: Arc::new(config),
            event_tx,
            local_podman_ci_available: Arc::new(AtomicBool::new(false)),
            ci_job_tx: Arc::new(OnceCell::new()),
            runner_registry,
        })
    }

    /// Emit a repository event to all SSE listeners
    pub fn emit_event(&self, event: RepoEvent) {
        // Ignore send errors (no receivers is fine)
        let _ = self.event_tx.send(event);
    }

    /// Set the CI job sender (can only be called once)
    pub fn set_ci_sender(&self, sender: Box<dyn Any + Send + Sync>) {
        let _ = self.ci_job_tx.set(sender);
    }

    /// Get the CI job sender (returns None if not set)
    pub fn ci_sender(&self) -> Option<&(dyn Any + Send + Sync)> {
        self.ci_job_tx.get().map(|b| b.as_ref())
    }
}