1use serde::{Deserialize, Serialize};
31use wasm_bindgen::prelude::*;
32use wasm_bindgen_futures::JsFuture;
33use web_sys::{Request, RequestInit, RequestMode, Response};
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct WasmProbeResult {
38 pub url: String,
40 pub success: bool,
42 pub status: Option<u16>,
44 pub error: Option<String>,
46 pub total_ms: f64,
48 pub dns_ms: Option<f64>,
50 pub tcp_ms: Option<f64>,
52 pub tls_ms: Option<f64>,
54 pub ttfb_ms: Option<f64>,
56 pub size_bytes: Option<u64>,
58 pub protocol: Option<String>,
60}
61
62#[wasm_bindgen]
64pub async fn probe_url(url: &str) -> JsValue {
65 let result = probe_url_internal(url).await;
66 serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL)
67}
68
69async fn probe_url_internal(url: &str) -> WasmProbeResult {
70 let start = now();
71
72 let opts = RequestInit::new();
73 opts.set_method("GET");
74 opts.set_mode(RequestMode::Cors);
75
76 let request = match Request::new_with_str_and_init(url, &opts) {
77 Ok(req) => req,
78 Err(e) => {
79 return WasmProbeResult {
80 url: url.to_string(),
81 success: false,
82 status: None,
83 error: Some(format!("failed to create request: {:?}", e)),
84 total_ms: now() - start,
85 dns_ms: None,
86 tcp_ms: None,
87 tls_ms: None,
88 ttfb_ms: None,
89 size_bytes: None,
90 protocol: None,
91 };
92 }
93 };
94
95 let window = match web_sys::window() {
96 Some(w) => w,
97 None => {
98 return WasmProbeResult {
99 url: url.to_string(),
100 success: false,
101 status: None,
102 error: Some("no window object available".to_string()),
103 total_ms: now() - start,
104 dns_ms: None,
105 tcp_ms: None,
106 tls_ms: None,
107 ttfb_ms: None,
108 size_bytes: None,
109 protocol: None,
110 };
111 }
112 };
113
114 let resp_value = match JsFuture::from(window.fetch_with_request(&request)).await {
115 Ok(v) => v,
116 Err(e) => {
117 return WasmProbeResult {
118 url: url.to_string(),
119 success: false,
120 status: None,
121 error: Some(format!("fetch failed: {:?}", e)),
122 total_ms: now() - start,
123 dns_ms: None,
124 tcp_ms: None,
125 tls_ms: None,
126 ttfb_ms: None,
127 size_bytes: None,
128 protocol: None,
129 };
130 }
131 };
132
133 let response: Response = match resp_value.dyn_into() {
134 Ok(r) => r,
135 Err(_) => {
136 return WasmProbeResult {
137 url: url.to_string(),
138 success: false,
139 status: None,
140 error: Some("fetch returned non-Response value".to_string()),
141 total_ms: now() - start,
142 dns_ms: None,
143 tcp_ms: None,
144 tls_ms: None,
145 ttfb_ms: None,
146 size_bytes: None,
147 protocol: None,
148 };
149 }
150 };
151
152 let status = response.status();
153 let total_ms = now() - start;
154
155 let timing = get_resource_timing(url);
157
158 WasmProbeResult {
159 url: url.to_string(),
160 success: (200..400).contains(&status),
161 status: Some(status),
162 error: None,
163 total_ms,
164 dns_ms: timing.as_ref().and_then(|t| t.dns_ms),
165 tcp_ms: timing.as_ref().and_then(|t| t.tcp_ms),
166 tls_ms: timing.as_ref().and_then(|t| t.tls_ms),
167 ttfb_ms: timing.as_ref().and_then(|t| t.ttfb_ms),
168 size_bytes: timing.as_ref().and_then(|t| t.size_bytes),
169 protocol: timing.and_then(|t| t.protocol),
170 }
171}
172
173struct ResourceTimingData {
175 dns_ms: Option<f64>,
176 tcp_ms: Option<f64>,
177 tls_ms: Option<f64>,
178 ttfb_ms: Option<f64>,
179 size_bytes: Option<u64>,
180 protocol: Option<String>,
181}
182
183fn get_resource_timing(url: &str) -> Option<ResourceTimingData> {
185 let window = web_sys::window()?;
186 let performance = window.performance()?;
187 let entries = performance.get_entries_by_name(url);
188
189 if entries.length() == 0 {
190 return None;
191 }
192
193 let entry = entries.get(entries.length() - 1);
195 let resource: web_sys::PerformanceResourceTiming = entry.dyn_into().ok()?;
196
197 let dns_start = resource.domain_lookup_start();
198 let dns_end = resource.domain_lookup_end();
199 let connect_start = resource.connect_start();
200 let connect_end = resource.connect_end();
201 let secure_start = resource.secure_connection_start();
202 let response_start = resource.response_start();
203
204 let dns_ms = if dns_end > dns_start {
205 Some(dns_end - dns_start)
206 } else {
207 None
208 };
209
210 let tcp_ms = if secure_start > 0.0 && secure_start > connect_start {
211 Some(secure_start - connect_start)
212 } else if connect_end > connect_start {
213 Some(connect_end - connect_start)
214 } else {
215 None
216 };
217
218 let tls_ms = if secure_start > 0.0 && connect_end > secure_start {
219 Some(connect_end - secure_start)
220 } else {
221 None
222 };
223
224 let ttfb_ms = if response_start > 0.0 {
225 Some(response_start)
226 } else {
227 None
228 };
229
230 let size_bytes = {
231 let size = resource.transfer_size();
232 if size > 0.0 {
233 Some(size as u64)
234 } else {
235 None
236 }
237 };
238
239 let protocol = {
240 let proto = resource.next_hop_protocol();
241 if proto.is_empty() {
242 None
243 } else {
244 Some(proto)
245 }
246 };
247
248 Some(ResourceTimingData {
249 dns_ms,
250 tcp_ms,
251 tls_ms,
252 ttfb_ms,
253 size_bytes,
254 protocol,
255 })
256}
257
258#[wasm_bindgen]
260pub fn get_navigation_timing() -> JsValue {
261 let timing = get_navigation_timing_internal();
262 serde_wasm_bindgen::to_value(&timing).unwrap_or(JsValue::NULL)
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct WasmNavigationTiming {
268 pub url: String,
270 pub dns_ms: f64,
272 pub tcp_ms: f64,
274 pub tls_ms: f64,
276 pub ttfb_ms: f64,
278 pub dom_content_loaded_ms: f64,
280 pub load_ms: f64,
282}
283
284fn get_navigation_timing_internal() -> Option<WasmNavigationTiming> {
285 let window = web_sys::window()?;
286 let performance = window.performance()?;
287 let entries = performance.get_entries_by_type("navigation");
288
289 if entries.length() == 0 {
290 return None;
291 }
292
293 let entry = entries.get(0);
294 let nav: web_sys::PerformanceNavigationTiming = entry.dyn_into().ok()?;
295
296 let dns_start = nav.domain_lookup_start();
297 let dns_end = nav.domain_lookup_end();
298 let connect_start = nav.connect_start();
299 let connect_end = nav.connect_end();
300 let secure_start = nav.secure_connection_start();
301 let response_start = nav.response_start();
302 let dom_content_loaded = nav.dom_content_loaded_event_end();
303 let load_end = nav.load_event_end();
304
305 let dns_ms = (dns_end - dns_start).max(0.0);
306 let tcp_ms = if secure_start > 0.0 {
307 (secure_start - connect_start).max(0.0)
308 } else {
309 (connect_end - connect_start).max(0.0)
310 };
311 let tls_ms = if secure_start > 0.0 {
312 (connect_end - secure_start).max(0.0)
313 } else {
314 0.0
315 };
316 let ttfb_ms = response_start.max(0.0);
317 let dom_content_loaded_ms = dom_content_loaded.max(0.0);
318 let load_ms = load_end.max(0.0);
319
320 Some(WasmNavigationTiming {
321 url: window.location().href().ok()?,
322 dns_ms,
323 tcp_ms,
324 tls_ms,
325 ttfb_ms,
326 dom_content_loaded_ms,
327 load_ms,
328 })
329}
330
331#[wasm_bindgen]
333pub fn get_resource_timings() -> JsValue {
334 let timings = get_resource_timings_internal();
335 serde_wasm_bindgen::to_value(&timings).unwrap_or(JsValue::NULL)
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct WasmResourceTiming {
341 pub url: String,
343 pub initiator_type: String,
345 pub duration_ms: f64,
347 pub size_bytes: u64,
349 pub from_cache: bool,
351 pub protocol: Option<String>,
353}
354
355fn get_resource_timings_internal() -> Vec<WasmResourceTiming> {
356 let Some(window) = web_sys::window() else {
357 return Vec::new();
358 };
359 let Some(performance) = window.performance() else {
360 return Vec::new();
361 };
362
363 let entries = performance.get_entries_by_type("resource");
364 let mut results = Vec::with_capacity(entries.length() as usize);
365
366 for i in 0..entries.length() {
367 let entry = entries.get(i);
368 if let Ok(resource) = entry.dyn_into::<web_sys::PerformanceResourceTiming>() {
369 let transfer_size = resource.transfer_size() as u64;
370 let decoded_size = resource.decoded_body_size() as u64;
371
372 results.push(WasmResourceTiming {
373 url: resource.name(),
374 initiator_type: resource.initiator_type(),
375 duration_ms: resource.duration(),
376 size_bytes: transfer_size,
377 from_cache: transfer_size == 0 && decoded_size > 0,
378 protocol: {
379 let p = resource.next_hop_protocol();
380 if p.is_empty() {
381 None
382 } else {
383 Some(p)
384 }
385 },
386 });
387 }
388 }
389
390 results
391}
392
393#[wasm_bindgen]
395extern "C" {
396 #[wasm_bindgen(thread_local_v2, js_namespace = navigator, js_name = connection)]
397 static CONNECTION: JsValue;
398
399 #[wasm_bindgen(catch, js_namespace = ["navigator", "connection"], js_name = type)]
400 fn connection_type() -> Result<JsValue, JsValue>;
401
402 #[wasm_bindgen(catch, js_namespace = ["navigator", "connection"], js_name = effectiveType)]
403 fn effective_type() -> Result<JsValue, JsValue>;
404
405 #[wasm_bindgen(catch, js_namespace = ["navigator", "connection"], js_name = rtt)]
406 fn connection_rtt() -> Result<JsValue, JsValue>;
407
408 #[wasm_bindgen(catch, js_namespace = ["navigator", "connection"], js_name = downlink)]
409 fn connection_downlink() -> Result<JsValue, JsValue>;
410
411 #[wasm_bindgen(catch, js_namespace = ["navigator", "connection"], js_name = saveData)]
412 fn connection_save_data() -> Result<JsValue, JsValue>;
413}
414
415#[wasm_bindgen]
420pub fn get_connection_info() -> JsValue {
421 let info = get_connection_info_internal();
422 serde_wasm_bindgen::to_value(&info).unwrap_or(JsValue::NULL)
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct WasmConnectionInfo {
428 pub connection_type: Option<String>,
430 pub effective_type: Option<String>,
432 pub rtt_ms: Option<u32>,
434 pub downlink_mbps: Option<f64>,
436 pub save_data: bool,
438 pub api_supported: bool,
440}
441
442fn get_connection_info_internal() -> WasmConnectionInfo {
443 let conn = CONNECTION.with(JsValue::clone);
445 if conn.is_undefined() || conn.is_null() {
446 return WasmConnectionInfo {
447 connection_type: None,
448 effective_type: None,
449 rtt_ms: None,
450 downlink_mbps: None,
451 save_data: false,
452 api_supported: false,
453 };
454 }
455
456 let connection_type = connection_type().ok().and_then(|v| v.as_string());
457
458 let effective_type = effective_type().ok().and_then(|v| v.as_string());
459
460 let rtt_ms = connection_rtt()
461 .ok()
462 .and_then(|v| v.as_f64())
463 .map(|v| v as u32);
464
465 let downlink_mbps = connection_downlink().ok().and_then(|v| v.as_f64());
466
467 let save_data = connection_save_data()
468 .ok()
469 .and_then(|v| v.as_bool())
470 .unwrap_or(false);
471
472 WasmConnectionInfo {
473 connection_type,
474 effective_type,
475 rtt_ms,
476 downlink_mbps,
477 save_data,
478 api_supported: true,
479 }
480}
481
482fn now() -> f64 {
484 web_sys::window()
485 .and_then(|w| w.performance())
486 .map(|p| p.now())
487 .unwrap_or(0.0)
488}
489
490#[wasm_bindgen]
492pub fn log(s: &str) {
493 web_sys::console::log_1(&s.into());
494}