Skip to main content

wavecraft_dev_server/
session.rs

1//! Development session lifecycle management
2//!
3//! The DevSession struct owns all components of the development server
4//! and ensures proper initialization and cleanup ordering.
5
6use anyhow::Result;
7use std::path::PathBuf;
8use std::sync::Arc;
9use tokio::sync::{mpsc, watch};
10
11use crate::host::DevServerHost;
12use crate::reload::guard::BuildGuard;
13use crate::reload::rebuild::{RebuildCallbacks, RebuildPipeline};
14use crate::reload::watcher::{FileWatcher, WatchEvent};
15use crate::ws::WsServer;
16
17#[cfg(feature = "audio")]
18use crate::audio::server::AudioHandle;
19
20fn log_rust_file_change(paths: &[PathBuf]) {
21    let timestamp = chrono::Local::now().format("%H:%M:%S");
22    if paths.len() == 1 {
23        println!(
24            "[{}] File changed: {}",
25            timestamp,
26            paths[0].file_name().unwrap_or_default().to_string_lossy()
27        );
28    } else {
29        println!("[{}] {} files changed", timestamp, paths.len());
30    }
31}
32
33fn report_rebuild_result(result: Result<Result<()>, tokio::task::JoinError>) {
34    match result {
35        Ok(Ok(())) => {
36            // Success - continue watching
37        }
38        Ok(Err(e)) => {
39            eprintln!("  {} Rebuild failed: {:#}", console::style("✗").red(), e);
40        }
41        Err(join_err) => {
42            eprintln!(
43                "  {} Hot-reload pipeline panicked: {}\n  {} Continuing to watch for changes...",
44                console::style("✗").red(),
45                join_err,
46                console::style("→").cyan()
47            );
48        }
49    }
50}
51
52async fn handle_rust_files_changed(paths: Vec<PathBuf>, pipeline: &Arc<RebuildPipeline>) {
53    log_rust_file_change(&paths);
54
55    // Trigger rebuild with panic recovery
56    let pipeline = Arc::clone(pipeline);
57    let result = tokio::spawn(async move { pipeline.handle_change().await }).await;
58    report_rebuild_result(result);
59}
60
61/// Development session managing WebSocket server, file watcher, and rebuild pipeline.
62///
63/// Components are owned in the correct order for proper drop semantics:
64/// 1. Watcher (drops first, stops sending events)
65/// 2. Pipeline (processes pending events, then stops)
66/// 3. Server (drops last, closes connections)
67pub struct DevSession {
68    /// File watcher for hot-reload triggers
69    #[allow(dead_code)] // Kept alive for the lifetime of the session
70    watcher: FileWatcher,
71    /// Rebuild pipeline handle (for graceful shutdown)
72    #[allow(dead_code)] // Kept alive for the lifetime of the session
73    _pipeline_handle: tokio::task::JoinHandle<()>,
74    /// WebSocket server (kept alive for the lifetime of the session)
75    #[allow(dead_code)] // Kept alive for the lifetime of the session
76    ws_server: Arc<WsServer<Arc<DevServerHost>>>,
77    /// Shutdown signal receiver (kept alive for the lifetime of the session)
78    #[allow(dead_code)]
79    _shutdown_rx: watch::Receiver<bool>,
80    /// Audio processing handle (if audio is enabled)
81    #[cfg(feature = "audio")]
82    #[allow(dead_code)] // Kept alive for the lifetime of the session
83    _audio_handle: Option<AudioHandle>,
84}
85
86impl DevSession {
87    /// Create a new development session.
88    ///
89    /// # Arguments
90    ///
91    /// * `engine_dir` - Path to the engine directory
92    /// * `host` - Parameter host (shared with IPC handler and pipeline)
93    /// * `ws_server` - WebSocket server (shared with pipeline)
94    /// * `shutdown_rx` - Shutdown signal receiver
95    /// * `callbacks` - CLI-specific callbacks for rebuild pipeline
96    /// * `audio_handle` - Optional audio processing handle (audio feature only)
97    ///
98    /// # Returns
99    ///
100    /// A `DevSession` that manages the lifecycle of all components.
101    #[allow(clippy::too_many_arguments)]
102    pub fn new(
103        engine_dir: PathBuf,
104        host: Arc<DevServerHost>,
105        ws_server: Arc<WsServer<Arc<DevServerHost>>>,
106        shutdown_rx: watch::Receiver<bool>,
107        callbacks: RebuildCallbacks,
108        #[cfg(feature = "audio")] audio_handle: Option<AudioHandle>,
109    ) -> Result<Self> {
110        // Create channel for watch events
111        let (watch_tx, mut watch_rx) = mpsc::unbounded_channel::<WatchEvent>();
112
113        // Create file watcher
114        let watcher = FileWatcher::new(&engine_dir, watch_tx, shutdown_rx.clone())?;
115
116        // Create build guard and pipeline
117        let guard = Arc::new(BuildGuard::new());
118        let pipeline = Arc::new(RebuildPipeline::new(
119            guard,
120            engine_dir,
121            Arc::clone(&host),
122            Arc::clone(&ws_server),
123            shutdown_rx.clone(),
124            callbacks,
125            #[cfg(feature = "audio")]
126            None, // Audio reload will be handled separately if needed
127        ));
128
129        // Spawn rebuild pipeline task with panic recovery
130        let shutdown_rx_for_task = shutdown_rx.clone();
131        let pipeline_handle = tokio::spawn(async move {
132            let mut shutdown_rx = shutdown_rx_for_task.clone();
133            loop {
134                if *shutdown_rx.borrow() {
135                    break;
136                }
137
138                tokio::select! {
139                    _ = shutdown_rx.changed() => {
140                        break;
141                    }
142                    maybe_event = watch_rx.recv() => {
143                        let event = match maybe_event {
144                            Some(event) => event,
145                            None => break,
146                        };
147
148                        match event {
149                            WatchEvent::RustFilesChanged(paths) => {
150                                handle_rust_files_changed(paths, &pipeline).await;
151                            }
152                        }
153                    }
154                }
155            }
156        });
157
158        Ok(Self {
159            watcher,
160            _pipeline_handle: pipeline_handle,
161            ws_server,
162            _shutdown_rx: shutdown_rx,
163            #[cfg(feature = "audio")]
164            _audio_handle: audio_handle,
165        })
166    }
167}