Skip to main content

wry_bindgen_runtime/
wry.rs

1//! Per-webview wry-bindgen integration for existing wry applications.
2//!
3//! A [`WryBindgen`] session is split into an app-thread runtime endpoint and a
4//! main-thread driver. Create one session for each webview/JavaScript
5//! realm.
6
7use alloc::boxed::Box;
8use alloc::rc::Rc;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11use base64::Engine;
12use core::cell::RefCell;
13use core::future::poll_fn;
14use core::pin::Pin;
15use core::task::Poll;
16
17use http::Response;
18
19use crate::batch::{Runtime, in_runtime};
20use crate::function_registry::FUNCTION_REGISTRY;
21use crate::ipc::{IPCMessage, decode_data};
22use crate::runtime::{
23    DriverCommand, DriverCommandReceiver, DriverCommandSender, DriverCommandWeakSender, IPCSenders,
24    Inbound, InboundSendError, WryIPC, dispatch_inbound_message,
25};
26
27struct WryBindgenResponder {
28    respond: Box<dyn FnOnce(Response<Vec<u8>>)>,
29}
30
31impl<F> From<F> for WryBindgenResponder
32where
33    F: FnOnce(Response<Vec<u8>>) + 'static,
34{
35    fn from(respond: F) -> Self {
36        Self {
37            respond: Box::new(respond),
38        }
39    }
40}
41
42impl WryBindgenResponder {
43    fn respond(self, response: Response<Vec<u8>>) {
44        (self.respond)(response);
45    }
46
47    fn respond_ipc(self, response: IPCMessage) {
48        let body = response.data();
49        // Encode as base64 - sync XMLHttpRequest cannot use responseType="arraybuffer"
50        let engine = base64::engine::general_purpose::STANDARD;
51        let body_base64 = engine.encode(body);
52        self.respond(
53            http::Response::builder()
54                .status(200)
55                .header("Content-Type", "text/plain")
56                .body(body_base64.into_bytes())
57                .expect("Failed to build response"),
58        );
59    }
60}
61
62/// Decode request data from the dioxus-data header.
63fn decode_request_data(request: &http::Request<Vec<u8>>) -> Option<IPCMessage> {
64    if let Some(header_value) = request.headers().get("dioxus-data") {
65        return decode_data(header_value.as_bytes());
66    }
67    None
68}
69
70/// Tracks the loading state of the webview.
71enum WebviewLoadingState {
72    /// Webview is still loading. The lock protocol permits at most one Rust IPC
73    /// message to be waiting for load, though normally this remains empty
74    /// because user code cannot run until the lock can be acquired.
75    Pending {
76        pending_ipc: Option<IPCMessage>,
77        acquire_lock: bool,
78    },
79    /// Webview is loaded and ready.
80    Loaded,
81}
82
83impl Default for WebviewLoadingState {
84    fn default() -> Self {
85        WebviewLoadingState::Pending {
86            pending_ipc: None,
87            acquire_lock: false,
88        }
89    }
90}
91
92/// Shared state for one webview instance.
93struct WebviewState {
94    /// Protocol message routing for this webview.
95    messages: WebviewMessageLayer,
96    // The state of the webview. Either loading (with queued messages) or loaded.
97    loading_state: WebviewLoadingState,
98}
99
100/// Transport-owned IPC routing state for one webview.
101///
102/// Under strict synchronous ping-pong:
103///
104/// - At most one JS XHR is suspended at any moment (JS blocks on each XHR
105///   before it can send the next one), so the responder lives in a single
106///   `current_xhr` slot.
107/// - Rust->JS calls are only delivered through that suspended XHR, and JS
108///   replies by parking the next XHR on the same response path.
109struct WebviewMessageLayer {
110    current_xhr: Option<WryBindgenResponder>,
111    /// The sender used to forward decoded IPC messages to the Rust runtime.
112    sender: IPCSenders,
113}
114
115impl WebviewState {
116    /// Create a new webview state.
117    fn new(sender: IPCSenders) -> Self {
118        Self {
119            messages: WebviewMessageLayer::new(sender),
120            loading_state: WebviewLoadingState::default(),
121        }
122    }
123
124    fn handle_driver_command(&mut self, command: DriverCommand) -> DriverAction {
125        match command {
126            DriverCommand::AcquireLock => self.handle_acquire_lock(),
127            DriverCommand::SendIpc(ipc_msg) => {
128                self.handle_ipc_message(ipc_msg);
129                DriverAction::None
130            }
131            DriverCommand::ReleaseLock => {
132                self.messages.release_lock();
133                DriverAction::None
134            }
135        }
136    }
137
138    fn handle_ipc_message(&mut self, ipc_msg: IPCMessage) {
139        if let WebviewLoadingState::Pending { pending_ipc, .. } = &mut self.loading_state {
140            assert!(
141                pending_ipc.replace(ipc_msg).is_none(),
142                "multiple Rust IPC messages queued before webview load"
143            );
144            return;
145        }
146
147        self.messages.receive_rust_message(ipc_msg);
148    }
149
150    fn handle_acquire_lock(&mut self) -> DriverAction {
151        if let WebviewLoadingState::Pending { acquire_lock, .. } = &mut self.loading_state {
152            *acquire_lock = true;
153            return DriverAction::None;
154        }
155
156        DriverAction::RequestJsLock
157    }
158
159    fn mark_loaded(&mut self) -> bool {
160        if let WebviewLoadingState::Pending {
161            pending_ipc,
162            acquire_lock,
163        } = std::mem::replace(&mut self.loading_state, WebviewLoadingState::Loaded)
164        {
165            if let Some(msg) = pending_ipc {
166                self.messages.receive_rust_message(msg);
167            }
168            return acquire_lock;
169        }
170
171        false
172    }
173
174    fn mark_initialized(&mut self) -> bool {
175        match self.loading_state {
176            WebviewLoadingState::Pending { .. } => self.mark_loaded(),
177            WebviewLoadingState::Loaded => true,
178        }
179    }
180}
181
182enum DriverAction {
183    None,
184    RequestJsLock,
185}
186
187impl DriverAction {
188    fn run(self, evaluate_script: &mut impl FnMut(&str)) {
189        match self {
190            DriverAction::None => {}
191            DriverAction::RequestJsLock => {
192                evaluate_script("window.__wry_acquire_handler_lock()");
193            }
194        }
195    }
196}
197
198impl WebviewMessageLayer {
199    fn new(sender: IPCSenders) -> Self {
200        Self {
201            current_xhr: None,
202            sender,
203        }
204    }
205
206    fn receive_js_message(&mut self, msg: IPCMessage, responder: WryBindgenResponder) {
207        self.park_and_forward(responder, Inbound::Message(msg));
208    }
209
210    fn receive_lock_request(&mut self, responder: WryBindgenResponder) {
211        self.park_and_forward(responder, Inbound::LockReady);
212    }
213
214    fn park_and_forward(&mut self, responder: WryBindgenResponder, inbound: Inbound) {
215        assert!(
216            self.current_xhr.is_none(),
217            "JS parked a new XHR while another JS XHR is waiting for Rust"
218        );
219        self.current_xhr = Some(responder);
220        match self.sender.send(inbound) {
221            Ok(()) => {}
222            Err(InboundSendError::Closed) => {
223                let responder = self.take_parked_xhr();
224                responder.respond(error_response());
225            }
226            Err(InboundSendError::Occupied) => {
227                panic!("inbound IPC slot occupied while parking a JS XHR")
228            }
229        }
230    }
231
232    fn receive_rust_message(&mut self, ipc_msg: IPCMessage) {
233        // Deliver as the response to the parked JS XHR. This is the only
234        // Rust->JS payload path; `evaluate_script` is reserved for asking JS to
235        // acquire this lock.
236        let responder = self.take_parked_xhr();
237        responder.respond_ipc(ipc_msg);
238    }
239
240    fn release_lock(&mut self) {
241        let responder = self.take_parked_xhr();
242        responder.respond(blank_response());
243    }
244
245    /// Take the JS XHR currently parked in this layer. Every caller runs only
246    /// while Rust holds the lock, so an XHR is always suspended here.
247    fn take_parked_xhr(&mut self) -> WryBindgenResponder {
248        self.current_xhr.take().unwrap()
249    }
250}
251
252/// Protocol handler for one webview session.
253///
254/// This struct is not `Send` because it holds main-thread webview state.
255pub struct ProtocolHandler {
256    webview: Rc<RefCell<WebviewState>>,
257    driver_commands: DriverCommandWeakSender,
258}
259
260impl ProtocolHandler {
261    /// Create a protocol handler closure suitable for `WebViewBuilder::with_asynchronous_custom_protocol`.
262    ///
263    /// The returned closure handles this subset of "{protocol}://" requests:
264    /// - "/__wbg__/preinitialized" - enables startup calls before the app lock
265    /// - "/__wbg__/initialized" - signals webview loaded
266    /// - "/__wbg__/snippets/{path}" - serves inline JS modules
267    /// - "/__wbg__/init.js" - serves the initialization script
268    /// - "/__wbg__/handler" - main IPC endpoint
269    ///
270    /// # Arguments
271    /// * `protocol` - The protocol scheme (e.g., "wry")
272    pub fn handle_request<R>(
273        &self,
274        protocol: &str,
275        request: &http::Request<Vec<u8>>,
276        responder: R,
277    ) -> Option<R>
278    where
279        R: FnOnce(Response<Vec<u8>>) + 'static,
280    {
281        let webviews = &self.webview;
282
283        let protocol_prefix = format!("{protocol}://index.html");
284        let android_prefix = format!("https://{protocol}.index.html");
285        let windows_prefix = format!("http://{protocol}.index.html");
286
287        let uri = request.uri().to_string();
288        let real_path = uri
289            .strip_prefix(&protocol_prefix)
290            .or_else(|| uri.strip_prefix(&windows_prefix))
291            .or_else(|| uri.strip_prefix(&android_prefix))
292            .unwrap_or(&uri);
293        let real_path = real_path.trim_matches('/');
294
295        let Some(path_without_wbg) = real_path.strip_prefix("__wbg__/") else {
296            // Not a wry-bindgen request - let the caller handle it
297            return Some(responder);
298        };
299
300        // Serve inline_js modules from __wbg__/snippets/
301        if let Some(path_without_snippets) = path_without_wbg.strip_prefix("snippets/") {
302            let responder = WryBindgenResponder::from(responder);
303            // Inventory-collected modules first, then any registered at runtime by
304            // `link_to!` (whose content is only known once the macro call runs).
305            if let Some(content) = FUNCTION_REGISTRY
306                .get_module(path_without_snippets)
307                .or_else(|| crate::function_registry::linked_module(path_without_snippets))
308            {
309                responder.respond(module_response(content));
310                return None;
311            }
312            responder.respond(not_found_response());
313            return None;
314        }
315
316        if path_without_wbg == "init.js" {
317            let responder = WryBindgenResponder::from(responder);
318            responder.respond(module_response(&init_script()));
319            return None;
320        }
321
322        if path_without_wbg == "preinitialized" {
323            let _ = webviews.borrow_mut().mark_loaded();
324            let responder = WryBindgenResponder::from(responder);
325            responder.respond(blank_response());
326            return None;
327        }
328
329        if path_without_wbg == "initialized" {
330            let acquire_lock = webviews.borrow_mut().mark_initialized();
331            if acquire_lock {
332                self.driver_commands.send(DriverCommand::AcquireLock);
333            }
334            let responder = WryBindgenResponder::from(responder);
335            responder.respond(blank_response());
336            return None;
337        }
338
339        // Js sent us either an Evaluate or Respond message
340        if path_without_wbg == "handler" {
341            let responder = WryBindgenResponder::from(responder);
342            let mut webview_state = webviews.borrow_mut();
343            if request.headers().get("wry-bindgen-lock").is_some() {
344                webview_state.messages.receive_lock_request(responder);
345                return None;
346            }
347            let Some(msg) = decode_request_data(request) else {
348                responder.respond(error_response());
349                return None;
350            };
351            webview_state.messages.receive_js_message(msg, responder);
352            return None;
353        }
354
355        Some(responder)
356    }
357}
358
359/// Get the initialization script that must be evaluated in the webview.
360///
361/// This script sets up the JavaScript function registry and IPC infrastructure.
362fn init_script() -> String {
363    /// The script you need to include in the initialization of your webview.
364    const INITIALIZATION_SCRIPT: &str = include_str!("./js/main.js");
365    let collect_functions = FUNCTION_REGISTRY.script();
366    format!("{INITIALIZATION_SCRIPT}\n{collect_functions}")
367}
368
369/// Per-webview wry-bindgen session.
370///
371/// Each session owns one JavaScript realm. Split it into a runtime endpoint for
372/// the app thread and a driver that must be polled on the webview thread.
373pub struct WryBindgen {
374    webview: Rc<RefCell<WebviewState>>,
375    ipc: WryIPC,
376    driver_commands: DriverCommandReceiver,
377    weak_driver_commands: DriverCommandWeakSender,
378}
379
380impl WryBindgen {
381    /// Create a new per-webview session.
382    pub fn new() -> Self {
383        let (ipc, senders, driver_commands) = WryIPC::new();
384        let weak_driver_commands = ipc.command_sender().downgrade();
385        Self {
386            webview: Rc::new(RefCell::new(WebviewState::new(senders))),
387            ipc,
388            driver_commands,
389            weak_driver_commands,
390        }
391    }
392
393    /// Get the protocol handler for this webview.
394    pub fn protocol_handler(&self) -> ProtocolHandler {
395        ProtocolHandler {
396            webview: self.webview.clone(),
397            driver_commands: self.weak_driver_commands.clone(),
398        }
399    }
400
401    /// Split the session into an app-thread runtime endpoint and a main-thread driver.
402    pub fn split(self) -> (WryBindgenRuntime, WryBindgenDriver) {
403        (
404            WryBindgenRuntime { ipc: self.ipc },
405            WryBindgenDriver {
406                webview: self.webview,
407                commands: self.driver_commands,
408            },
409        )
410    }
411}
412
413impl Default for WryBindgen {
414    fn default() -> Self {
415        Self::new()
416    }
417}
418
419/// RAII guard for a held JS lock.
420///
421/// Holding the guard means a JS XHR is parked, suspending the JS event loop so
422/// Rust can drive JS. Dropping it replies to that XHR, handing control back to
423/// the JS event loop until the next wake. Tying release to the guard's scope
424/// keeps acquire and release paired.
425struct JsLockGuard {
426    commands: DriverCommandSender,
427}
428
429impl JsLockGuard {
430    fn acquire(ipc: &WryIPC) -> Self {
431        Self {
432            commands: ipc.command_sender(),
433        }
434    }
435}
436
437impl Drop for JsLockGuard {
438    fn drop(&mut self) {
439        self.commands.send(DriverCommand::ReleaseLock);
440    }
441}
442
443/// Runtime endpoint moved to the app thread.
444pub struct WryBindgenRuntime {
445    ipc: WryIPC,
446}
447
448impl WryBindgenRuntime {
449    /// Build a sendable wrapper that creates and runs the app future on the
450    /// thread where it is awaited.
451    pub fn run<F, Fut>(
452        self,
453        app: F,
454    ) -> impl IntoFuture<Output = (), IntoFuture: 'static> + Send + 'static
455    where
456        F: FnOnce() -> Fut + Send + 'static,
457        Fut: core::future::Future<Output = ()> + 'static,
458    {
459        struct RuntimeFuture<F, Fut> {
460            app: F,
461            ipc: WryIPC,
462            phantom: core::marker::PhantomData<fn(Fut)>,
463        }
464
465        impl<F, Fut> RuntimeFuture<F, Fut> {
466            fn new(app: F, ipc: WryIPC) -> Self {
467                Self {
468                    app,
469                    ipc,
470                    phantom: core::marker::PhantomData,
471                }
472            }
473        }
474
475        impl<F, Fut> IntoFuture for RuntimeFuture<F, Fut>
476        where
477            F: FnOnce() -> Fut + Send + 'static,
478            Fut: core::future::Future<Output = ()> + 'static,
479        {
480            type IntoFuture = Pin<Box<dyn core::future::Future<Output = ()>>>;
481            type Output = ();
482
483            fn into_future(self) -> Self::IntoFuture {
484                let Self { app, ipc, .. } = self;
485                let mut runtime = Some(Runtime::new(ipc));
486                let mut app = Some(app);
487                let mut run_app = None::<Pin<Box<Fut>>>;
488
489                // The runtime drives the JS event loop by parking a synchronous XHR
490                // (the "lock"). On each wake we drain inbound items from the shared
491                // channel; `just_polled_app` distinguishes "the app future just
492                // parked itself, stay idle" from "a wake means the app future wants
493                // to run, so ask JS to park an XHR for the next poll".
494                let poll_driver = poll_fn(move |ctx| {
495                    let mut just_polled_app = false;
496                    loop {
497                        let Some(rt) = runtime.as_ref() else {
498                            return Poll::Ready(());
499                        };
500                        match rt.ipc().poll_recv(ctx) {
501                            // An idle JS→Rust callback. It replies through its own
502                            // parked XHR, so we just dispatch it; the app future may
503                            // now want polling, which the next Pending below requests.
504                            Poll::Ready(Some(Inbound::Message(msg))) => {
505                                let owned = runtime.take().expect("runtime available");
506                                let (owned, _) =
507                                    in_runtime(owned, || dispatch_inbound_message(&msg));
508                                runtime = Some(owned);
509                                just_polled_app = false;
510                            }
511                            // A parked XHR is available: poll the app future while the
512                            // guard holds the lock, releasing it when the poll returns.
513                            Poll::Ready(Some(Inbound::LockReady)) => {
514                                let _guard = JsLockGuard::acquire(rt.ipc());
515                                if run_app.is_none() {
516                                    run_app = Some(Box::pin(app
517                                        .take()
518                                        .expect("app constructor called once")(
519                                    )));
520                                }
521                                let owned = runtime.take().expect("runtime available");
522                                let (owned, poll_result) = in_runtime(owned, || {
523                                    run_app
524                                        .as_mut()
525                                        .expect("app future must exist")
526                                        .as_mut()
527                                        .poll(ctx)
528                                });
529                                runtime = Some(owned);
530                                if poll_result.is_ready() {
531                                    return Poll::Ready(());
532                                }
533                                just_polled_app = true;
534                            }
535                            Poll::Ready(None) => return Poll::Ready(()),
536                            Poll::Pending => {
537                                // Woken with no parked XHR. Unless we just polled the
538                                // app future (it registered its own waker and is idle),
539                                // this wake means it wants to run: ask JS to park an XHR.
540                                if !just_polled_app {
541                                    rt.ipc().send_acquire_lock();
542                                }
543                                return Poll::Pending;
544                            }
545                        }
546                    }
547                });
548
549                Box::pin(poll_driver)
550            }
551        }
552
553        RuntimeFuture::new(app, self.ipc)
554    }
555}
556
557/// Main-thread driver for one webview session.
558pub struct WryBindgenDriver {
559    webview: Rc<RefCell<WebviewState>>,
560    commands: DriverCommandReceiver,
561}
562
563impl WryBindgenDriver {
564    /// Attach the driver to the webview's script evaluator.
565    ///
566    /// The evaluator is only used to ask JavaScript to acquire the synchronous
567    /// handler lock before polling the async runtime.
568    pub fn with_evaluate_script(
569        self,
570        evaluate_script: impl FnMut(&str) + 'static,
571    ) -> WryBindgenWebviewDriver {
572        WryBindgenWebviewDriver {
573            driver: self,
574            evaluate_script: Box::new(evaluate_script),
575        }
576    }
577}
578
579/// Main-thread driver bound to the webview's script evaluator.
580pub struct WryBindgenWebviewDriver {
581    driver: WryBindgenDriver,
582    evaluate_script: Box<dyn FnMut(&str)>,
583}
584
585impl WryBindgenWebviewDriver {
586    /// Poll the main-thread driver and evaluate scripts only when acquiring the
587    /// JS lock for an async runtime poll.
588    pub fn poll(&mut self, cx: &mut core::task::Context<'_>) -> Poll<()> {
589        loop {
590            match self.driver.commands.poll_recv(cx) {
591                Poll::Ready(Some(command)) => {
592                    let action = self
593                        .driver
594                        .webview
595                        .borrow_mut()
596                        .handle_driver_command(command);
597                    action.run(&mut self.evaluate_script);
598                }
599                Poll::Ready(None) => return Poll::Ready(()),
600                Poll::Pending => return Poll::Pending,
601            }
602        }
603    }
604}
605
606/// Create a blank HTTP response.
607fn blank_response() -> http::Response<Vec<u8>> {
608    http::Response::builder()
609        .status(200)
610        .body(vec![])
611        .expect("Failed to build blank response")
612}
613
614/// Create an error HTTP response.
615fn error_response() -> http::Response<Vec<u8>> {
616    http::Response::builder()
617        .status(400)
618        .body(vec![])
619        .expect("Failed to build error response")
620}
621
622/// Create a JavaScript module HTTP response.
623fn module_response(content: &str) -> http::Response<Vec<u8>> {
624    http::Response::builder()
625        .status(200)
626        .header("Content-Type", "application/javascript")
627        .header("access-control-allow-origin", "*")
628        .body(content.as_bytes().to_vec())
629        .expect("Failed to build module response")
630}
631
632/// Create a not found HTTP response.
633fn not_found_response() -> http::Response<Vec<u8>> {
634    http::Response::builder()
635        .status(404)
636        .body(b"Not Found".to_vec())
637        .expect("Failed to build not found response")
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::ipc::{DecodedVariant, MessageType};
644    use std::sync::Arc;
645
646    fn ipc_message(message_type: MessageType) -> IPCMessage {
647        crate::ipc::empty_message(message_type)
648    }
649
650    fn handler_request(message_type: MessageType) -> http::Request<Vec<u8>> {
651        let engine = base64::engine::general_purpose::STANDARD;
652        let body_base64 = engine.encode(ipc_message(message_type).data());
653
654        http::Request::builder()
655            .uri("wry://index.html/__wbg__/handler")
656            .header("dioxus-data", body_base64)
657            .body(Vec::new())
658            .expect("failed to build request")
659    }
660
661    fn lock_request() -> http::Request<Vec<u8>> {
662        http::Request::builder()
663            .uri("wry://index.html/__wbg__/handler")
664            .header("wry-bindgen-lock", "1")
665            .body(Vec::new())
666            .expect("failed to build request")
667    }
668
669    fn initialized_request() -> http::Request<Vec<u8>> {
670        http::Request::builder()
671            .uri("wry://index.html/__wbg__/initialized")
672            .body(Vec::new())
673            .expect("failed to build request")
674    }
675
676    struct NoopWake;
677
678    impl std::task::Wake for NoopWake {
679        fn wake(self: Arc<Self>) {}
680    }
681
682    fn poll_forwarded_message(ipc: &WryIPC) -> IPCMessage {
683        let waker = std::task::Waker::from(Arc::new(NoopWake));
684        let mut cx = std::task::Context::from_waker(&waker);
685        match ipc.poll_recv(&mut cx) {
686            Poll::Ready(Some(Inbound::Message(msg))) => msg,
687            other => panic!("expected forwarded IPC message, got {other:?}"),
688        }
689    }
690
691    fn poll_driver(driver: &mut WryBindgenWebviewDriver) -> Poll<()> {
692        let waker = std::task::Waker::from(Arc::new(NoopWake));
693        let mut cx = std::task::Context::from_waker(&waker);
694        driver.poll(&mut cx)
695    }
696
697    #[test]
698    fn js_respond_is_forwarded_and_parks_xhr() {
699        let (ipc, sender, _driver_commands) = WryIPC::new();
700        let mut layer = WebviewMessageLayer::new(sender);
701        let responder_called = Rc::new(RefCell::new(false));
702        let captured_responder_called = responder_called.clone();
703
704        layer.receive_js_message(
705            ipc_message(MessageType::Respond),
706            WryBindgenResponder::from(move |_| {
707                *captured_responder_called.borrow_mut() = true;
708            }),
709        );
710
711        assert!(layer.current_xhr.is_some());
712        assert!(
713            !*responder_called.borrow(),
714            "JS response XHR should stay parked for Rust's next reply"
715        );
716        let received = poll_forwarded_message(&ipc);
717        assert!(matches!(
718            received.decoded().unwrap(),
719            DecodedVariant::Respond { .. }
720        ));
721    }
722
723    #[test]
724    fn js_message_while_xhr_is_parked_panics() {
725        let (_ipc, sender, _driver_commands) = WryIPC::new();
726        let mut layer = WebviewMessageLayer::new(sender);
727        layer.current_xhr = Some(WryBindgenResponder::from(|_| {}));
728
729        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
730            layer.receive_js_message(
731                ipc_message(MessageType::Evaluate),
732                WryBindgenResponder::from(|_| {}),
733            );
734        }));
735
736        assert!(result.is_err());
737    }
738
739    #[test]
740    fn lock_request_while_xhr_is_parked_panics() {
741        let (_ipc, sender, _driver_commands) = WryIPC::new();
742        let mut layer = WebviewMessageLayer::new(sender);
743        layer.current_xhr = Some(WryBindgenResponder::from(|_| {}));
744
745        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
746            layer.receive_lock_request(WryBindgenResponder::from(|_| {}));
747        }));
748
749        assert!(result.is_err());
750    }
751
752    #[test]
753    fn rust_outbound_messages_use_same_parked_xhr_response_path() {
754        for message_type in [MessageType::Evaluate, MessageType::Respond] {
755            let (_ipc, sender, _driver_commands) = WryIPC::new();
756            let mut layer = WebviewMessageLayer::new(sender);
757            let response = Rc::new(RefCell::new(None));
758            let captured_response = response.clone();
759            let message = ipc_message(message_type);
760            let expected_body = message.data().to_vec();
761
762            layer.current_xhr = Some(WryBindgenResponder::from(move |response| {
763                *captured_response.borrow_mut() = Some(response);
764            }));
765            layer.receive_rust_message(message);
766
767            assert!(layer.current_xhr.is_none());
768            let response = response
769                .borrow_mut()
770                .take()
771                .expect("parked XHR should receive Rust IPC");
772            assert_eq!(response.status(), http::StatusCode::OK);
773            let engine = base64::engine::general_purpose::STANDARD;
774            let body = engine
775                .decode(response.body())
776                .expect("response body should be base64 IPC bytes");
777            assert_eq!(body, expected_body);
778        }
779    }
780
781    #[test]
782    fn handler_responds_error_when_evaluate_arrives_after_runtime_drop() {
783        let bindgen = WryBindgen::new();
784        let protocol_handler = bindgen.protocol_handler();
785        drop(bindgen);
786
787        let response = Rc::new(RefCell::new(None));
788        let captured_response = response.clone();
789        let request = handler_request(MessageType::Evaluate);
790
791        let unhandled = protocol_handler.handle_request("wry", &request, move |response| {
792            *captured_response.borrow_mut() = Some(response)
793        });
794
795        assert!(unhandled.is_none());
796        let response = response
797            .borrow_mut()
798            .take()
799            .expect("closed runtime should receive an error response");
800        assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
801    }
802
803    #[test]
804    fn lock_request_is_queued_until_webview_loads() {
805        let bindgen = WryBindgen::new();
806        let protocol_handler = bindgen.protocol_handler();
807
808        let evaluated_scripts = Rc::new(RefCell::new(Vec::new()));
809        let captured_scripts = evaluated_scripts.clone();
810        let (runtime, driver) = bindgen.split();
811        let mut driver = driver.with_evaluate_script(move |script| {
812            captured_scripts.borrow_mut().push(script.to_string());
813        });
814
815        runtime.ipc.send_acquire_lock();
816        assert!(matches!(poll_driver(&mut driver), Poll::Pending));
817        assert!(evaluated_scripts.borrow().is_empty());
818
819        let response = Rc::new(RefCell::new(None));
820        let captured_response = response.clone();
821        let request = initialized_request();
822        let unhandled = protocol_handler.handle_request("wry", &request, move |response| {
823            *captured_response.borrow_mut() = Some(response)
824        });
825
826        assert!(unhandled.is_none());
827        assert_eq!(
828            response.borrow().as_ref().unwrap().status(),
829            http::StatusCode::OK
830        );
831        assert!(matches!(poll_driver(&mut driver), Poll::Pending));
832        assert_eq!(
833            evaluated_scripts.borrow().as_slice(),
834            ["window.__wry_acquire_handler_lock()"]
835        );
836    }
837
838    #[test]
839    fn lock_request_while_js_xhr_is_parked_is_not_dropped_or_duplicated() {
840        let bindgen = WryBindgen::new();
841        let protocol_handler = bindgen.protocol_handler();
842
843        let evaluated_scripts = Rc::new(RefCell::new(Vec::new()));
844        let captured_scripts = evaluated_scripts.clone();
845        let (runtime, driver) = bindgen.split();
846        let mut driver = driver.with_evaluate_script(move |script| {
847            captured_scripts.borrow_mut().push(script.to_string());
848        });
849
850        let request = initialized_request();
851        let unhandled = protocol_handler.handle_request("wry", &request, |_| {});
852        assert!(unhandled.is_none());
853
854        let response = Rc::new(RefCell::new(None));
855        let captured_response = response.clone();
856        let request = handler_request(MessageType::Evaluate);
857
858        let unhandled = protocol_handler.handle_request("wry", &request, move |response| {
859            *captured_response.borrow_mut() = Some(response)
860        });
861
862        assert!(unhandled.is_none());
863        assert!(
864            response.borrow().is_none(),
865            "JS callback XHR should stay parked while Rust handles it"
866        );
867
868        runtime.ipc.send_acquire_lock();
869        assert!(matches!(poll_driver(&mut driver), Poll::Pending));
870        assert_eq!(
871            evaluated_scripts.borrow().as_slice(),
872            ["window.__wry_acquire_handler_lock()"],
873            "lock script should be requested while the parked XHR is outstanding"
874        );
875
876        runtime.ipc.send_ipc(ipc_message(MessageType::Respond));
877        assert!(matches!(poll_driver(&mut driver), Poll::Pending));
878
879        let response = response
880            .borrow_mut()
881            .take()
882            .expect("parked JS callback XHR should receive Rust's response");
883        assert_eq!(response.status(), http::StatusCode::OK);
884        assert_eq!(
885            evaluated_scripts.borrow().as_slice(),
886            ["window.__wry_acquire_handler_lock()"],
887            "answering the parked XHR should not duplicate the in-flight lock request"
888        );
889    }
890
891    #[test]
892    fn handler_responds_error_when_lock_arrives_after_runtime_drop() {
893        let bindgen = WryBindgen::new();
894        let protocol_handler = bindgen.protocol_handler();
895        drop(bindgen);
896
897        let response = Rc::new(RefCell::new(None));
898        let captured_response = response.clone();
899        let request = lock_request();
900
901        let unhandled = protocol_handler.handle_request("wry", &request, move |response| {
902            *captured_response.borrow_mut() = Some(response)
903        });
904
905        assert!(unhandled.is_none());
906        let response = response
907            .borrow_mut()
908            .take()
909            .expect("closed runtime should receive an error response");
910        assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
911    }
912}