Skip to main content

asupersync_browser_core/
lib.rs

1#![deny(unsafe_code)]
2#![allow(clippy::missing_errors_doc)]
3// wasm-bindgen requires String at the JS boundary; impl functions mirror those signatures.
4#![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/// Reset helper for host-side deterministic tests.
656#[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/// Host-side diagnostics helper for export-boundary tests.
667#[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/// `runtime_create` ABI symbol.
884#[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/// Host adapter for `runtime_create`.
891#[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/// `runtime_close` ABI symbol.
897#[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/// Host adapter for `runtime_close`.
907#[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/// `scope_enter` ABI symbol.
916#[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/// Host adapter for `scope_enter`.
926#[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/// `scope_close` ABI symbol.
935#[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/// Host adapter for `scope_close`.
945#[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/// `task_spawn` ABI symbol.
954#[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/// Host adapter for `task_spawn`.
964#[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/// `task_join` ABI symbol.
973#[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/// Host adapter for `task_join`.
984#[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/// `task_cancel` ABI symbol.
994#[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/// Host adapter for `task_cancel`.
1004#[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/// `fetch_request` ABI symbol.
1013#[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/// Host adapter for `fetch_request`.
1023#[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/// `websocket_open` bridge symbol.
1032#[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/// Host adapter for `websocket_open`.
1042#[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/// `websocket_send` bridge symbol.
1051#[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/// Host adapter for `websocket_send`.
1061#[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/// `websocket_recv` bridge symbol.
1070#[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/// Host adapter for `websocket_recv`.
1080#[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/// `websocket_close` bridge symbol.
1089#[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/// Host adapter for `websocket_close`.
1099#[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/// `websocket_cancel` bridge symbol.
1108#[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/// Host adapter for `websocket_cancel`.
1118#[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/// `abi_version` ABI symbol.
1127#[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/// Host adapter for `abi_version`.
1134#[cfg(not(target_arch = "wasm32"))]
1135pub fn abi_version() -> Result<String, String> {
1136    abi_version_impl()
1137}
1138
1139/// `abi_fingerprint` ABI symbol.
1140#[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/// Host adapter for `abi_fingerprint`.
1148#[cfg(not(target_arch = "wasm32"))]
1149#[must_use]
1150pub const fn abi_fingerprint() -> u64 {
1151    abi_fingerprint_impl()
1152}