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