1use fob_bundler::builders::asset_registry::AssetRegistry;
7use parking_lot::RwLock;
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::time::Instant;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum BuildStatus {
16 NotStarted,
18 InProgress { started_at: Instant },
20 Success { duration_ms: u64 },
22 Failed { error: String },
24}
25
26impl BuildStatus {
27 pub fn is_in_progress(&self) -> bool {
29 matches!(self, BuildStatus::InProgress { .. })
30 }
31
32 pub fn is_success(&self) -> bool {
34 matches!(self, BuildStatus::Success { .. })
35 }
36
37 pub fn is_not_started(&self) -> bool {
39 matches!(self, BuildStatus::NotStarted)
40 }
41
42 pub fn error(&self) -> Option<&str> {
44 match self {
45 BuildStatus::Failed { error } => Some(error),
46 _ => None,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Default)]
56pub struct BundleCache {
57 files: HashMap<String, (Vec<u8>, String)>,
59}
60
61impl BundleCache {
62 pub fn new() -> Self {
64 Self {
65 files: HashMap::new(),
66 }
67 }
68
69 pub fn insert(&mut self, path: String, content: Vec<u8>, content_type: String) {
77 self.files.insert(path, (content, content_type));
78 }
79
80 pub fn get(&self, path: &str) -> Option<&(Vec<u8>, String)> {
86 self.files.get(path)
87 }
88
89 pub fn clear(&mut self) {
91 self.files.clear();
92 }
93
94 pub fn len(&self) -> usize {
96 self.files.len()
97 }
98
99 pub fn is_empty(&self) -> bool {
101 self.files.is_empty()
102 }
103
104 pub fn find_entry_point(&self) -> Option<String> {
113 for path in self.files.keys() {
114 if path.ends_with(".js") || path.ends_with(".mjs") {
115 if !path.contains(".map") && !path.starts_with("/__") {
117 return Some(path.clone());
118 }
119 }
120 }
121 None
122 }
123}
124
125pub type ClientRegistry = Arc<RwLock<HashMap<usize, tokio::sync::mpsc::Sender<String>>>>;
129
130pub struct DevServerState {
135 pub status: RwLock<BuildStatus>,
137
138 pub cache: RwLock<BundleCache>,
140
141 pub clients: ClientRegistry,
143
144 pub next_client_id: RwLock<usize>,
146
147 pub asset_registry: RwLock<Arc<AssetRegistry>>,
149
150 pub out_dir: PathBuf,
152}
153
154impl DevServerState {
155 pub fn new(out_dir: PathBuf) -> Self {
161 Self {
162 status: RwLock::new(BuildStatus::NotStarted),
163 cache: RwLock::new(BundleCache::new()),
164 clients: Arc::new(RwLock::new(HashMap::new())),
165 next_client_id: RwLock::new(0),
166 asset_registry: RwLock::new(Arc::new(AssetRegistry::new())),
167 out_dir,
168 }
169 }
170
171 #[cfg(test)]
173 pub fn new_with_registry(registry: Arc<AssetRegistry>) -> Self {
174 Self {
175 status: RwLock::new(BuildStatus::NotStarted),
176 cache: RwLock::new(BundleCache::new()),
177 clients: Arc::new(RwLock::new(HashMap::new())),
178 next_client_id: RwLock::new(0),
179 asset_registry: RwLock::new(registry),
180 out_dir: PathBuf::from("dist"),
181 }
182 }
183
184 pub fn start_build(&self) {
186 *self.status.write() = BuildStatus::InProgress {
187 started_at: Instant::now(),
188 };
189 }
190
191 pub fn complete_build(&self, duration_ms: u64) {
197 *self.status.write() = BuildStatus::Success { duration_ms };
198 }
199
200 pub fn fail_build(&self, error: String) {
206 *self.status.write() = BuildStatus::Failed { error };
207 }
208
209 pub fn get_status(&self) -> BuildStatus {
211 self.status.read().clone()
212 }
213
214 pub fn update_cache(&self, new_cache: BundleCache) {
220 *self.cache.write() = new_cache;
221 }
222
223 pub fn get_cached_file(&self, path: &str) -> Option<(Vec<u8>, String)> {
225 self.cache.read().get(path).cloned()
226 }
227
228 pub fn clear_cache(&self) {
230 self.cache.write().clear();
231 }
232
233 pub fn register_client(&self) -> (usize, tokio::sync::mpsc::Receiver<String>) {
239 let id = {
240 let mut next_id = self.next_client_id.write();
241 let id = *next_id;
242 *next_id += 1;
243 id
244 };
245
246 let (tx, rx) = tokio::sync::mpsc::channel(100);
247 self.clients.write().insert(id, tx);
248
249 (id, rx)
250 }
251
252 pub fn unregister_client(&self, id: usize) {
254 self.clients.write().remove(&id);
255 }
256
257 pub async fn broadcast(&self, event: &crate::dev::DevEvent) {
263 let json = serde_json::to_string(event).unwrap_or_else(|_| "{}".to_string());
264 let clients = self.clients.read().clone();
268
269 let mut failed_ids = Vec::new();
271
272 for (id, tx) in clients {
274 if tx.send(json.clone()).await.is_err() {
275 failed_ids.push(id);
277 }
278 }
279
280 for id in failed_ids {
282 self.unregister_client(id);
283 }
284 }
285
286 pub fn client_count(&self) -> usize {
288 self.clients.read().len()
289 }
290
291 pub fn asset_registry(&self) -> Arc<AssetRegistry> {
293 Arc::clone(&*self.asset_registry.read())
294 }
295
296 pub fn update_asset_registry(&self, registry: Arc<AssetRegistry>) {
298 *self.asset_registry.write() = registry;
299 }
300
301 pub fn get_out_dir(&self) -> &PathBuf {
303 &self.out_dir
304 }
305}
306
307impl Default for DevServerState {
308 fn default() -> Self {
309 Self::new(PathBuf::from("dist"))
310 }
311}
312
313pub type SharedState = Arc<DevServerState>;
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_build_status_is_in_progress() {
322 let status = BuildStatus::InProgress {
323 started_at: Instant::now(),
324 };
325 assert!(status.is_in_progress());
326 assert!(!status.is_success());
327 assert!(status.error().is_none());
328 }
329
330 #[test]
331 fn test_build_status_success() {
332 let status = BuildStatus::Success { duration_ms: 100 };
333 assert!(!status.is_in_progress());
334 assert!(status.is_success());
335 assert!(status.error().is_none());
336 }
337
338 #[test]
339 fn test_build_status_failed() {
340 let status = BuildStatus::Failed {
341 error: "Test error".to_string(),
342 };
343 assert!(!status.is_in_progress());
344 assert!(!status.is_success());
345 assert_eq!(status.error(), Some("Test error"));
346 }
347
348 #[test]
349 fn test_bundle_cache_operations() {
350 let mut cache = BundleCache::new();
351 assert!(cache.is_empty());
352
353 cache.insert(
354 "/index.js".to_string(),
355 b"console.log('test')".to_vec(),
356 "application/javascript".to_string(),
357 );
358
359 assert_eq!(cache.len(), 1);
360 assert!(!cache.is_empty());
361
362 let file = cache.get("/index.js");
363 assert!(file.is_some());
364
365 let (content, content_type) = file.unwrap();
366 assert_eq!(content, b"console.log('test')");
367 assert_eq!(content_type, "application/javascript");
368
369 cache.clear();
370 assert!(cache.is_empty());
371 }
372
373 #[test]
374 fn test_dev_server_state_build_lifecycle() {
375 let state = DevServerState::new(PathBuf::from("dist"));
376
377 assert!(matches!(state.get_status(), BuildStatus::NotStarted));
378
379 state.start_build();
380 assert!(state.get_status().is_in_progress());
381
382 state.complete_build(150);
383 assert!(state.get_status().is_success());
384
385 state.fail_build("Test error".to_string());
386 assert_eq!(state.get_status().error(), Some("Test error"));
387 }
388
389 #[test]
390 fn test_dev_server_state_cache() {
391 let state = DevServerState::new(PathBuf::from("dist"));
392
393 let mut cache = BundleCache::new();
394 cache.insert(
395 "/test.js".to_string(),
396 b"test".to_vec(),
397 "application/javascript".to_string(),
398 );
399
400 state.update_cache(cache);
401
402 let file = state.get_cached_file("/test.js");
403 assert!(file.is_some());
404
405 state.clear_cache();
406 assert!(state.get_cached_file("/test.js").is_none());
407 }
408
409 #[tokio::test]
410 async fn test_client_registration() {
411 let state = Arc::new(DevServerState::new(PathBuf::from("dist")));
412
413 let (id1, _rx1) = state.register_client();
414 let (id2, _rx2) = state.register_client();
415
416 assert_eq!(state.client_count(), 2);
417 assert_ne!(id1, id2);
418
419 state.unregister_client(id1);
420 assert_eq!(state.client_count(), 1);
421 }
422}