orbiton/
hmr.rs

1// Hot Module Replacement (HMR) support for the Orbit UI framework
2
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, Mutex};
6use std::time::{Duration, Instant};
7
8/// HMR update data
9#[derive(Debug, Clone)]
10pub struct HmrUpdate {
11    /// The module path
12    pub module: String,
13    /// When the update was detected
14    pub timestamp: Instant,
15    /// Whether the module has been updated
16    pub is_updated: bool,
17}
18
19/// HMR context manager
20#[derive(Debug, Clone)]
21pub struct HmrContext {
22    /// Modified modules
23    modules: Arc<Mutex<HashMap<String, HmrUpdate>>>,
24    /// Last full rebuild time
25    last_rebuild: Arc<Mutex<Option<Instant>>>,
26    /// Project root directory
27    project_root: PathBuf,
28}
29
30impl Default for HmrContext {
31    fn default() -> Self {
32        Self::new(PathBuf::from("."))
33    }
34}
35
36impl HmrContext {
37    /// Create a new HMR context
38    pub fn new(project_root: PathBuf) -> Self {
39        Self {
40            modules: Arc::new(Mutex::new(HashMap::new())),
41            last_rebuild: Arc::new(Mutex::new(None)),
42            project_root,
43        }
44    }
45
46    /// Record a file change
47    pub fn record_file_change(&self, path: &Path) -> Option<String> {
48        let rel_path = path.strip_prefix(&self.project_root).ok()?;
49        let path_str = rel_path.to_string_lossy().replace('\\', "/");
50
51        // Extract module path for Rust and Orbit files
52        if let Some(ext) = path.extension() {
53            let ext_str = ext.to_string_lossy();
54
55            let module = if ext_str == "rs" || ext_str == "orbit" {
56                if path_str.starts_with("src/") {
57                    Some(
58                        path_str
59                            .replace("src/", "")
60                            .replace(".rs", "")
61                            .replace(".orbit", ""),
62                    )
63                } else {
64                    // Not in src directory, might be lib or other code
65                    None
66                }
67            } else {
68                // Not a Rust or Orbit file
69                None
70            };
71
72            if let Some(module_path) = module {
73                let mut modules = self.modules.lock().unwrap();
74                modules.insert(
75                    module_path.clone(),
76                    HmrUpdate {
77                        module: module_path.clone(),
78                        timestamp: Instant::now(),
79                        is_updated: false,
80                    },
81                );
82                return Some(module_path);
83            }
84        }
85
86        None
87    }
88
89    /// Mark all modules as updated
90    pub fn mark_modules_updated(&self) {
91        let mut modules = self.modules.lock().unwrap();
92        for update in modules.values_mut() {
93            update.is_updated = true;
94        }
95    }
96
97    /// Check if any modules need updating
98    pub fn needs_update(&self) -> bool {
99        let modules = self.modules.lock().unwrap();
100        modules.values().any(|update| !update.is_updated)
101    }
102
103    /// Get pending module updates
104    pub fn get_pending_updates(&self) -> Vec<String> {
105        let modules = self.modules.lock().unwrap();
106        modules
107            .values()
108            .filter(|update| !update.is_updated)
109            .map(|update| update.module.clone())
110            .collect()
111    }
112
113    /// Record a full rebuild
114    pub fn record_rebuild(&self) {
115        let mut last_rebuild = self.last_rebuild.lock().unwrap();
116        *last_rebuild = Some(Instant::now());
117
118        // Mark all modules as updated when a full rebuild happens
119        self.mark_modules_updated();
120    }
121    /// Check if a rebuild is needed
122    pub fn should_rebuild(&self, debounce_time: Duration) -> bool {
123        // Check if enough time has passed since last rebuild
124        let last_rebuild = self.last_rebuild.lock().unwrap();
125        if let Some(instant) = *last_rebuild {
126            if instant.elapsed() < debounce_time {
127                return false;
128            }
129        }
130        drop(last_rebuild); // Explicitly drop the lock before checking needs_update
131
132        // Check if we have pending updates
133        self.needs_update()
134    }
135
136    /// Clear all pending updates
137    pub fn clear(&self) {
138        let mut modules = self.modules.lock().unwrap();
139        modules.clear();
140    }
141
142    /// Get the age of the oldest pending update
143    pub fn get_oldest_update_age(&self) -> Option<Duration> {
144        let modules = self.modules.lock().unwrap();
145        modules
146            .values()
147            .filter(|update| !update.is_updated)
148            .map(|update| update.timestamp.elapsed())
149            .max()
150    }
151
152    /// Get all updates that are older than the specified duration
153    pub fn get_stale_updates(&self, max_age: Duration) -> Vec<String> {
154        let modules = self.modules.lock().unwrap();
155        modules
156            .values()
157            .filter(|update| !update.is_updated && update.timestamp.elapsed() > max_age)
158            .map(|update| update.module.clone())
159            .collect()
160    }
161
162    /// Force clear stale updates (useful for cleanup)
163    pub fn clear_stale_updates(&self, max_age: Duration) {
164        let mut modules = self.modules.lock().unwrap();
165        modules.retain(|_, update| update.is_updated || update.timestamp.elapsed() <= max_age);
166    }
167}