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