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(
115            &engine_dir,
116            &callbacks.additional_watch_paths,
117            watch_tx,
118            shutdown_rx.clone(),
119        )?;
120
121        // Create build guard and pipeline
122        let guard = Arc::new(BuildGuard::new());
123        let pipeline = Arc::new(RebuildPipeline::new(
124            guard,
125            engine_dir,
126            Arc::clone(&host),
127            Arc::clone(&ws_server),
128            shutdown_rx.clone(),
129            callbacks,
130            #[cfg(feature = "audio")]
131            None, // Audio reload will be handled separately if needed
132        ));
133
134        // Spawn rebuild pipeline task with panic recovery
135        let shutdown_rx_for_task = shutdown_rx.clone();
136        let pipeline_handle = tokio::spawn(async move {
137            let mut shutdown_rx = shutdown_rx_for_task.clone();
138            loop {
139                if *shutdown_rx.borrow() {
140                    break;
141                }
142
143                tokio::select! {
144                    _ = shutdown_rx.changed() => {
145                        break;
146                    }
147                    maybe_event = watch_rx.recv() => {
148                        let event = match maybe_event {
149                            Some(event) => event,
150                            None => break,
151                        };
152
153                        match event {
154                            WatchEvent::RustFilesChanged(paths) => {
155                                handle_rust_files_changed(paths, &pipeline).await;
156                            }
157                        }
158                    }
159                }
160            }
161        });
162
163        Ok(Self {
164            watcher,
165            _pipeline_handle: pipeline_handle,
166            ws_server,
167            _shutdown_rx: shutdown_rx,
168            #[cfg(feature = "audio")]
169            _audio_handle: audio_handle,
170        })
171    }
172}