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