Skip to main content

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