1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
use async_trait::async_trait;
use dioxus_core::ScopeState;
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use js_sys::Function;
use serde_json::Value;
use std::{cell::RefCell, rc::Rc, str::FromStr};
use wasm_bindgen::prelude::*;

/// Provides the WebEvalProvider through [`cx.provide_context`].
pub fn init_eval(cx: &ScopeState) {
    let provider: Rc<dyn EvalProvider> = Rc::new(WebEvalProvider {});
    cx.provide_context(provider);
}

/// Reprents the web-target's provider of evaluators.
pub struct WebEvalProvider;
impl EvalProvider for WebEvalProvider {
    fn new_evaluator(&self, js: String) -> Result<Rc<dyn Evaluator>, EvalError> {
        WebEvaluator::new(js).map(|eval| Rc::new(eval) as Rc<dyn Evaluator + 'static>)
    }
}

/// Required to avoid blocking the Rust WASM thread.
const PROMISE_WRAPPER: &str = r#"
    return new Promise(async (resolve, _reject) => {
        {JS_CODE}
        resolve(null);
    });
    "#;

/// Reprents a web-target's JavaScript evaluator.
pub struct WebEvaluator {
    dioxus: Dioxus,
    channel_receiver: async_channel::Receiver<serde_json::Value>,
    result: RefCell<Option<serde_json::Value>>,
}

impl WebEvaluator {
    /// Creates a new evaluator for web-based targets.
    pub fn new(js: String) -> Result<Self, EvalError> {
        let (channel_sender, channel_receiver) = async_channel::unbounded();

        // This Rc cloning mess hurts but it seems to work..
        let recv_value = Closure::<dyn FnMut(JsValue)>::new(move |data| {
            match serde_wasm_bindgen::from_value::<serde_json::Value>(data) {
                Ok(data) => _ = channel_sender.send_blocking(data),
                Err(e) => {
                    // Can't really do much here.
                    tracing::error!("failed to serialize JsValue to serde_json::Value (eval communication) - {}", e);
                }
            }
        });

        let dioxus = Dioxus::new(recv_value.as_ref().unchecked_ref());
        recv_value.forget();

        // Wrap the evaluated JS in a promise so that wasm can continue running (send/receive data from js)
        let code = PROMISE_WRAPPER.replace("{JS_CODE}", &js);

        let result = match Function::new_with_args("dioxus", &code).call1(&JsValue::NULL, &dioxus) {
            Ok(result) => {
                if let Ok(stringified) = js_sys::JSON::stringify(&result) {
                    if !stringified.is_undefined() && stringified.is_valid_utf16() {
                        let string: String = stringified.into();
                        Value::from_str(&string).map_err(|e| {
                            EvalError::Communication(format!("Failed to parse result - {}", e))
                        })?
                    } else {
                        return Err(EvalError::Communication(
                            "Failed to stringify result".into(),
                        ));
                    }
                } else {
                    return Err(EvalError::Communication(
                        "Failed to stringify result".into(),
                    ));
                }
            }
            Err(err) => {
                return Err(EvalError::InvalidJs(
                    err.as_string().unwrap_or("unknown".to_string()),
                ));
            }
        };

        Ok(Self {
            dioxus,
            channel_receiver,
            result: RefCell::new(Some(result)),
        })
    }
}

#[async_trait(?Send)]
impl Evaluator for WebEvaluator {
    /// Runs the evaluated JavaScript.
    async fn join(&self) -> Result<serde_json::Value, EvalError> {
        self.result.take().ok_or(EvalError::Finished)
    }

    /// Sends a message to the evaluated JavaScript.
    fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
        let data = match serde_wasm_bindgen::to_value::<serde_json::Value>(&data) {
            Ok(d) => d,
            Err(e) => return Err(EvalError::Communication(e.to_string())),
        };

        self.dioxus.rustSend(data);
        Ok(())
    }

    /// Gets an UnboundedReceiver to receive messages from the evaluated JavaScript.
    async fn recv(&self) -> Result<serde_json::Value, EvalError> {
        self.channel_receiver
            .recv()
            .await
            .map_err(|_| EvalError::Communication("failed to receive data from js".to_string()))
    }
}

#[wasm_bindgen(module = "/src/eval.js")]
extern "C" {
    pub type Dioxus;

    #[wasm_bindgen(constructor)]
    pub fn new(recv_callback: &Function) -> Dioxus;

    #[wasm_bindgen(method)]
    pub fn rustSend(this: &Dioxus, data: JsValue);
}