Convert use_eval to use send/recv system (#1080)

* progress: reworked

don't run this, it'll kill your web browser

* feat: use_eval but with comms

* revision: async recv & recv_sync

* revision: use_eval

* revision: standard eval interface

* revision: use serde_json::Value instead of JsValue

* revision: docs

* revision: error message

* create: desktop eval (wip)

* fix: desktop eval

* revision: wrap use_eval in Rc<RefCell<_>>

* fix: fmt, clippy

* fix: desktop tests

* revision: change to channel system

- fixes clippy errors
- fixes playwright tests

* fix: tests

* fix: eval example

* fix: fmt

* fix: tests, desktop stuff

* fix: tests

* feat: drop handler

* fix: tests

* fix: rustfmt

* revision: web promise/callback system

* fix: recv error

* revision: IntoFuture, functionation

* fix: ci

* revision: playwright web

* remove: unescessary code

* remove dioxus-html from public examples

* prototype-patch

* fix web eval

* fix: rustfmt

* fix: CI

* make use_eval more efficient

* implement eval for liveview as well

* fix playwright tests

* fix clippy

* more clippy fixes

* fix clippy

* fix stack overflow

* fix desktop mock

* fix clippy

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
This commit is contained in:
Miles MurgawandEvan Almloff authored and GitHub committed 2023-07-21 17:36:25 -05:00
1 parent b36a7a3993
commit 6210c6fefe
31 files changed
+1025 -410

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
export class Dioxus {
constructor(sendCallback, returnCallback) {
this.sendCallback = sendCallback;
this.returnCallback = returnCallback;
this.promiseResolve = null;
this.received = [];
}
// Receive message from Rust
recv() {
return new Promise((resolve, _reject) => {
// If data already exists, resolve immediately
let data = this.received.shift();
if (data) {
resolve(data);
return;
}
// Otherwise set a resolve callback
this.promiseResolve = resolve;
});
}
// Send message to rust.
send(data) {
this.sendCallback(data);
}
// Internal rust send
rustSend(data) {
// If a promise is waiting for data, resolve it, and clear the resolve callback
if (this.promiseResolve) {
this.promiseResolve(data);
this.promiseResolve = null;
return;
}
// Otherwise add the data to a queue
this.received.push(data);
}
}
+130
View File
@@ -0,0 +1,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.
log::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);
}
+5 -2
View File
@@ -54,18 +54,17 @@
// - Do DOM work in the next requestAnimationFrame callback
pub use crate::cfg::Config;
pub use crate::util::{use_eval, EvalResult};
use dioxus_core::{Element, Scope, VirtualDom};
use futures_util::{pin_mut, FutureExt, StreamExt};
mod cache;
mod cfg;
mod dom;
mod eval;
mod file_engine;
mod hot_reload;
#[cfg(feature = "hydrate")]
mod rehydrate;
mod util;
// Currently disabled since it actually slows down immediate rendering
// todo: only schedule non-immediate renders through ric/raf
@@ -168,6 +167,10 @@ pub async fn run_with_props<T: 'static>(root: fn(Scope<T>) -> Element, root_prop
let mut dom = VirtualDom::new_with_props(root, root_props);
// Eval
let cx = dom.base_scope();
eval::init_eval(cx);
#[cfg(feature = "panic_hook")]
if cfg.default_panic_hook {
console_error_panic_hook::set_once();
-70
View File
@@ -1,70 +0,0 @@
//! Utilities specific to websys
use std::{
future::{IntoFuture, Ready},
rc::Rc,
str::FromStr,
};
use dioxus_core::*;
use serde::de::Error;
use serde_json::Value;
/// Get a closure that executes any JavaScript in the webpage.
///
/// # Safety
///
/// Please be very careful with this function. A script with too many dynamic
/// parts is practically asking for a hacker to find an XSS vulnerability in
/// it. **This applies especially to web targets, where the JavaScript context
/// has access to most, if not all of your application data.**
///
/// # Panics
///
/// The closure will panic if the provided script is not valid JavaScript code
/// or if it returns an uncaught error.
pub fn use_eval(cx: &ScopeState) -> &Rc<dyn Fn(String) -> EvalResult> {
cx.use_hook(|| {
Rc::new(|script: String| EvalResult {
value: if let Ok(value) =
js_sys::Function::new_no_args(&script).call0(&wasm_bindgen::JsValue::NULL)
{
if let Ok(stringified) = js_sys::JSON::stringify(&value) {
if !stringified.is_undefined() && stringified.is_valid_utf16() {
let string: String = stringified.into();
Value::from_str(&string)
} else {
Err(serde_json::Error::custom("Failed to stringify result"))
}
} else {
Err(serde_json::Error::custom("Failed to stringify result"))
}
} else {
Err(serde_json::Error::custom("Failed to execute script"))
},
}) as Rc<dyn Fn(String) -> EvalResult>
})
}
/// A wrapper around the result of a JavaScript evaluation.
/// This implements IntoFuture to be compatible with the desktop renderer's EvalResult.
pub struct EvalResult {
value: Result<Value, serde_json::Error>,
}
impl EvalResult {
/// Get the result of the Javascript execution.
pub fn get(self) -> Result<Value, serde_json::Error> {
self.value
}
}
impl IntoFuture for EvalResult {
type Output = Result<Value, serde_json::Error>;
type IntoFuture = Ready<Result<Value, serde_json::Error>>;
fn into_future(self) -> Self::IntoFuture {
std::future::ready(self.value)
}
}