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 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) => {} Err(TryRecvError::Disconnected) => {
289 break; }
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; }
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}