Skip to main content

nfw_core/hmr/
watcher.rs

1use notify::{Event, EventKind, RecursiveMode, Watcher};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use tokio::sync::RwLock;
6
7pub struct FileWatcher {
8    watcher: notify::RecommendedWatcher,
9    watched_paths: Vec<PathBuf>,
10    #[allow(dead_code)]
11    debounce_ms: u64,
12}
13
14impl FileWatcher {
15    pub fn new(debounce_ms: u64) -> anyhow::Result<Self> {
16        let watcher =
17            notify::recommended_watcher(move |res: Result<Event, notify::Error>| match res {
18                Ok(event) => {
19                    tracing::debug!("File event: {:?}", event);
20                }
21                Err(e) => {
22                    tracing::error!("Watch error: {:?}", e);
23                }
24            })?;
25
26        Ok(Self {
27            watcher,
28            watched_paths: Vec::new(),
29            debounce_ms,
30        })
31    }
32
33    pub fn watch(&mut self, path: impl AsRef<Path>) -> anyhow::Result<()> {
34        let path = path.as_ref().to_path_buf();
35        if !path.exists() {
36            std::fs::create_dir_all(&path)?;
37        }
38        self.watcher.watch(&path, RecursiveMode::Recursive)?;
39        self.watched_paths.push(path);
40        Ok(())
41    }
42
43    pub fn unwatch(&mut self, path: impl AsRef<Path>) -> anyhow::Result<()> {
44        let path = path.as_ref();
45        self.watcher.unwatch(path)?;
46        self.watched_paths.retain(|p| p != path);
47        Ok(())
48    }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ChangedFile {
53    pub path: PathBuf,
54    pub kind: FileChangeKind,
55}
56
57#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
58pub enum FileChangeKind {
59    Created,
60    Modified,
61    Deleted,
62    Renamed,
63}
64
65impl From<EventKind> for FileChangeKind {
66    fn from(kind: EventKind) -> Self {
67        match kind {
68            EventKind::Create(_) => FileChangeKind::Created,
69            EventKind::Modify(_) => FileChangeKind::Modified,
70            EventKind::Remove(_) => FileChangeKind::Deleted,
71            _ => FileChangeKind::Modified,
72        }
73    }
74}
75
76pub struct HmrServer {
77    pub port: u16,
78    connections: Arc<RwLock<Vec<tokio::sync::mpsc::Sender<HmrMessage>>>>,
79}
80
81impl HmrServer {
82    pub fn new(port: u16) -> anyhow::Result<Self> {
83        Ok(Self {
84            port,
85            connections: Arc::new(RwLock::new(Vec::new())),
86        })
87    }
88
89    pub async fn notify_change(&self, files: Vec<ChangedFile>) {
90        let message = HmrMessage::Reload { files };
91        let connections = self.connections.write().await;
92
93        for tx in connections.iter() {
94            let _ = tx.try_send(message.clone());
95        }
96    }
97
98    pub async fn broadcast_message(&self, message: HmrMessage) {
99        let connections = self.connections.write().await;
100
101        for tx in connections.iter() {
102            let _ = tx.try_send(message.clone());
103        }
104    }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(tag = "type")]
109pub enum HmrMessage {
110    Reload { files: Vec<ChangedFile> },
111    FullReload { reason: String },
112    ModuleUpdate { module: String, code: String },
113    Error { message: String },
114    Connected { client_id: String },
115}
116
117pub struct HmrClient {
118    #[allow(dead_code)]
119    ws_url: String,
120}
121
122impl HmrClient {
123    pub fn new(ws_url: &str) -> Self {
124        Self {
125            ws_url: ws_url.to_string(),
126        }
127    }
128
129    pub fn generate_script() -> String {
130        r#"
131(function() {
132    var ws = null;
133    var reconnectAttempts = 0;
134    var maxReconnectAttempts = 10;
135    
136    function connect() {
137        var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
138        ws = new WebSocket(protocol + '//' + window.location.host + '/__hmr');
139        
140        ws.onopen = function() {
141            console.log('[HMR] Connected to dev server');
142            reconnectAttempts = 0;
143        };
144        
145        ws.onmessage = function(event) {
146            var data = JSON.parse(event.data);
147            
148            switch (data.type) {
149                case 'reload':
150                    console.log('[HMR] Reloading modules:', data.files);
151                    if (window.__nestforgeWebHot) {
152                        window.__nestforgeWebHot.invalidate();
153                    } else {
154                        window.location.reload();
155                    }
156                    break;
157                case 'fullReload':
158                    console.log('[HMR] Full reload:', data.reason);
159                    window.location.reload();
160                    break;
161                case 'moduleUpdate':
162                    console.log('[HMR] Module updated:', data.module);
163                    break;
164                case 'error':
165                    console.error('[HMR] Error:', data.message);
166                    break;
167            }
168        };
169        
170        ws.onclose = function() {
171            console.log('[HMR] Disconnected');
172            if (reconnectAttempts < maxReconnectAttempts) {
173                reconnectAttempts++;
174                setTimeout(connect, 1000 * reconnectAttempts);
175            }
176        };
177    }
178    
179    connect();
180    window.__hmrClient = ws;
181})();
182"#
183        .to_string()
184    }
185}