diff --git a/Cargo.toml b/Cargo.toml index 8de8027c..80760a4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ path = "experiments/test_advanced_math.rs" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" js-sys = "0.3" -web-sys = { version = "0.3", features = ["Window", "Request", "RequestInit", "RequestMode", "Response", "Headers"] } +web-sys = { version = "0.3", features = ["Window", "WorkerGlobalScope", "Request", "RequestInit", "RequestMode", "Response", "Headers"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" console_error_panic_hook = "0.1" diff --git a/changelog.d/20260126_140137_fix_worker_fetch.md b/changelog.d/20260126_140137_fix_worker_fetch.md new file mode 100644 index 00000000..beb7fd59 --- /dev/null +++ b/changelog.d/20260126_140137_fix_worker_fetch.md @@ -0,0 +1,11 @@ +--- +bump: patch +--- + +### Fixed +- Currency API fetch now works in Web Worker context (fixes the root cause of issue #18) +- The `fetch_json` function now uses `js_sys::global()` to detect and use either `Window` or `WorkerGlobalScope` context +- Previously, `web_sys::window()` returned `None` in Web Workers, causing silent API fetch failures + +### Changed +- Added `WorkerGlobalScope` feature to web-sys dependency in Cargo.toml diff --git a/docs/case-studies/issue-18/README.md b/docs/case-studies/issue-18/README.md index ef167977..a4296ecc 100644 --- a/docs/case-studies/issue-18/README.md +++ b/docs/case-studies/issue-18/README.md @@ -29,34 +29,37 @@ Users need transparency about currency conversion rates for: ## Root Cause Analysis -### Primary Issue: Missing Rate Application Pipeline - -**Critical Finding**: The web worker successfully fetches exchange rates from the API but **never applies them to the Calculator instance**. - -In `web/src/worker.ts`: -```typescript -async function fetchExchangeRates() { - const responseJson = await fetch_exchange_rates('usd'); - const response: ExchangeRatesResponse = JSON.parse(responseJson); - - if (response.success) { - const rates = JSON.parse(response.rates_json); - // PROBLEM: rates are parsed but NEVER applied to calculator! - self.postMessage({ - type: 'ratesLoaded', - data: { ... ratesCount: Object.keys(rates).length } - }); - } +### Primary Issue: Web Worker Context Incompatibility + +**Critical Finding**: The currency API fetch code used `web_sys::window()` which returns `None` in a Web Worker context. Web Workers don't have a `window` object - they use `WorkerGlobalScope` instead. + +In `src/currency_api.rs` (before fix): +```rust +async fn fetch_json(url: &str) -> Result<(String, HashMap), CurrencyApiError> { + let window = web_sys::window() + .ok_or_else(|| CurrencyApiError::NetworkError("No window object available".to_string()))?; + // This always fails in a Web Worker! + // ... } ``` -The architecture has all the pieces but they're not connected: -1. `fetch_exchange_rates()` in WASM returns rates ✅ -2. Worker receives rates and counts them ✅ -3. **Worker never updates Calculator with the rates** ❌ -4. Result: Calculator uses hardcoded fallback rates +The fix requires checking for both Window and WorkerGlobalScope contexts: +```rust +let global = js_sys::global(); +let resp_value = if let Some(window) = global.dyn_ref::() { + JsFuture::from(window.fetch_with_request(&request)).await +} else if let Some(worker) = global.dyn_ref::() { + JsFuture::from(worker.fetch_with_request(&request)).await +} else { + return Err(CurrencyApiError::NetworkError("Neither Window nor WorkerGlobalScope available".to_string())); +}; +``` + +### Secondary Issue: Missing Rate Application Pipeline (Fixed in PR #38) + +The web worker originally fetched exchange rates from the API but **never applied them to the Calculator instance**. This was fixed by adding `update_rates_from_api` call in `web/src/worker.ts`. -### Secondary Issue: Hardcoded Fallback Rates +### Tertiary Issue: Hardcoded Fallback Rates The calculator uses **hardcoded exchange rates** in `src/types/currency.rs:188`: diff --git a/src/currency_api.rs b/src/currency_api.rs index fe8ba59c..45c0ddc3 100644 --- a/src/currency_api.rs +++ b/src/currency_api.rs @@ -107,10 +107,8 @@ async fn fetch_rates_from_url( } /// Performs the actual fetch and JSON parsing. +/// Works in both Window and Web Worker contexts. async fn fetch_json(url: &str) -> Result<(String, HashMap), CurrencyApiError> { - let window = web_sys::window() - .ok_or_else(|| CurrencyApiError::NetworkError("No window object available".to_string()))?; - let opts = RequestInit::new(); opts.set_method("GET"); opts.set_mode(RequestMode::Cors); @@ -124,9 +122,21 @@ async fn fetch_json(url: &str) -> Result<(String, HashMap), Currenc .set("Accept", "application/json") .map_err(|e| CurrencyApiError::NetworkError(format!("Failed to set headers: {:?}", e)))?; - let resp_value = JsFuture::from(window.fetch_with_request(&request)) - .await - .map_err(|e| CurrencyApiError::NetworkError(format!("Fetch failed: {:?}", e)))?; + // Use the global fetch function which works in both Window and Worker contexts. + // In a browser window, this is window.fetch; in a worker, this is self.fetch. + let global = js_sys::global(); + let resp_value = if let Some(window) = global.dyn_ref::() { + // Running in a Window context + JsFuture::from(window.fetch_with_request(&request)).await + } else if let Some(worker) = global.dyn_ref::() { + // Running in a Web Worker context + JsFuture::from(worker.fetch_with_request(&request)).await + } else { + return Err(CurrencyApiError::NetworkError( + "Neither Window nor WorkerGlobalScope available".to_string(), + )); + } + .map_err(|e| CurrencyApiError::NetworkError(format!("Fetch failed: {:?}", e)))?; let resp: Response = resp_value .dyn_into()