webpage_quality_analyzer 1.0.2

High-performance webpage quality analyzer with 115 comprehensive metrics - Rust library with WASM, C++, and Python bindings
Documentation
//! WASM-based async runtime for browser environments
//!
//! This implementation uses:
//! - wasm-bindgen-futures for async/Promise bridge
//! - web-sys for browser fetch API
//! - spawn_local for background tasks

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};

/// WASM-based runtime implementation using browser APIs
#[derive(Debug, Clone, Default)]
pub struct WasmRuntime;

impl WasmRuntime {
    pub fn new() -> Self {
        Self
    }

    /// Convert web_sys Response to FetchResult
    async fn response_to_fetch_result(
        url: String,
        response: Response,
    ) -> Result<FetchResult, AnalyzeError> {
        let status = response.status() as u16;

        // Extract headers
        let headers_obj = response.headers();
        let mut headers = Vec::new();

        // Get body text
        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,
        })
    }

    /// Create a fetch request
    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 {
            // Get window object
            let window = web_sys::window().ok_or_else(|| {
                AnalyzeError::NetworkError("No window object available".to_string())
            })?;

            // Create request
            let request = Self::create_request(&url, "GET")?;

            // Perform fetch
            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())
            })?;

            // Check status
            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 {
            // Get window object
            let window = match web_sys::window() {
                Some(w) => w,
                None => return Ok(false),
            };

            // Create HEAD request
            let request = match Self::create_request(&url, "HEAD") {
                Ok(r) => r,
                Err(_) => return Ok(false),
            };

            // Perform fetch
            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();

            // Sequential execution for WASM to avoid lifetime issues
            // Browser environment typically handles parallelism internally
            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");
    }
}