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