use super::{AsyncRuntime, FetchResult};
use crate::AnalyzeError;
use std::future::Future;
use std::pin::Pin;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{Request, RequestInit, RequestMode, Response};
#[derive(Debug, Clone, Default)]
pub struct WasmRuntime;
impl WasmRuntime {
pub fn new() -> Self {
Self
}
async fn response_to_fetch_result(
url: String,
response: Response,
) -> Result<FetchResult, AnalyzeError> {
let status = response.status() as u16;
let headers_obj = response.headers();
let mut headers = Vec::new();
let body_promise = response
.text()
.map_err(|_| AnalyzeError::NetworkError("Failed to get response text".to_string()))?;
let body_js = JsFuture::from(body_promise)
.await
.map_err(|_| AnalyzeError::NetworkError("Failed to read response body".to_string()))?;
let body = body_js.as_string().ok_or_else(|| {
AnalyzeError::NetworkError("Response body is not a string".to_string())
})?;
Ok(FetchResult {
url,
status,
body,
headers,
})
}
fn create_request(url: &str, method: &str) -> Result<Request, AnalyzeError> {
let mut opts = RequestInit::new();
opts.method(method);
opts.mode(RequestMode::Cors);
Request::new_with_str_and_init(url, &opts).map_err(|_| {
AnalyzeError::NetworkError(format!("Failed to create request for {}", url))
})
}
}
impl AsyncRuntime for WasmRuntime {
fn fetch_url(
&self,
url: &str,
) -> Pin<Box<dyn Future<Output = Result<FetchResult, AnalyzeError>> + '_>> {
let url = url.to_string();
Box::pin(async move {
let window = web_sys::window().ok_or_else(|| {
AnalyzeError::NetworkError("No window object available".to_string())
})?;
let request = Self::create_request(&url, "GET")?;
let fetch_promise = window.fetch_with_request(&request);
let response_js = JsFuture::from(fetch_promise).await.map_err(|e| {
AnalyzeError::NetworkError(format!(
"Fetch failed for {}: {:?}",
url,
e.as_string().unwrap_or_else(|| "Unknown error".to_string())
))
})?;
let response: Response = response_js.dyn_into().map_err(|_| {
AnalyzeError::NetworkError("Failed to cast to Response".to_string())
})?;
if !response.ok() {
return Err(AnalyzeError::NetworkError(format!(
"HTTP error {}: {}",
response.status(),
url
)));
}
Self::response_to_fetch_result(url.clone(), response).await
})
}
fn check_url(
&self,
url: &str,
) -> Pin<Box<dyn Future<Output = Result<bool, AnalyzeError>> + '_>> {
let url = url.to_string();
Box::pin(async move {
let window = match web_sys::window() {
Some(w) => w,
None => return Ok(false),
};
let request = match Self::create_request(&url, "HEAD") {
Ok(r) => r,
Err(_) => return Ok(false),
};
let fetch_promise = window.fetch_with_request(&request);
match JsFuture::from(fetch_promise).await {
Ok(response_js) => {
if let Ok(response) = response_js.dyn_into::<Response>() {
let status = response.status();
Ok(response.ok() || status == 301 || status == 302)
} else {
Ok(false)
}
}
Err(_) => Ok(false),
}
})
}
fn fetch_batch(
&self,
urls: Vec<String>,
) -> Pin<Box<dyn Future<Output = Vec<Result<FetchResult, AnalyzeError>>> + '_>> {
Box::pin(async {
let mut results = Vec::new();
for url in urls {
let window = match web_sys::window() {
Some(w) => w,
None => {
results.push(Err(AnalyzeError::NetworkError(
"No window object".to_string(),
)));
continue;
}
};
let request = match Self::create_request(&url, "GET") {
Ok(r) => r,
Err(e) => {
results.push(Err(e));
continue;
}
};
let fetch_promise = window.fetch_with_request(&request);
match JsFuture::from(fetch_promise).await {
Ok(response_js) => match response_js.dyn_into::<Response>() {
Ok(response) => {
let result =
Self::response_to_fetch_result(url.clone(), response).await;
results.push(result);
}
Err(_) => {
results.push(Err(AnalyzeError::NetworkError(
"Failed to cast response".to_string(),
)));
}
},
Err(e) => {
results.push(Err(AnalyzeError::NetworkError(format!(
"Fetch failed: {:?}",
e.as_string().unwrap_or_else(|| "Unknown error".to_string())
))));
}
}
}
results
})
}
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + 'static,
{
spawn_local(future);
}
fn runtime_name(&self) -> &'static str {
"wasm"
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_wasm_runtime_name() {
let runtime = WasmRuntime::new();
assert_eq!(runtime.runtime_name(), "wasm");
}
}