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}