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, InstancePre, 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 timings::SsrProbe,
28 wasm::{HostState, Message, WasmInstance, build_linker},
29};
30
31#[cfg(feature = "ssr-timings")]
32use super::timings::SsrTimings;
33
34pub fn get_now() -> Duration {
35 let start = SystemTime::now();
36 match start.duration_since(UNIX_EPOCH) {
37 Ok(duration) => duration,
38 Err(err) => {
39 log::error!("Time went backwards: {err}");
40 Duration::from_secs(0)
41 }
42 }
43}
44
45pub type ServerStateMap = HashMap<String, Arc<ServerState>>;
46
47static STATE: OnceLock<Arc<RwLock<ServerStateMap>>> = OnceLock::new();
48
49#[derive(Clone)]
50pub struct ServerState {
51 engine: Engine,
52 instance_pre: InstancePre<HostState>,
55 module_compile: Duration,
57 pub mount_config: MountConfig,
58 pub port_watch: Option<u16>,
59}
60
61impl ServerState {
62 pub fn init(mount_config: &MountConfig) -> Result<(), ErrorCode> {
63 Self::init_with_watch(mount_config, None)
64 }
65
66 pub fn init_with_watch(
67 mount_config: &MountConfig,
68 port_watch: Option<u16>,
69 ) -> Result<(), ErrorCode> {
70 let engine = Engine::default();
71
72 let (module, module_compile) = build_module_wasm(&engine, mount_config)?;
73
74 let instance_pre = build_linker(&engine)?
77 .instantiate_pre(&module)
78 .map_err(|err| {
79 log::error!("WASM import resolution failed: {err:?}");
80 ErrorCode::ServeWasmInstanceFailed
81 })?;
82
83 let mutex = STATE.get_or_init(|| Arc::new(RwLock::new(ServerStateMap::new())));
84
85 let mut guard = mutex.write();
86 guard.insert(
87 mount_config.mount_point().to_string(),
88 Arc::new(Self {
89 engine,
90 instance_pre,
91 module_compile,
92 mount_config: mount_config.clone(),
93 port_watch,
94 }),
95 );
96
97 Ok(())
98 }
99
100 pub fn module_compile_time(&self) -> Duration {
102 self.module_compile
103 }
104
105 pub fn global(mount_point: &str) -> Arc<ServerState> {
106 let mutex = STATE.get_or_init(|| Arc::new(RwLock::new(ServerStateMap::new())));
107
108 let guard = mutex.read();
109
110 if let Some(state) = guard.get(mount_point) {
111 return state.clone();
112 }
113
114 unreachable!();
115 }
116
117 pub async fn request(&self, url: &str) -> ResponseState {
118 self.request_inner(url, &SsrProbe::new()).await
119 }
120
121 #[cfg(feature = "ssr-timings")]
123 pub async fn request_timed(&self, url: &str) -> (ResponseState, SsrTimings) {
124 let probe = SsrProbe::new();
125 let mark = probe.start();
126
127 let response = self.request_inner(url, &probe).await;
128
129 let timings = probe.finish(mark, response.body.len());
130 (response, timings)
131 }
132
133 async fn request_inner(&self, url: &str, probe: &SsrProbe) -> ResponseState {
134 let (sender, mut receiver) = unbounded_channel::<Message>();
135
136 let request = RequestState {
137 url: url.to_string(),
138 env: self.mount_config.env.clone(),
139 };
140
141 let fetch = FetchCache::new();
142
143 let instantiate_mark = probe.start();
144 let handle_command = Arc::new({
145 let sender = sender.clone();
146 let probe = probe.clone();
147
148 move |request: RequestState, command| match command {
149 CommandForBrowser::FetchCacheGet => {
150 browser_response::FetchCacheGet { data: None }.to_json()
151 }
152 CommandForBrowser::FetchExec { request, callback } => {
153 sender
154 .send(Message::FetchRequest { callback, request })
155 .inspect_err(|err| log::error!("Error sending FetchRequest: {err}"))
156 .unwrap_or_default();
157
158 JsJson::Null
159 }
160 CommandForBrowser::SetStatus { status } => {
161 sender
162 .send(Message::SetStatus(status))
163 .inspect_err(|err| log::error!("Error sending FetchRequest: {err}"))
164 .unwrap_or_default();
165
166 JsJson::Null
167 }
168 CommandForBrowser::IsBrowser => {
169 let response = browser_response::IsBrowser { value: false };
170
171 response.to_json()
172 }
173 CommandForBrowser::GetDateNow => {
174 let time = get_now().as_millis();
175
176 let response = browser_response::GetDateNow { value: time as u64 };
177
178 response.to_json()
179 }
180 CommandForBrowser::WebsocketRegister {
181 host: _,
182 callback: _,
183 } => JsJson::Null,
184 CommandForBrowser::WebsocketUnregister { callback: _ } => JsJson::Null,
185 CommandForBrowser::WebsocketSendMessage {
186 callback: _,
187 message: _,
188 } => JsJson::Null,
189 CommandForBrowser::TimerSet {
190 callback,
191 duration,
192 kind: _,
193 } => {
194 if duration == 0 {
195 sender
196 .send(Message::SetTimeoutZero { callback })
197 .inspect_err(|err| log::error!("Error sending SetTimeoutZero: {err}"))
198 .unwrap_or_default();
199 }
200
201 JsJson::Null
202 }
203 CommandForBrowser::TimerClear { callback: _ } => JsJson::Null,
204 CommandForBrowser::LocationCallback {
205 target: _,
206 mode: _,
207 callback: _,
208 } => JsJson::Null,
209 CommandForBrowser::LocationSet {
210 target: _,
211 mode: _,
212 value: _,
213 } => JsJson::Null,
214 CommandForBrowser::LocationGet { target: _ } => {
215 let url = request.url.clone();
216 browser_response::LocationGet { value: url }.to_json()
217 }
218 CommandForBrowser::CookieGet { name: _ } => {
219 browser_response::CookieGet { value: "".into() }.to_json()
220 }
221 CommandForBrowser::CookieSet {
222 name: _,
223 value: _,
224 expires_in: _,
225 } => JsJson::Null,
226 CommandForBrowser::CookieJsonGet { name: _ } => browser_response::CookieJsonGet {
227 value: JsJson::Null,
228 }
229 .to_json(),
230 CommandForBrowser::CookieJsonSet {
231 name: _,
232 value: _,
233 expires_in: _,
234 } => JsJson::Null,
235 CommandForBrowser::GetEnv { name } => {
236 let env_value = request.env(name);
237
238 browser_response::GetEnv { value: env_value }.to_json()
239 }
240 CommandForBrowser::Log {
241 kind,
242 message,
243 arg2: _,
244 arg3: _,
245 arg4: _,
246 } => {
247 if kind == ConsoleLogLevel::Error {
248 log::warn!("{message}");
249 } else {
250 log::info!("{message}");
251 }
252
253 JsJson::Null
254 }
255 CommandForBrowser::TimezoneOffset => {
256 browser_response::TimezoneOffset { value: 0 }.to_json()
257 }
258 CommandForBrowser::HistoryBack => JsJson::Null,
259 CommandForBrowser::GetRandom { min, max: _ } => {
260 browser_response::GetRandom { value: min }.to_json()
261 }
262 CommandForBrowser::JsApiCall { commands: _ } => JsJson::Null,
263 CommandForBrowser::DomBulkUpdate { commands } => {
264 let blob_bytes = commands.len();
268 let decode_mark = probe.start();
269
270 match decode_dom_commands(&commands) {
271 Ok(list) => {
272 probe.decoded(decode_mark, blob_bytes, list.len());
275
276 sender
277 .send(Message::DomUpdate(list))
278 .inspect_err(|err| log::error!("Error sending DomUpdate: {err}"))
279 .unwrap_or_default();
280 }
281 Err(err) => log::error!("Error decoding DomBulkUpdate: {err}"),
282 }
283
284 JsJson::Null
285 }
286 }
287 });
288
289 let mut inst = WasmInstance::new(
290 &self.engine,
291 &self.instance_pre,
292 HostState {
293 request,
294 sender: sender.clone(),
295 probe: probe.clone(),
296 handle_command,
297 },
298 );
299 probe.instantiate(instantiate_mark);
300
301 inst.call_vertigo_entry_function();
306
307 if let Some(result) = inst.handle_url(url) {
308 return result;
309 }
310
311 let spawn_resource = SpawnOwner::new({
312 let sender = sender.clone();
313
314 async move {
315 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
316 let _ = sender.send(Message::TimeoutAndSendResponse);
317 }
318 });
319
320 let mut html_response = HtmlResponse::new(
321 sender.clone(),
322 &self.mount_config,
323 inst,
324 self.mount_config.env.clone(),
325 fetch,
326 probe.clone(),
327 );
328
329 loop {
330 let message = receiver.try_recv();
331
332 match message {
333 Ok(message) => {
334 if let Some(response) = html_response.process_message(message) {
335 return response;
336 };
337 continue;
338 }
339 Err(TryRecvError::Empty) => {} Err(TryRecvError::Disconnected) => {
341 break; }
343 }
344
345 if html_response.awaiting_response() {
346 let wait_mark = probe.start();
349 let message = receiver.recv().await;
350 probe.fetch_wait(wait_mark);
351
352 if let Some(message) = message
353 && let Some(response) = html_response.process_message(message)
354 {
355 return response;
356 };
357 } else {
358 break; }
360 }
361
362 spawn_resource.off();
363 html_response.build_response()
364 }
365}
366
367fn build_module_wasm(
368 engine: &Engine,
369 mount_path: &MountConfig,
370) -> Result<(Module, Duration), ErrorCode> {
371 let full_wasm_path = mount_path.get_wasm_fs_path();
372
373 log::info!("Mounting {} -> {full_wasm_path}", mount_path.mount_point());
374
375 let wasm_content = match std::fs::read(&full_wasm_path) {
376 Ok(wasm_content) => wasm_content,
377 Err(error) => {
378 log::error!("Problem reading the path: wasm_path={full_wasm_path}, error={error}");
379 return Err(ErrorCode::ServeWasmReadFailed);
380 }
381 };
382
383 let now = Instant::now();
384
385 let module = match Module::from_binary(engine, &wasm_content) {
386 Ok(module) => module,
387 Err(err) => {
388 log::error!("Wasm compilation error: error={err}");
389 return Err(ErrorCode::ServeWasmCompileFailed);
390 }
391 };
392
393 let elapsed = now.elapsed();
394 log::info!("WASM module compiled in {} ms.", elapsed.as_millis());
395 Ok((module, elapsed))
396}