1#![deny(unsafe_code)]
12#![allow(clippy::missing_errors_doc)]
13#![allow(clippy::needless_pass_by_value)]
15
16pub mod error;
17mod exports;
18pub mod types;
19
20pub use exports::{
21 abi_fingerprint, abi_version, browser_operator_snapshot, fetch_request, runtime_close,
22 runtime_create, scope_close, scope_enter, task_cancel, task_join, task_spawn, websocket_cancel,
23 websocket_close, websocket_open, websocket_recv, websocket_send,
24};
25
26use crate::error::dispatch_error_json;
27use crate::types::{
28 BrowserOperatorConsoleSnapshot, decode_json_payload, decode_optional_consumer_version,
29 encode_json_payload,
30};
31use asupersync::types::WasmDispatcherDiagnostics;
32use asupersync::types::{
33 WASM_ABI_MAJOR_VERSION, WASM_ABI_MINOR_VERSION, WASM_ABI_SIGNATURE_FINGERPRINT_V1,
34 WasmAbiCancellation, WasmAbiErrorCode, WasmAbiFailure, WasmAbiOutcomeEnvelope,
35 WasmAbiRecoverability, WasmAbiValue, WasmAbiVersion, WasmDispatchError, WasmExportDispatcher,
36 WasmFetchRequest, WasmHandleRef, WasmScopeEnterRequest, WasmTaskCancelRequest,
37 WasmTaskSpawnRequest,
38};
39use std::cell::RefCell;
40use std::collections::{HashMap, VecDeque};
41#[cfg(target_arch = "wasm32")]
42use std::rc::Rc;
43#[cfg(target_arch = "wasm32")]
44use wasm_bindgen::closure::Closure;
45#[cfg(target_arch = "wasm32")]
46use wasm_bindgen::{JsCast, JsValue};
47#[cfg(target_arch = "wasm32")]
48use wasm_bindgen_futures::{JsFuture, spawn_local};
49#[cfg(target_arch = "wasm32")]
50use web_sys::{
51 AbortController, BinaryType, CloseEvent, Event, MessageEvent, RequestInit, Response, WebSocket,
52 WorkerGlobalScope,
53};
54
55thread_local! {
56 static DISPATCHER: RefCell<WasmExportDispatcher> = RefCell::new(WasmExportDispatcher::new());
57}
58#[cfg(target_arch = "wasm32")]
59thread_local! {
60 static INFLIGHT_FETCHES: RefCell<HashMap<WasmHandleRef, AbortController>> = RefCell::new(HashMap::new());
61}
62thread_local! {
63 static INFLIGHT_WEBSOCKETS: RefCell<HashMap<WasmHandleRef, BrowserWebSocketHostState>> = RefCell::new(HashMap::new());
64}
65
66#[derive(Debug, Clone, serde::Deserialize)]
67struct BrowserWebSocketOpenRequest {
68 scope: WasmHandleRef,
69 url: String,
70 protocols: Option<Vec<String>>,
71}
72
73#[derive(Debug, Clone, serde::Deserialize)]
74struct BrowserWebSocketSendRequest {
75 socket: WasmHandleRef,
76 value: WasmAbiValue,
77}
78
79#[derive(Debug, Clone, serde::Deserialize)]
80struct BrowserWebSocketRecvRequest {
81 socket: WasmHandleRef,
82}
83
84#[derive(Debug, Clone, serde::Deserialize)]
85struct BrowserWebSocketCloseRequest {
86 socket: WasmHandleRef,
87 reason: Option<String>,
88}
89
90#[derive(Debug, Clone, serde::Deserialize)]
91struct BrowserWebSocketCancelRequest {
92 socket: WasmHandleRef,
93 kind: String,
94 message: Option<String>,
95}
96
97#[cfg(target_arch = "wasm32")]
98struct BrowserWebSocketHostState {
99 socket: WebSocket,
100 inbox: Rc<RefCell<VecDeque<WasmAbiOutcomeEnvelope>>>,
101 _on_message: Closure<dyn FnMut(MessageEvent)>,
102 _on_close: Closure<dyn FnMut(CloseEvent)>,
103 _on_error: Closure<dyn FnMut(Event)>,
104}
105
106#[cfg(target_arch = "wasm32")]
107impl Drop for BrowserWebSocketHostState {
108 fn drop(&mut self) {
109 self.socket.set_onmessage(None);
110 self.socket.set_onclose(None);
111 self.socket.set_onerror(None);
112 let _ = self.socket.close();
113 }
114}
115
116#[cfg(not(target_arch = "wasm32"))]
117struct BrowserWebSocketHostState {
118 inbox: VecDeque<WasmAbiOutcomeEnvelope>,
119 closed: bool,
120}
121
122fn parse_json<T: serde::de::DeserializeOwned>(raw: &str, field: &str) -> Result<T, String> {
123 decode_json_payload(raw, field)
124}
125
126fn encode_json<T: serde::Serialize>(value: &T, field: &str) -> Result<String, String> {
127 encode_json_payload(value, field)
128}
129
130fn parse_consumer_version(raw: Option<String>) -> Result<Option<WasmAbiVersion>, String> {
131 decode_optional_consumer_version(raw)
132}
133
134fn to_error_string(err: WasmDispatchError) -> String {
135 dispatch_error_json(&err)
136}
137
138fn with_dispatcher<R>(
139 f: impl FnOnce(&mut WasmExportDispatcher) -> Result<R, WasmDispatchError>,
140) -> Result<R, String> {
141 DISPATCHER.with(|dispatcher| {
142 let mut dispatcher = dispatcher.borrow_mut();
143 f(&mut dispatcher).map_err(to_error_string)
144 })
145}
146
147fn dispatcher_handle_is_live(handle: &WasmHandleRef) -> bool {
148 DISPATCHER.with(|dispatcher| dispatcher.borrow().handles().get(handle).is_ok())
149}
150
151#[cfg(target_arch = "wasm32")]
152fn cleanup_released_fetches() {
153 INFLIGHT_FETCHES.with(|inflight| {
154 inflight
155 .borrow_mut()
156 .retain(|handle, _| dispatcher_handle_is_live(handle));
157 });
158}
159
160#[cfg(not(target_arch = "wasm32"))]
161const fn cleanup_released_fetches() {}
162
163fn cleanup_released_websockets() {
164 INFLIGHT_WEBSOCKETS.with(|sockets| {
165 sockets
166 .borrow_mut()
167 .retain(|handle, _| dispatcher_handle_is_live(handle));
168 });
169}
170
171fn cleanup_released_host_state() {
172 cleanup_released_fetches();
173 cleanup_released_websockets();
174}
175
176fn normalize_fetch_method(method: &str) -> Result<String, String> {
177 let normalized = method.trim().to_ascii_uppercase();
178 if normalized.is_empty() {
179 return Err("fetch method must not be empty".to_string());
180 }
181 match normalized.as_str() {
182 "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" => Ok(normalized),
183 _ => Err(format!("unsupported fetch method: {normalized}")),
184 }
185}
186
187fn normalize_fetch_request(request: WasmFetchRequest) -> Result<WasmFetchRequest, String> {
188 let method = normalize_fetch_method(&request.method)?;
189 if matches!(method.as_str(), "GET" | "HEAD") && request.body.is_some() {
190 return Err(format!(
191 "fetch method {method} does not permit a request body"
192 ));
193 }
194 Ok(WasmFetchRequest { method, ..request })
195}
196
197const fn fetch_pending_outcome(handle: WasmHandleRef) -> WasmAbiOutcomeEnvelope {
198 WasmAbiOutcomeEnvelope::Ok {
199 value: WasmAbiValue::Handle(handle),
200 }
201}
202
203#[allow(clippy::missing_const_for_fn)]
204fn fetch_error_outcome(
205 message: String,
206 recoverability: WasmAbiRecoverability,
207) -> WasmAbiOutcomeEnvelope {
208 WasmAbiOutcomeEnvelope::Err {
209 failure: WasmAbiFailure {
210 code: WasmAbiErrorCode::InternalFailure,
211 recoverability,
212 message,
213 },
214 }
215}
216
217fn cancelled_outcome(
218 kind: &str,
219 phase: &str,
220 message: Option<String>,
221 origin_task: Option<String>,
222) -> WasmAbiOutcomeEnvelope {
223 WasmAbiOutcomeEnvelope::Cancelled {
224 cancellation: WasmAbiCancellation {
225 kind: kind.to_string(),
226 phase: phase.to_string(),
227 origin_region: "browser".to_string(),
228 origin_task,
229 timestamp_nanos: 0,
230 message,
231 truncated: false,
232 },
233 }
234}
235
236#[cfg(target_arch = "wasm32")]
237fn take_inflight_fetch(handle: &WasmHandleRef) -> Option<AbortController> {
238 INFLIGHT_FETCHES.with(|inflight| inflight.borrow_mut().remove(handle))
239}
240
241#[cfg(target_arch = "wasm32")]
242fn register_inflight_fetch(handle: WasmHandleRef, controller: AbortController) {
243 INFLIGHT_FETCHES.with(|inflight| {
244 inflight.borrow_mut().insert(handle, controller);
245 });
246}
247
248#[cfg(target_arch = "wasm32")]
249fn js_value_message(value: &JsValue) -> String {
250 value
251 .as_string()
252 .or_else(|| {
253 js_sys::JSON::stringify(value)
254 .ok()
255 .and_then(|json| json.as_string())
256 })
257 .unwrap_or_else(|| "non-string JS error".to_string())
258}
259
260#[cfg(target_arch = "wasm32")]
261fn js_error_name(value: &JsValue) -> Option<String> {
262 js_sys::Reflect::get(value, &JsValue::from_str("name"))
263 .ok()
264 .and_then(|name| name.as_string())
265}
266
267#[cfg(target_arch = "wasm32")]
268fn abort_cancelled_outcome(message: String) -> WasmAbiOutcomeEnvelope {
269 cancelled_outcome("abort_signal", "cancelling", Some(message), None)
270}
271
272fn normalize_websocket_url(url: &str) -> Result<String, String> {
273 let normalized = url.trim();
274 if normalized.is_empty() {
275 return Err("websocket URL must not be empty".to_string());
276 }
277 let (scheme, rest) = normalized
278 .split_once("://")
279 .ok_or_else(|| format!("websocket URL must start with ws:// or wss://: {normalized}"))?;
280 if !(scheme.eq_ignore_ascii_case("ws") || scheme.eq_ignore_ascii_case("wss")) {
281 return Err(format!(
282 "websocket URL must start with ws:// or wss://: {normalized}"
283 ));
284 }
285 Ok(format!("{}://{rest}", scheme.to_ascii_lowercase()))
286}
287
288const fn websocket_pending_outcome(handle: WasmHandleRef) -> WasmAbiOutcomeEnvelope {
289 WasmAbiOutcomeEnvelope::Ok {
290 value: WasmAbiValue::Handle(handle),
291 }
292}
293
294const fn websocket_idle_outcome() -> WasmAbiOutcomeEnvelope {
295 WasmAbiOutcomeEnvelope::Ok {
296 value: WasmAbiValue::Unit,
297 }
298}
299
300const fn websocket_send_outcome() -> WasmAbiOutcomeEnvelope {
301 WasmAbiOutcomeEnvelope::Ok {
302 value: WasmAbiValue::Unit,
303 }
304}
305
306fn spawn_websocket_handle(
307 scope: WasmHandleRef,
308 consumer_version: Option<WasmAbiVersion>,
309) -> Result<WasmHandleRef, String> {
310 let spawn = WasmTaskSpawnRequest {
311 scope,
312 label: Some("browser-websocket".to_string()),
313 cancel_kind: Some("abort_signal".to_string()),
314 };
315 with_dispatcher(|dispatcher| dispatcher.task_spawn(&spawn, consumer_version))
316}
317
318fn finalize_websocket_handle(
319 handle: &WasmHandleRef,
320 outcome: WasmAbiOutcomeEnvelope,
321 consumer_version: Option<WasmAbiVersion>,
322) -> Result<WasmAbiOutcomeEnvelope, String> {
323 with_dispatcher(|dispatcher| dispatcher.task_join(handle, outcome, consumer_version))
324}
325
326fn cancel_websocket_handle(
327 request: &WasmTaskCancelRequest,
328 consumer_version: Option<WasmAbiVersion>,
329) -> Result<WasmAbiOutcomeEnvelope, String> {
330 with_dispatcher(|dispatcher| dispatcher.task_cancel(request, consumer_version))
331}
332
333fn with_websocket_state_mut<R>(
334 handle: &WasmHandleRef,
335 f: impl FnOnce(&mut BrowserWebSocketHostState) -> Result<R, String>,
336) -> Result<R, String> {
337 INFLIGHT_WEBSOCKETS.with(|sockets| {
338 let mut sockets = sockets.borrow_mut();
339 let state = sockets
340 .get_mut(handle)
341 .ok_or_else(|| format!("unknown websocket handle: {handle:?}"))?;
342 f(state)
343 })
344}
345
346fn take_websocket_state(handle: &WasmHandleRef) -> Option<BrowserWebSocketHostState> {
347 INFLIGHT_WEBSOCKETS.with(|sockets| sockets.borrow_mut().remove(handle))
348}
349
350fn insert_websocket_state(handle: WasmHandleRef, state: BrowserWebSocketHostState) {
351 INFLIGHT_WEBSOCKETS.with(|sockets| {
352 sockets.borrow_mut().insert(handle, state);
353 });
354}
355
356#[cfg(target_arch = "wasm32")]
357fn finalize_fetch_outcome(handle: WasmHandleRef, outcome: WasmAbiOutcomeEnvelope) {
358 if take_inflight_fetch(&handle).is_none() {
359 return;
360 }
361 if matches!(outcome, WasmAbiOutcomeEnvelope::Cancelled { .. }) {
362 let _ = with_dispatcher(|dispatcher| dispatcher.apply_abort(&handle));
363 }
364 let _ = with_dispatcher(|dispatcher| dispatcher.fetch_complete(&handle, outcome));
365}
366
367#[cfg(target_arch = "wasm32")]
368fn host_fetch_with_str_and_init(url: &str, init: &RequestInit) -> Result<js_sys::Promise, String> {
369 if let Some(window) = web_sys::window() {
370 return Ok(window.fetch_with_str_and_init(url, init));
371 }
372
373 if let Ok(worker) = js_sys::global().dyn_into::<WorkerGlobalScope>() {
374 return Ok(worker.fetch_with_str_and_init(url, init));
375 }
376
377 Err("window or WorkerGlobalScope fetch host is not available in this host context".to_string())
378}
379
380#[cfg(target_arch = "wasm32")]
381async fn run_browser_fetch(
382 request: WasmFetchRequest,
383 signal: web_sys::AbortSignal,
384) -> WasmAbiOutcomeEnvelope {
385 let init = RequestInit::new();
386 init.set_method(&request.method);
387 init.set_signal(Some(&signal));
388 if let Some(body) = request.body {
389 let body = js_sys::Uint8Array::from(body.as_slice());
390 init.set_body(&body.into());
391 }
392
393 let fetch_promise = match host_fetch_with_str_and_init(&request.url, &init) {
394 Ok(fetch_promise) => fetch_promise,
395 Err(message) => {
396 return fetch_error_outcome(message, WasmAbiRecoverability::Permanent);
397 }
398 };
399 match JsFuture::from(fetch_promise).await {
400 Ok(response_value) => {
401 let status = response_value
402 .dyn_into::<Response>()
403 .ok()
404 .map(|response| u64::from(response.status()));
405 let value = status.map_or(WasmAbiValue::Unit, WasmAbiValue::U64);
406 WasmAbiOutcomeEnvelope::Ok { value }
407 }
408 Err(error) => {
409 let message = js_value_message(&error);
410 if js_error_name(&error).as_deref() == Some("AbortError") {
411 abort_cancelled_outcome(format!("fetch aborted by AbortSignal: {message}"))
412 } else {
413 fetch_error_outcome(
414 format!("browser fetch rejected: {message}"),
415 WasmAbiRecoverability::Transient,
416 )
417 }
418 }
419 }
420}
421
422#[cfg(target_arch = "wasm32")]
423fn spawn_browser_fetch(handle: WasmHandleRef, request: WasmFetchRequest) -> Result<(), String> {
424 let controller = AbortController::new().map_err(|err| {
425 format!(
426 "failed to create AbortController for fetch handle {:?}: {}",
427 handle,
428 js_value_message(&err)
429 )
430 })?;
431 let signal = controller.signal();
432 register_inflight_fetch(handle, controller);
433 spawn_local(async move {
434 let outcome = run_browser_fetch(request, signal).await;
435 finalize_fetch_outcome(handle, outcome);
436 });
437 Ok(())
438}
439
440#[cfg(target_arch = "wasm32")]
441fn websocket_outcome_from_message_event(event: MessageEvent) -> WasmAbiOutcomeEnvelope {
442 let payload = event.data();
443 if let Some(text) = payload.as_string() {
444 return WasmAbiOutcomeEnvelope::Ok {
445 value: WasmAbiValue::String(text),
446 };
447 }
448 if let Ok(buffer) = payload.dyn_into::<js_sys::ArrayBuffer>() {
449 let bytes = js_sys::Uint8Array::new(&buffer).to_vec();
450 return WasmAbiOutcomeEnvelope::Ok {
451 value: WasmAbiValue::Bytes(bytes),
452 };
453 }
454 fetch_error_outcome(
455 "websocket message payload type is unsupported".to_string(),
456 WasmAbiRecoverability::Unknown,
457 )
458}
459
460#[cfg(target_arch = "wasm32")]
461fn setup_browser_websocket(
462 handle: WasmHandleRef,
463 request: &BrowserWebSocketOpenRequest,
464) -> Result<(), String> {
465 let socket = if let Some(protocols) = request.protocols.as_ref() {
466 if protocols.is_empty() {
467 WebSocket::new(&request.url)
468 } else {
469 let js_protocols = js_sys::Array::new();
470 for protocol in protocols {
471 js_protocols.push(&JsValue::from_str(protocol));
472 }
473 WebSocket::new_with_str_sequence(&request.url, &js_protocols)
474 }
475 } else {
476 WebSocket::new(&request.url)
477 }
478 .map_err(|err| {
479 format!(
480 "failed to construct browser WebSocket: {}",
481 js_value_message(&err)
482 )
483 })?;
484 socket.set_binary_type(BinaryType::Arraybuffer);
485
486 let inbox = Rc::new(RefCell::new(VecDeque::new()));
487 let inbox_for_message = Rc::clone(&inbox);
488 let on_message = Closure::wrap(Box::new(move |event: MessageEvent| {
489 inbox_for_message
490 .borrow_mut()
491 .push_back(websocket_outcome_from_message_event(event));
492 }) as Box<dyn FnMut(MessageEvent)>);
493
494 let inbox_for_close = Rc::clone(&inbox);
495 let on_close = Closure::wrap(Box::new(move |event: CloseEvent| {
496 let message = if event.reason().is_empty() {
497 format!("websocket closed with code {}", event.code())
498 } else {
499 format!(
500 "websocket closed with code {} ({})",
501 event.code(),
502 event.reason()
503 )
504 };
505 inbox_for_close.borrow_mut().push_back(cancelled_outcome(
506 "websocket_close",
507 "completed",
508 Some(message),
509 None,
510 ));
511 }) as Box<dyn FnMut(CloseEvent)>);
512
513 let inbox_for_error = Rc::clone(&inbox);
514 let on_error = Closure::wrap(Box::new(move |_event: Event| {
515 inbox_for_error.borrow_mut().push_back(fetch_error_outcome(
516 "browser websocket error event".to_string(),
517 WasmAbiRecoverability::Transient,
518 ));
519 }) as Box<dyn FnMut(Event)>);
520
521 socket.set_onmessage(Some(on_message.as_ref().unchecked_ref()));
522 socket.set_onclose(Some(on_close.as_ref().unchecked_ref()));
523 socket.set_onerror(Some(on_error.as_ref().unchecked_ref()));
524
525 INFLIGHT_WEBSOCKETS.with(|sockets| {
526 sockets.borrow_mut().insert(
527 handle,
528 BrowserWebSocketHostState {
529 socket,
530 inbox,
531 _on_message: on_message,
532 _on_close: on_close,
533 _on_error: on_error,
534 },
535 );
536 });
537
538 Ok(())
539}
540
541#[cfg(not(target_arch = "wasm32"))]
542#[allow(clippy::unnecessary_wraps)]
543fn setup_browser_websocket(
544 handle: WasmHandleRef,
545 request: &BrowserWebSocketOpenRequest,
546) -> Result<(), String> {
547 let _requested_protocols = request.protocols.as_ref().map(std::vec::Vec::len);
548 INFLIGHT_WEBSOCKETS.with(|sockets| {
549 sockets.borrow_mut().insert(
550 handle,
551 BrowserWebSocketHostState {
552 inbox: VecDeque::new(),
553 closed: false,
554 },
555 );
556 });
557 Ok(())
558}
559
560#[cfg(target_arch = "wasm32")]
561fn send_browser_websocket_message(
562 handle: &WasmHandleRef,
563 value: WasmAbiValue,
564) -> Result<(), String> {
565 with_websocket_state_mut(handle, |state| match value {
566 WasmAbiValue::String(text) => state.socket.send_with_str(&text).map_err(|err| {
567 format!(
568 "websocket send_with_str failed for {:?}: {}",
569 handle,
570 js_value_message(&err)
571 )
572 }),
573 WasmAbiValue::Bytes(bytes) => state.socket.send_with_u8_array(&bytes).map_err(|err| {
574 format!(
575 "websocket send_with_u8_array failed for {:?}: {}",
576 handle,
577 js_value_message(&err)
578 )
579 }),
580 other => Err(format!(
581 "websocket send requires string/bytes payload, got {other:?}"
582 )),
583 })
584}
585
586#[cfg(not(target_arch = "wasm32"))]
587fn send_browser_websocket_message(
588 handle: &WasmHandleRef,
589 value: WasmAbiValue,
590) -> Result<(), String> {
591 with_websocket_state_mut(handle, |state| {
592 if state.closed {
593 return Err(format!("websocket handle {handle:?} is already closed"));
594 }
595 match value {
596 WasmAbiValue::String(text) => state.inbox.push_back(WasmAbiOutcomeEnvelope::Ok {
597 value: WasmAbiValue::String(text),
598 }),
599 WasmAbiValue::Bytes(bytes) => state.inbox.push_back(WasmAbiOutcomeEnvelope::Ok {
600 value: WasmAbiValue::Bytes(bytes),
601 }),
602 other => {
603 return Err(format!(
604 "websocket send requires string/bytes payload, got {other:?}"
605 ));
606 }
607 }
608 Ok(())
609 })
610}
611
612fn recv_browser_websocket_message(
613 handle: &WasmHandleRef,
614) -> Result<WasmAbiOutcomeEnvelope, String> {
615 with_websocket_state_mut(handle, |state| {
616 #[cfg(target_arch = "wasm32")]
617 let next = state.inbox.borrow_mut().pop_front();
618 #[cfg(not(target_arch = "wasm32"))]
619 let next = state.inbox.pop_front();
620
621 Ok(next.unwrap_or_else(websocket_idle_outcome))
622 })
623}
624
625const MAX_WEBSOCKET_CLOSE_REASON_BYTES: usize = 123;
626
627fn validate_websocket_close_reason(reason: &str) -> Result<(), String> {
628 if reason.len() > MAX_WEBSOCKET_CLOSE_REASON_BYTES {
629 return Err(format!(
630 "websocket close reason exceeds {MAX_WEBSOCKET_CLOSE_REASON_BYTES} bytes"
631 ));
632 }
633 Ok(())
634}
635
636#[cfg(target_arch = "wasm32")]
637fn close_browser_websocket_socket(
638 state: &mut BrowserWebSocketHostState,
639 reason: Option<&str>,
640) -> Result<(), String> {
641 if let Some(reason) = reason {
642 validate_websocket_close_reason(reason)?;
643 state
644 .socket
645 .close_with_code_and_reason(1000, reason)
646 .map_err(|err| format!("websocket close failed: {}", js_value_message(&err)))?;
647 } else {
648 state
649 .socket
650 .close()
651 .map_err(|err| format!("websocket close failed: {}", js_value_message(&err)))?;
652 }
653 Ok(())
654}
655
656#[cfg(not(target_arch = "wasm32"))]
657#[allow(clippy::unnecessary_wraps)]
658fn close_browser_websocket_socket(
659 state: &mut BrowserWebSocketHostState,
660 reason: Option<&str>,
661) -> Result<(), String> {
662 if let Some(reason) = reason {
663 validate_websocket_close_reason(reason)?;
664 state.inbox.push_back(cancelled_outcome(
665 "websocket_close",
666 "completed",
667 Some(reason.to_string()),
668 None,
669 ));
670 }
671 state.closed = true;
672 Ok(())
673}
674
675pub fn reset_dispatcher_for_tests() {
677 DISPATCHER.with(|dispatcher| {
678 *dispatcher.borrow_mut() = WasmExportDispatcher::new();
679 });
680 #[cfg(target_arch = "wasm32")]
681 INFLIGHT_FETCHES.with(|fetches| {
682 fetches.borrow_mut().clear();
683 });
684 INFLIGHT_WEBSOCKETS.with(|sockets| {
685 sockets.borrow_mut().clear();
686 });
687}
688
689#[must_use]
691pub fn dispatcher_diagnostics_for_tests() -> WasmDispatcherDiagnostics {
692 DISPATCHER.with(|dispatcher| dispatcher.borrow().diagnostic_snapshot())
693}
694
695fn runtime_create_impl(consumer_version_json: Option<String>) -> Result<String, String> {
696 let consumer_version = parse_consumer_version(consumer_version_json)?;
697 let handle = with_dispatcher(|dispatcher| dispatcher.runtime_create(consumer_version))?;
698 encode_json(&handle, "runtime_create.response")
699}
700
701fn browser_operator_snapshot_impl() -> Result<String, String> {
702 cleanup_released_host_state();
703 let snapshot = DISPATCHER.with(|dispatcher| {
704 let diagnostics = dispatcher.borrow().diagnostic_snapshot();
705 BrowserOperatorConsoleSnapshot::from_dispatcher_diagnostics(&diagnostics)
706 });
707 encode_json(&snapshot, "browser_operator_snapshot.response")
708}
709
710fn runtime_close_impl(
711 handle_json: String,
712 consumer_version_json: Option<String>,
713) -> Result<String, String> {
714 let handle: WasmHandleRef = parse_json(&handle_json, "runtime_close.request")?;
715 let consumer_version = parse_consumer_version(consumer_version_json)?;
716 let outcome =
717 with_dispatcher(|dispatcher| dispatcher.runtime_close(&handle, consumer_version))?;
718 cleanup_released_host_state();
719 encode_json(&outcome, "runtime_close.response")
720}
721
722fn scope_enter_impl(
723 request_json: String,
724 consumer_version_json: Option<String>,
725) -> Result<String, String> {
726 let request: WasmScopeEnterRequest = parse_json(&request_json, "scope_enter.request")?;
727 let consumer_version = parse_consumer_version(consumer_version_json)?;
728 let handle = with_dispatcher(|dispatcher| dispatcher.scope_enter(&request, consumer_version))?;
729 encode_json(&handle, "scope_enter.response")
730}
731
732fn scope_close_impl(
733 handle_json: String,
734 consumer_version_json: Option<String>,
735) -> Result<String, String> {
736 let handle: WasmHandleRef = parse_json(&handle_json, "scope_close.request")?;
737 let consumer_version = parse_consumer_version(consumer_version_json)?;
738 let outcome = with_dispatcher(|dispatcher| dispatcher.scope_close(&handle, consumer_version))?;
739 cleanup_released_host_state();
740 encode_json(&outcome, "scope_close.response")
741}
742
743fn task_spawn_impl(
744 request_json: String,
745 consumer_version_json: Option<String>,
746) -> Result<String, String> {
747 let request: WasmTaskSpawnRequest = parse_json(&request_json, "task_spawn.request")?;
748 let consumer_version = parse_consumer_version(consumer_version_json)?;
749 let handle = with_dispatcher(|dispatcher| dispatcher.task_spawn(&request, consumer_version))?;
750 encode_json(&handle, "task_spawn.response")
751}
752
753fn task_join_impl(
754 handle_json: String,
755 outcome_json: String,
756 consumer_version_json: Option<String>,
757) -> Result<String, String> {
758 let handle: WasmHandleRef = parse_json(&handle_json, "task_join.request.handle")?;
759 let outcome: WasmAbiOutcomeEnvelope = parse_json(&outcome_json, "task_join.request.outcome")?;
760 let consumer_version = parse_consumer_version(consumer_version_json)?;
761 let joined =
762 with_dispatcher(|dispatcher| dispatcher.task_join(&handle, outcome, consumer_version))?;
763 encode_json(&joined, "task_join.response")
764}
765
766fn task_cancel_impl(
767 request_json: String,
768 consumer_version_json: Option<String>,
769) -> Result<String, String> {
770 let request: WasmTaskCancelRequest = parse_json(&request_json, "task_cancel.request")?;
771 let consumer_version = parse_consumer_version(consumer_version_json)?;
772 let outcome = with_dispatcher(|dispatcher| dispatcher.task_cancel(&request, consumer_version))?;
773
774 #[cfg(target_arch = "wasm32")]
775 if let Some(controller) = take_inflight_fetch(&request.task) {
776 controller.abort();
777 }
778
779 encode_json(&outcome, "task_cancel.response")
780}
781
782fn fetch_request_impl(
783 request_json: String,
784 consumer_version_json: Option<String>,
785) -> Result<String, String> {
786 let request: WasmFetchRequest = parse_json(&request_json, "fetch_request.request")?;
787 let request = normalize_fetch_request(request)?;
788 let consumer_version = parse_consumer_version(consumer_version_json)?;
789 let handle =
790 with_dispatcher(|dispatcher| dispatcher.fetch_request(&request, consumer_version))?;
791 #[cfg(target_arch = "wasm32")]
792 if let Err(setup_err) = spawn_browser_fetch(handle, request.clone()) {
793 let setup_outcome = fetch_error_outcome(
794 format!("failed to start browser fetch: {setup_err}"),
795 WasmAbiRecoverability::Permanent,
796 );
797 let _ =
798 with_dispatcher(|dispatcher| dispatcher.fetch_complete(&handle, setup_outcome.clone()));
799 return encode_json(&setup_outcome, "fetch_request.response");
800 }
801 encode_json(&fetch_pending_outcome(handle), "fetch_request.response")
802}
803
804fn websocket_open_impl(
805 request_json: String,
806 consumer_version_json: Option<String>,
807) -> Result<String, String> {
808 let request: BrowserWebSocketOpenRequest = parse_json(&request_json, "websocket_open.request")?;
809 let url = normalize_websocket_url(&request.url)?;
810 let request = BrowserWebSocketOpenRequest { url, ..request };
811 let consumer_version = parse_consumer_version(consumer_version_json)?;
812 let handle = spawn_websocket_handle(request.scope, consumer_version)?;
813 if let Err(setup_err) = setup_browser_websocket(handle, &request) {
814 let setup_outcome = fetch_error_outcome(
815 format!("failed to start browser websocket: {setup_err}"),
816 WasmAbiRecoverability::Permanent,
817 );
818 let _ = finalize_websocket_handle(&handle, setup_outcome.clone(), consumer_version);
819 return encode_json(&setup_outcome, "websocket_open.response");
820 }
821 encode_json(
822 &websocket_pending_outcome(handle),
823 "websocket_open.response",
824 )
825}
826
827fn websocket_send_impl(
828 request_json: String,
829 _consumer_version_json: Option<String>,
830) -> Result<String, String> {
831 let request: BrowserWebSocketSendRequest = parse_json(&request_json, "websocket_send.request")?;
832 send_browser_websocket_message(&request.socket, request.value)?;
833 encode_json(&websocket_send_outcome(), "websocket_send.response")
834}
835
836fn websocket_recv_impl(
837 request_json: String,
838 _consumer_version_json: Option<String>,
839) -> Result<String, String> {
840 let request: BrowserWebSocketRecvRequest = parse_json(&request_json, "websocket_recv.request")?;
841 let outcome = recv_browser_websocket_message(&request.socket)?;
842 encode_json(&outcome, "websocket_recv.response")
843}
844
845fn websocket_close_impl(
846 request_json: String,
847 consumer_version_json: Option<String>,
848) -> Result<String, String> {
849 let request: BrowserWebSocketCloseRequest =
850 parse_json(&request_json, "websocket_close.request")?;
851 let consumer_version = parse_consumer_version(consumer_version_json)?;
852 let close_reason = request.reason.clone();
853 let mut state = take_websocket_state(&request.socket)
854 .ok_or_else(|| format!("unknown websocket handle: {:?}", request.socket))?;
855 if let Err(err) = close_browser_websocket_socket(&mut state, close_reason.as_deref()) {
856 insert_websocket_state(request.socket, state);
857 return Err(err);
858 }
859 let outcome = if let Some(reason) = close_reason {
860 cancelled_outcome(
861 "websocket_close",
862 "completed",
863 Some(reason),
864 Some(format!("{:?}", request.socket)),
865 )
866 } else {
867 websocket_send_outcome()
868 };
869 let closed = finalize_websocket_handle(&request.socket, outcome, consumer_version)?;
870 encode_json(&closed, "websocket_close.response")
871}
872
873fn websocket_cancel_impl(
874 request_json: String,
875 consumer_version_json: Option<String>,
876) -> Result<String, String> {
877 let request: BrowserWebSocketCancelRequest =
878 parse_json(&request_json, "websocket_cancel.request")?;
879 let consumer_version = parse_consumer_version(consumer_version_json)?;
880 let cancel_message = request.message.clone();
881 let cancel = WasmTaskCancelRequest {
882 task: request.socket,
883 kind: request.kind.clone(),
884 message: cancel_message.clone(),
885 };
886 let _ = cancel_websocket_handle(&cancel, consumer_version)?;
887 if let Some(mut state) = take_websocket_state(&request.socket)
888 && let Err(err) = close_browser_websocket_socket(&mut state, cancel_message.as_deref())
889 {
890 insert_websocket_state(request.socket, state);
891 return Err(err);
892 }
893 let cancelled = cancelled_outcome(
894 &request.kind,
895 "cancelling",
896 request.message,
897 Some(format!("{:?}", request.socket)),
898 );
899 let joined = finalize_websocket_handle(&request.socket, cancelled, consumer_version)?;
900 encode_json(&joined, "websocket_cancel.response")
901}
902
903fn abi_version_impl() -> Result<String, String> {
904 let version = WasmAbiVersion {
905 major: WASM_ABI_MAJOR_VERSION,
906 minor: WASM_ABI_MINOR_VERSION,
907 };
908 encode_json(&version, "abi_version.response")
909}
910
911const fn abi_fingerprint_impl() -> u64 {
912 WASM_ABI_SIGNATURE_FINGERPRINT_V1
913}
914
915#[cfg(target_arch = "wasm32")]
916fn into_js_error(err: String) -> JsValue {
917 JsValue::from_str(&err)
918}