Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions changelog.d/20260126_140137_fix_worker_fetch.md
Original file line number Diff line number Diff line change
@@ -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
51 changes: 27 additions & 24 deletions docs/case-studies/issue-18/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, f64>), 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::<web_sys::Window>() {
JsFuture::from(window.fetch_with_request(&request)).await
} else if let Some(worker) = global.dyn_ref::<web_sys::WorkerGlobalScope>() {
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`:

Expand Down
22 changes: 16 additions & 6 deletions src/currency_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, f64>), 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);
Expand All @@ -124,9 +122,21 @@ async fn fetch_json(url: &str) -> Result<(String, HashMap<String, f64>), 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::<web_sys::Window>() {
// Running in a Window context
JsFuture::from(window.fetch_with_request(&request)).await
} else if let Some(worker) = global.dyn_ref::<web_sys::WorkerGlobalScope>() {
// 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()
Expand Down