vertigo_cli/serve/
server_state.rs

1use parking_lot::RwLock;
2use std::{
3    collections::HashMap,
4    sync::{Arc, OnceLock},
5    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
6};
7use tokio::sync::mpsc::{error::TryRecvError, unbounded_channel};
8use vertigo::{
9    JsJson, JsJsonSerialize,
10    dev::command::{CommandForBrowser, ConsoleLogLevel, browser_response},
11};
12use wasmtime::{Engine, Module};
13
14use crate::{
15    commons::{ErrorCode, spawn::SpawnOwner},
16    serve::html::FetchCache,
17};
18
19use super::{
20    html::HtmlResponse,
21    mount_path::MountConfig,
22    request_state::RequestState,
23    response_state::ResponseState,
24    wasm::{Message, WasmInstance},
25};
26
27pub fn get_now() -> Duration {
28    let start = SystemTime::now();
29    match start.duration_since(UNIX_EPOCH) {
30        Ok(duration) => duration,
31        Err(err) => {
32            log::error!("Time went backwards: {err}");
33            Duration::from_secs(0)
34        }
35    }
36}
37
38pub type ServerStateMap = HashMap<String, Arc<ServerState>>;
39
40static STATE: OnceLock<Arc<RwLock<ServerStateMap>>> = OnceLock::new();
41
42#[derive(Clone)]
43pub struct ServerState {
44    engine: Engine,
45    module: Module,
46    pub mount_config: MountConfig,
47    pub port_watch: Option<u16>,
48}
49
50impl ServerState {
51    pub fn init(mount_config: &MountConfig) -> Result<(), ErrorCode> {
52        Self::init_with_watch(mount_config, None)
53    }
54
55    pub fn init_with_watch(
56        mount_config: &MountConfig,
57        port_watch: Option<u16>,
58    ) -> Result<(), ErrorCode> {
59        let engine = Engine::default();
60
61        let module = build_module_wasm(&engine, mount_config)?;
62
63        let mutex = STATE.get_or_init(|| Arc::new(RwLock::new(ServerStateMap::new())));
64
65        let mut guard = mutex.write();
66        guard.insert(
67            mount_config.mount_point().to_string(),
68            Arc::new(Self {
69                engine,
70                module,
71                mount_config: mount_config.clone(),
72                port_watch,
73            }),
74        );
75
76        Ok(())
77    }
78
79    pub fn global(mount_point: &str) -> Arc<ServerState> {
80        let mutex = STATE.get_or_init(|| Arc::new(RwLock::new(ServerStateMap::new())));
81
82        let guard = mutex.read();
83
84        if let Some(state) = guard.get(mount_point) {
85            return state.clone();
86        }
87
88        unreachable!();
89    }
90
91    pub async fn request(&self, url: &str) -> ResponseState {
92        let (sender, mut receiver) = unbounded_channel::<Message>();
93
94        let request = RequestState {
95            url: url.to_string(),
96            env: self.mount_config.env.clone(),
97        };
98
99        let fetch = FetchCache::new();
100
101        let mut inst = WasmInstance::new(
102            sender.clone(),
103            &self.engine,
104            &self.module,
105            request,
106            Arc::new({
107                let sender = sender.clone();
108
109                move |request: RequestState, command| match command {
110                    CommandForBrowser::FetchCacheGet => {
111                        browser_response::FetchCacheGet { data: None }.to_json()
112                    }
113                    CommandForBrowser::FetchExec { request, callback } => {
114                        sender
115                            .send(Message::FetchRequest { callback, request })
116                            .inspect_err(|err| log::error!("Error sending FetchRequest: {err}"))
117                            .unwrap_or_default();
118
119                        JsJson::Null
120                    }
121                    CommandForBrowser::SetStatus { status } => {
122                        sender
123                            .send(Message::SetStatus(status))
124                            .inspect_err(|err| log::error!("Error sending FetchRequest: {err}"))
125                            .unwrap_or_default();
126
127                        JsJson::Null
128                    }
129                    CommandForBrowser::IsBrowser => {
130                        let response = browser_response::IsBrowser { value: false };
131
132                        response.to_json()
133                    }
134                    CommandForBrowser::GetDateNow => {
135                        let time = get_now().as_millis();
136
137                        let response = browser_response::GetDateNow { value: time as u64 };
138
139                        response.to_json()
140                    }
141                    CommandForBrowser::WebsocketRegister {
142                        host: _,
143                        callback: _,
144                    } => JsJson::Null,
145                    CommandForBrowser::WebsocketUnregister { callback: _ } => JsJson::Null,
146                    CommandForBrowser::WebsocketSendMessage {
147                        callback: _,
148                        message: _,
149                    } => JsJson::Null,
150                    CommandForBrowser::TimerSet {
151                        callback,
152                        duration,
153                        kind: _,
154                    } => {
155                        if duration == 0 {
156                            sender
157                                .send(Message::SetTimeoutZero { callback })
158                                .inspect_err(|err| {
159                                    log::error!("Error sending SetTimeoutZero: {err}")
160                                })
161                                .unwrap_or_default();
162                        }
163
164                        JsJson::Null
165                    }
166                    CommandForBrowser::TimerClear { callback: _ } => JsJson::Null,
167                    CommandForBrowser::LocationCallback {
168                        target: _,
169                        mode: _,
170                        callback: _,
171                    } => JsJson::Null,
172                    CommandForBrowser::LocationSet {
173                        target: _,
174                        mode: _,
175                        value: _,
176                    } => JsJson::Null,
177                    CommandForBrowser::LocationGet { target: _ } => {
178                        let url = request.url.clone();
179                        browser_response::LocationGet { value: url }.to_json()
180                    }
181                    CommandForBrowser::CookieGet { name: _ } => {
182                        browser_response::CookieGet { value: "".into() }.to_json()
183                    }
184                    CommandForBrowser::CookieSet {
185                        name: _,
186                        value: _,
187                        expires_in: _,
188                    } => JsJson::Null,
189                    CommandForBrowser::CookieJsonGet { name: _ } => {
190                        browser_response::CookieJsonGet {
191                            value: JsJson::Null,
192                        }
193                        .to_json()
194                    }
195                    CommandForBrowser::CookieJsonSet {
196                        name: _,
197                        value: _,
198                        expires_in: _,
199                    } => JsJson::Null,
200                    CommandForBrowser::GetEnv { name } => {
201                        let env_value = request.env(name);
202
203                        browser_response::GetEnv { value: env_value }.to_json()
204                    }
205                    CommandForBrowser::Log {
206                        kind,
207                        message,
208                        arg2: _,
209                        arg3: _,
210                        arg4: _,
211                    } => {
212                        if kind == ConsoleLogLevel::Error {
213                            log::warn!("{message}");
214                        } else {
215                            log::info!("{message}");
216                        }
217
218                        JsJson::Null
219                    }
220                    CommandForBrowser::TimezoneOffset => {
221                        browser_response::TimezoneOffset { value: 0 }.to_json()
222                    }
223                    CommandForBrowser::HistoryBack => JsJson::Null,
224                    CommandForBrowser::GetRandom { min, max: _ } => {
225                        browser_response::GetRandom { value: min }.to_json()
226                    }
227                    CommandForBrowser::JsApiCall { commands: _ } => JsJson::Null,
228                    CommandForBrowser::DomBulkUpdate { list } => {
229                        sender
230                            .send(Message::DomUpdate(list))
231                            .inspect_err(|err| log::error!("Error sending DomUpdate: {err}"))
232                            .unwrap_or_default();
233
234                        JsJson::Null
235                    }
236                }
237            }),
238        );
239
240        // -- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
241        //TODO - ultimately, do not call call_vertigo_entry_function if something is returned by handle_url
242        // -- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
243
244        inst.call_vertigo_entry_function();
245
246        if let Some(result) = inst.handle_url(url) {
247            return result;
248        }
249
250        let spawn_resource = SpawnOwner::new({
251            let sender = sender.clone();
252
253            async move {
254                tokio::time::sleep(std::time::Duration::from_secs(10)).await;
255                let _ = sender.send(Message::TimeoutAndSendResponse);
256            }
257        });
258
259        let mut html_response = HtmlResponse::new(
260            sender.clone(),
261            &self.mount_config,
262            inst,
263            self.mount_config.env.clone(),
264            fetch,
265        );
266
267        loop {
268            let message = receiver.try_recv();
269
270            match message {
271                Ok(message) => {
272                    if let Some(response) = html_response.process_message(message) {
273                        return response;
274                    };
275                    continue;
276                }
277                Err(TryRecvError::Empty) => {} // continue this iteration
278                Err(TryRecvError::Disconnected) => {
279                    break; // send response to browser
280                }
281            }
282
283            if html_response.awaiting_response() {
284                let message = receiver.recv().await;
285                if let Some(message) = message
286                    && let Some(response) = html_response.process_message(message)
287                {
288                    return response;
289                };
290            } else {
291                break; // send response to browser
292            }
293        }
294
295        spawn_resource.off();
296        html_response.build_response()
297    }
298}
299
300fn build_module_wasm(engine: &Engine, mount_path: &MountConfig) -> Result<Module, ErrorCode> {
301    let full_wasm_path = mount_path.get_wasm_fs_path();
302
303    log::info!("Mounting {} -> {full_wasm_path}", mount_path.mount_point());
304
305    let wasm_content = match std::fs::read(&full_wasm_path) {
306        Ok(wasm_content) => wasm_content,
307        Err(error) => {
308            log::error!("Problem reading the path: wasm_path={full_wasm_path}, error={error}");
309            return Err(ErrorCode::ServeWasmReadFailed);
310        }
311    };
312
313    let now = Instant::now();
314
315    let module = match Module::from_binary(engine, &wasm_content) {
316        Ok(module) => module,
317        Err(err) => {
318            log::error!("Wasm compilation error: error={err}");
319            return Err(ErrorCode::ServeWasmCompileFailed);
320        }
321    };
322
323    log::info!("WASM module compiled in {} ms.", now.elapsed().as_millis());
324    Ok(module)
325}