Refactor and fix eval channels (#2302)

* wip

* pull out eval into the interpreter

* fix web eval

* fix DioxusChannel name

* properly drop dioxus channel

* use typescript dioxus chanel in desktop

* add more comments to native eval

* add desktop headless eval tests

* expand web playwright eval tests

* fix web headless tests

* fix default hasher path

* run eval tests on windows

* restore desktop query drop code

* remove data from drop desktop query message

* catch syntax errors in desktop eval

* catch js runtime errors in desktop

* fix typo interprerter -> interpreter

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan AlmloffandJonathan Kelley authored and GitHub committed 2024-04-26 11:55:48 -04:00
1 parent 1d72ef16c4
commit cbeda0af76
23 files changed
+477 -218

No files matched your search

Generated
+1
View File
@@ -2507,6 +2507,7 @@ dependencies = [
"sledgehammer_bindgen", "sledgehammer_bindgen",
"sledgehammer_utils", "sledgehammer_utils",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures",
"web-sys", "web-sys",
] ]
+1
View File
@@ -87,6 +87,7 @@ futures-channel = "0.3.21"
futures-util = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false }
rustc-hash = "1.1.0" rustc-hash = "1.1.0"
wasm-bindgen = "0.2.92" wasm-bindgen = "0.2.92"
wasm-bindgen-futures = "0.4.42"
html_parser = "0.7.0" html_parser = "0.7.0"
thiserror = "1.0.40" thiserror = "1.0.40"
prettyplease = { version = "0.2.16", features = ["verbatim"] } prettyplease = { version = "0.2.16", features = ["verbatim"] }
+6 -1
View File
@@ -17,7 +17,7 @@ dioxus-html = { workspace = true, features = [
"mounted", "mounted",
"eval", "eval",
] } ] }
dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] } dioxus-interpreter-js = { workspace = true, features = ["binary-protocol", "eval"] }
dioxus-cli-config = { workspace = true, features = ["read-config"] } dioxus-cli-config = { workspace = true, features = ["read-config"] }
generational-box = { workspace = true } generational-box = { workspace = true }
@@ -96,3 +96,8 @@ harness = false
name = "check_rendering" name = "check_rendering"
path = "headless_tests/rendering.rs" path = "headless_tests/rendering.rs"
harness = false harness = false
[[test]]
name = "check_eval"
path = "headless_tests/eval.rs"
harness = false
+64
View File
@@ -0,0 +1,64 @@
use dioxus::prelude::*;
use dioxus_desktop::window;
use serde::Deserialize;
#[path = "./utils.rs"]
mod utils;
pub fn main() {
utils::check_app_exits(app);
}
static EVALS_RECEIVED: GlobalSignal<usize> = Signal::global(|| 0);
static EVALS_RETURNED: GlobalSignal<usize> = Signal::global(|| 0);
fn app() -> Element {
// Double 100 values in the value
use_future(|| async {
let mut eval = eval(
r#"for (let i = 0; i < 100; i++) {
let value = await dioxus.recv();
dioxus.send(value*2);
}"#,
);
for i in 0..100 {
eval.send(serde_json::Value::from(i)).unwrap();
let value = eval.recv().await.unwrap();
assert_eq!(value, serde_json::Value::from(i * 2));
EVALS_RECEIVED.with_mut(|x| *x += 1);
}
});
// Make sure returning no value resolves the future
use_future(|| async {
let eval = eval(r#"return;"#);
eval.await.unwrap();
EVALS_RETURNED.with_mut(|x| *x += 1);
});
// Return a value from the future
use_future(|| async {
let eval = eval(
r#"
return [1, 2, 3];
"#,
);
assert_eq!(
Vec::<i32>::deserialize(&eval.await.unwrap()).unwrap(),
vec![1, 2, 3]
);
EVALS_RETURNED.with_mut(|x| *x += 1);
});
use_memo(|| {
println!("expected 100 evals received found {}", EVALS_RECEIVED());
println!("expected 2 eval returned found {}", EVALS_RETURNED());
if EVALS_RECEIVED() == 100 && EVALS_RETURNED() == 2 {
window().close();
}
});
None
}
+1 -1
View File
@@ -18,7 +18,7 @@ fn use_inner_html(id: &'static str) -> Option<String> {
let res = eval(&format!( let res = eval(&format!(
r#"let element = document.getElementById('{}'); r#"let element = document.getElementById('{}');
return element.innerHTML"#, return element.innerHTML"#,
id id
)) ))
.await .await
+1 -1
View File
@@ -3,7 +3,7 @@ use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
use crate::{query::Query, DesktopContext}; use crate::{query::Query, DesktopContext};
/// Reprents the desktop-target's provider of evaluators. /// Represents the desktop-target's provider of evaluators.
pub struct DesktopEvalProvider { pub struct DesktopEvalProvider {
pub(crate) desktop_ctx: DesktopContext, pub(crate) desktop_ctx: DesktopContext,
} }
+6 -1
View File
@@ -1,4 +1,5 @@
use crate::{assets::*, edits::EditQueue}; use crate::{assets::*, edits::EditQueue};
use dioxus_interpreter_js::eval::NATIVE_EVAL_JS;
use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS; use dioxus_interpreter_js::unified_bindings::SLEDGEHAMMER_JS;
use dioxus_interpreter_js::NATIVE_JS; use dioxus_interpreter_js::NATIVE_JS;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -196,7 +197,7 @@ fn module_loader(root_id: &str, headless: bool) -> String {
// And then extend it with our native bindings // And then extend it with our native bindings
{NATIVE_JS} {NATIVE_JS}
// The nativeinterprerter extends the sledgehammer interpreter with a few extra methods that we use for IPC // The native interpreter extends the sledgehammer interpreter with a few extra methods that we use for IPC
window.interpreter = new NativeInterpreter("{EDITS_PATH}"); window.interpreter = new NativeInterpreter("{EDITS_PATH}");
// Wait for the page to load before sending the initialize message // Wait for the page to load before sending the initialize message
@@ -209,6 +210,10 @@ fn module_loader(root_id: &str, headless: bool) -> String {
window.interpreter.waitForRequest({headless}); window.interpreter.waitForRequest({headless});
}} }}
</script> </script>
<script type="module">
// Include the code for eval
{NATIVE_EVAL_JS}
</script>
"# "#
) )
} }
+78 -113
View File
@@ -7,45 +7,6 @@ use slab::Slab;
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use thiserror::Error; use thiserror::Error;
/*
todo:
- write this in the interpreter itself rather than in blobs of inline javascript...
- it could also be simpler, probably?
*/
const DIOXUS_CODE: &str = r#"
let dioxus = {
recv: function () {
return new Promise((resolve, _reject) => {
// Ever 50 ms check for new data
let timeout = setTimeout(() => {
let __msg = null;
while (true) {
let __data = _message_queue.shift();
if (__data) {
__msg = __data;
break;
}
}
clearTimeout(timeout);
resolve(__msg);
}, 50);
});
},
send: function (value) {
window.ipc.postMessage(
JSON.stringify({
"method":"query",
"params": {
"id": _request_id,
"data": value,
"returned_value": false
}
})
);
}
}"#;
/// Tracks what query ids are currently active /// Tracks what query ids are currently active
pub(crate) struct SharedSlab<T = ()> { pub(crate) struct SharedSlab<T = ()> {
pub slab: Rc<RefCell<Slab<T>>>, pub slab: Rc<RefCell<Slab<T>>>,
@@ -69,12 +30,10 @@ impl<T> Default for SharedSlab<T> {
pub(crate) struct QueryEntry { pub(crate) struct QueryEntry {
channel_sender: futures_channel::mpsc::UnboundedSender<Value>, channel_sender: futures_channel::mpsc::UnboundedSender<Value>,
return_sender: Option<futures_channel::oneshot::Sender<Value>>, return_sender: Option<futures_channel::oneshot::Sender<Result<Value, String>>>,
pub owner: Option<Owner>, pub owner: Option<Owner>,
} }
const QUEUE_NAME: &str = "__msg_queues";
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them. /// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub(crate) struct QueryEngine { pub(crate) struct QueryEngine {
@@ -100,40 +59,51 @@ impl QueryEngine {
// We embed the return of the eval in a function so we can send it back to the main thread // We embed the return of the eval in a function so we can send it back to the main thread
if let Err(err) = context.webview.evaluate_script(&format!( if let Err(err) = context.webview.evaluate_script(&format!(
r#"(function(){{ r#"(function(){{
(async (resolve, _reject) => {{ let dioxus = window.createQuery({request_id});
{DIOXUS_CODE} let post_error = function(err) {{
if (!window.{QUEUE_NAME}) {{
window.{QUEUE_NAME} = [];
}}
let _request_id = {request_id};
if (!window.{QUEUE_NAME}[{request_id}]) {{
window.{QUEUE_NAME}[{request_id}] = [];
}}
let _message_queue = window.{QUEUE_NAME}[{request_id}];
{script}
}})().then((result)=>{{
let returned_value = {{ let returned_value = {{
"method":"query", "method": "query",
"params": {{ "params": {{
"id": {request_id}, "id": {request_id},
"data": result, "data": {{
"returned_value": true "data": err,
"method": "return_error"
}}
}} }}
}}; }};
window.ipc.postMessage( window.ipc.postMessage(
JSON.stringify(returned_value) JSON.stringify(returned_value)
); );
}}) }};
try {{
const AsyncFunction = async function () {{}}.constructor;
let promise = (new AsyncFunction("dioxus", {script:?}))(dioxus);
promise
.then((result)=>{{
let returned_value = {{
"method": "query",
"params": {{
"id": {request_id},
"data": {{
"data": result,
"method": "return"
}}
}}
}};
window.ipc.postMessage(
JSON.stringify(returned_value)
);
}})
.catch(err => post_error(`Error running JS: ${{err}}`));
}} catch (error) {{
post_error(`Invalid JS: ${{error}}`);
}}
}})();"# }})();"#
)) { )) {
tracing::warn!("Query error: {err}"); tracing::warn!("Query error: {err}");
} }
Query { Query {
slab: self.active_requests.clone(),
id: request_id, id: request_id,
receiver: rx, receiver: rx,
return_receiver: Some(return_rx), return_receiver: Some(return_rx),
@@ -144,19 +114,26 @@ impl QueryEngine {
/// Send a query channel message to the correct query /// Send a query channel message to the correct query
pub fn send(&self, data: QueryResult) { pub fn send(&self, data: QueryResult) {
let QueryResult { let QueryResult { id, data } = data;
id,
data,
returned_value,
} = data;
let mut slab = self.active_requests.slab.borrow_mut(); let mut slab = self.active_requests.slab.borrow_mut();
if let Some(entry) = slab.get_mut(id) { if let Some(entry) = slab.get_mut(id) {
if returned_value { match data {
if let Some(sender) = entry.return_sender.take() { QueryResultData::Return { data } => {
let _ = sender.send(data); if let Some(sender) = entry.return_sender.take() {
let _ = sender.send(Ok(data.unwrap_or_default()));
}
}
QueryResultData::ReturnError { data } => {
if let Some(sender) = entry.return_sender.take() {
let _ = sender.send(Err(data.to_string()));
}
}
QueryResultData::Drop => {
slab.remove(id);
}
QueryResultData::Send { data } => {
let _ = entry.channel_sender.unbounded_send(data);
} }
} else {
let _ = entry.channel_sender.unbounded_send(data);
} }
} }
} }
@@ -164,9 +141,8 @@ impl QueryEngine {
pub(crate) struct Query<V: DeserializeOwned> { pub(crate) struct Query<V: DeserializeOwned> {
desktop: DesktopContext, desktop: DesktopContext,
slab: SharedSlab<QueryEntry>,
receiver: futures_channel::mpsc::UnboundedReceiver<Value>, receiver: futures_channel::mpsc::UnboundedReceiver<Value>,
return_receiver: Option<futures_channel::oneshot::Receiver<Value>>, return_receiver: Option<futures_channel::oneshot::Receiver<Result<Value, String>>>,
pub id: usize, pub id: usize,
phantom: std::marker::PhantomData<V>, phantom: std::marker::PhantomData<V>,
} }
@@ -183,18 +159,7 @@ impl<V: DeserializeOwned> Query<V> {
let queue_id = self.id; let queue_id = self.id;
let data = message.to_string(); let data = message.to_string();
let script = format!( let script = format!(r#"window.getQuery({queue_id}).rustSend({data});"#);
r#"
if (!window.{QUEUE_NAME}) {{
window.{QUEUE_NAME} = [];
}}
if (!window.{QUEUE_NAME}[{queue_id}]) {{
window.{QUEUE_NAME}[{queue_id}] = [];
}}
window.{QUEUE_NAME}[{queue_id}].push({data});
"#
);
self.desktop self.desktop
.webview .webview
@@ -211,13 +176,17 @@ impl<V: DeserializeOwned> Query<V> {
) -> std::task::Poll<Result<Value, QueryError>> { ) -> std::task::Poll<Result<Value, QueryError>> {
self.receiver self.receiver
.poll_next_unpin(cx) .poll_next_unpin(cx)
.map(|result| result.ok_or(QueryError::Recv)) .map(|result| result.ok_or(QueryError::Recv(String::from("Receive channel closed"))))
} }
/// Receive the result of the query /// Receive the result of the query
pub async fn result(&mut self) -> Result<Value, QueryError> { pub async fn result(&mut self) -> Result<Value, QueryError> {
match self.return_receiver.take() { match self.return_receiver.take() {
Some(receiver) => receiver.await.map_err(|_| QueryError::Recv), Some(receiver) => match receiver.await {
Ok(Ok(data)) => Ok(data),
Ok(Err(err)) => Err(QueryError::Recv(err)),
Err(err) => Err(QueryError::Recv(err.to_string())),
},
None => Err(QueryError::Finished), None => Err(QueryError::Finished),
} }
} }
@@ -228,36 +197,21 @@ impl<V: DeserializeOwned> Query<V> {
cx: &mut std::task::Context<'_>, cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<Value, QueryError>> { ) -> std::task::Poll<Result<Value, QueryError>> {
match self.return_receiver.as_mut() { match self.return_receiver.as_mut() {
Some(receiver) => receiver.poll_unpin(cx).map_err(|_| QueryError::Recv), Some(receiver) => receiver.poll_unpin(cx).map(|result| match result {
Ok(Ok(data)) => Ok(data),
Ok(Err(err)) => Err(QueryError::Recv(err)),
Err(err) => Err(QueryError::Recv(err.to_string())),
}),
None => std::task::Poll::Ready(Err(QueryError::Finished)), None => std::task::Poll::Ready(Err(QueryError::Finished)),
} }
} }
} }
impl<V: DeserializeOwned> Drop for Query<V> {
fn drop(&mut self) {
self.slab.slab.borrow_mut().remove(self.id);
let queue_id = self.id;
_ = self.desktop.webview.evaluate_script(&format!(
r#"
if (!window.{QUEUE_NAME}) {{
window.{QUEUE_NAME} = [];
}}
if (window.{QUEUE_NAME}[{queue_id}]) {{
window.{QUEUE_NAME}[{queue_id}] = [];
}}
"#
));
}
}
#[derive(Error, Debug)] #[derive(Error, Debug)]
#[non_exhaustive] #[non_exhaustive]
pub enum QueryError { pub enum QueryError {
#[error("Error receiving query result.")] #[error("Error receiving query result: {0}")]
Recv, Recv(String),
#[error("Error sending message to query: {0}")] #[error("Error sending message to query: {0}")]
Send(String), Send(String),
#[error("Error deserializing query result: {0}")] #[error("Error deserializing query result: {0}")]
@@ -269,7 +223,18 @@ pub enum QueryError {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub(crate) struct QueryResult { pub(crate) struct QueryResult {
id: usize, id: usize,
data: Value, data: QueryResultData,
#[serde(default)] }
returned_value: bool,
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "method")]
enum QueryResultData {
#[serde(rename = "return")]
Return { data: Option<Value> },
#[serde(rename = "return_error")]
ReturnError { data: Value },
#[serde(rename = "send")]
Send { data: Value },
#[serde(rename = "drop")]
Drop,
} }
+3
View File
@@ -12,6 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
[dependencies] [dependencies]
wasm-bindgen = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true }
wasm-bindgen-futures = { workspace = true, optional = true }
js-sys = { version = "0.3.56", optional = true } js-sys = { version = "0.3.56", optional = true }
web-sys = { version = "0.3.56", optional = true, features = [ web-sys = { version = "0.3.56", optional = true, features = [
"Element", "Element",
@@ -34,9 +35,11 @@ sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"]
webonly = [ webonly = [
"sledgehammer", "sledgehammer",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures",
"js-sys", "js-sys",
"web-sys", "web-sys",
"sledgehammer_bindgen/web", "sledgehammer_bindgen/web",
] ]
binary-protocol = ["sledgehammer", "dioxus-core", "dioxus-html"] binary-protocol = ["sledgehammer", "dioxus-core", "dioxus-html"]
minimal_bindings = [] minimal_bindings = []
eval = []
+12 -13
View File
@@ -1,4 +1,5 @@
use std::process::Command; use std::collections::hash_map::DefaultHasher;
use std::{hash::Hasher, process::Command};
fn main() { fn main() {
// If any TS changes, re-run the build script // If any TS changes, re-run the build script
@@ -7,6 +8,8 @@ fn main() {
println!("cargo:rerun-if-changed=src/ts/serialize.ts"); println!("cargo:rerun-if-changed=src/ts/serialize.ts");
println!("cargo:rerun-if-changed=src/ts/set_attribute.ts"); println!("cargo:rerun-if-changed=src/ts/set_attribute.ts");
println!("cargo:rerun-if-changed=src/ts/common.ts"); println!("cargo:rerun-if-changed=src/ts/common.ts");
println!("cargo:rerun-if-changed=src/ts/eval.ts");
println!("cargo:rerun-if-changed=src/ts/native_eval.ts");
// Compute the hash of the ts files // Compute the hash of the ts files
let hash = hash_ts_files(); let hash = hash_ts_files();
@@ -22,34 +25,30 @@ fn main() {
gen_bindings("common", "common"); gen_bindings("common", "common");
gen_bindings("native", "native"); gen_bindings("native", "native");
gen_bindings("core", "core"); gen_bindings("core", "core");
gen_bindings("eval", "eval");
gen_bindings("native_eval", "native_eval");
std::fs::write("src/js/hash.txt", hash.to_string()).unwrap(); std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
} }
/// Hashes the contents of a directory /// Hashes the contents of a directory
fn hash_ts_files() -> u128 { fn hash_ts_files() -> u64 {
let mut out = 0;
let files = [ let files = [
include_str!("src/ts/common.ts"), include_str!("src/ts/common.ts"),
include_str!("src/ts/native.ts"), include_str!("src/ts/native.ts"),
include_str!("src/ts/core.ts"), include_str!("src/ts/core.ts"),
include_str!("src/ts/eval.ts"),
include_str!("src/ts/native_eval.ts"),
]; ];
// Let's make the dumbest hasher by summing the bytes of the files let mut hash = DefaultHasher::new();
// The location is multiplied by the byte value to make sure that the order of the bytes matters
let mut idx = 0;
for file in files { for file in files {
// windows + git does a weird thing with line endings, so we need to normalize them // windows + git does a weird thing with line endings, so we need to normalize them
for line in file.lines() { for line in file.lines() {
idx += 1; hash.write(line.as_bytes());
for byte in line.bytes() {
idx += 1;
out += (byte as u128) * (idx as u128);
}
} }
} }
out hash.finish()
} }
// okay...... so tsc might fail if the user doesn't have it installed // okay...... so tsc might fail if the user doesn't have it installed
+49
View File
@@ -0,0 +1,49 @@
/// Code for the Dioxus channel used to communicate between the dioxus and javascript code
pub const NATIVE_EVAL_JS: &str = include_str!("./js/native_eval.js");
#[cfg(feature = "webonly")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub struct JSOwner {
_owner: Box<dyn std::any::Any>,
}
#[cfg(feature = "webonly")]
impl JSOwner {
pub fn new(owner: impl std::any::Any) -> Self {
Self {
_owner: Box::new(owner),
}
}
}
#[cfg(feature = "webonly")]
#[wasm_bindgen::prelude::wasm_bindgen(module = "/src/js/eval.js")]
extern "C" {
pub type WebDioxusChannel;
#[wasm_bindgen(constructor)]
pub fn new(owner: JSOwner) -> WebDioxusChannel;
#[wasm_bindgen(method, js_name = "rustSend")]
pub fn rust_send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
#[wasm_bindgen(method, js_name = "rustRecv")]
pub async fn rust_recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
#[wasm_bindgen(method)]
pub fn send(this: &WebDioxusChannel, value: wasm_bindgen::JsValue);
#[wasm_bindgen(method)]
pub async fn recv(this: &WebDioxusChannel) -> wasm_bindgen::JsValue;
#[wasm_bindgen(method)]
pub fn weak(this: &WebDioxusChannel) -> WeakDioxusChannel;
pub type WeakDioxusChannel;
#[wasm_bindgen(method, js_name = "rustSend")]
pub fn rust_send(this: &WeakDioxusChannel, value: wasm_bindgen::JsValue);
#[wasm_bindgen(method, js_name = "rustRecv")]
pub async fn rust_recv(this: &WeakDioxusChannel) -> wasm_bindgen::JsValue;
}
+1
View File
@@ -0,0 +1 @@
class Channel{pending;waiting;constructor(){this.pending=[],this.waiting=[]}send(data){if(this.waiting.length>0){this.waiting.shift()(data);return}this.pending.push(data)}async recv(){return new Promise((resolve,_reject)=>{if(this.pending.length>0){resolve(this.pending.shift());return}this.waiting.push(resolve)})}}class WeakDioxusChannel{inner;constructor(channel){this.inner=new WeakRef(channel)}rustSend(data){let channel=this.inner.deref();if(channel)channel.rustSend(data)}async rustRecv(){let channel=this.inner.deref();if(channel)return await channel.rustRecv()}}class DioxusChannel{weak(){return new WeakDioxusChannel(this)}}class WebDioxusChannel extends DioxusChannel{js_to_rust;rust_to_js;owner;constructor(owner){super();this.owner=owner,this.js_to_rust=new Channel,this.rust_to_js=new Channel}weak(){return new WeakDioxusChannel(this)}async recv(){return await this.rust_to_js.recv()}send(data){this.js_to_rust.send(data)}rustSend(data){this.rust_to_js.send(data)}async rustRecv(){return await this.js_to_rust.recv()}}export{WebDioxusChannel,WeakDioxusChannel,DioxusChannel,Channel};
+1 -1
View File
@@ -1 +1 @@
14148301494 2770005544568683192
@@ -0,0 +1 @@
class Channel{pending;waiting;constructor(){this.pending=[],this.waiting=[]}send(data){if(this.waiting.length>0){this.waiting.shift()(data);return}this.pending.push(data)}async recv(){return new Promise((resolve,_reject)=>{if(this.pending.length>0){resolve(this.pending.shift());return}this.waiting.push(resolve)})}}class WeakDioxusChannel{inner;constructor(channel){this.inner=new WeakRef(channel)}rustSend(data){let channel=this.inner.deref();if(channel)channel.rustSend(data)}async rustRecv(){let channel=this.inner.deref();if(channel)return await channel.rustRecv()}}class DioxusChannel{weak(){return new WeakDioxusChannel(this)}}class QueryParams{id;data;constructor(id,method,data){this.id=id,this.data={method,data}}}window.__msg_queues=window.__msg_queues||[];window.finalizationRegistry=window.finalizationRegistry||new FinalizationRegistry(({id})=>{window.ipc.postMessage(JSON.stringify({method:"query",params:new QueryParams(id,"drop")}))});window.getQuery=function(request_id){return window.__msg_queues[request_id]};window.createQuery=function(request_id){return new NativeDioxusChannel(request_id)};class NativeDioxusChannel extends DioxusChannel{rust_to_js;request_id;constructor(request_id){super();this.rust_to_js=new Channel,this.request_id=request_id,window.__msg_queues[request_id]=this.weak(),window.finalizationRegistry.register(this,{id:request_id})}async recv(){return await this.rust_to_js.recv()}send(data){window.ipc.postMessage(JSON.stringify({method:"query",params:new QueryParams(this.request_id,"send",data)}))}rustSend(data){this.rust_to_js.send(data)}async rustRecv(){}}export{NativeDioxusChannel};
+3
View File
@@ -20,6 +20,9 @@ pub mod unified_bindings;
#[cfg(feature = "sledgehammer")] #[cfg(feature = "sledgehammer")]
pub use unified_bindings::*; pub use unified_bindings::*;
#[cfg(feature = "eval")]
pub mod eval;
// Common bindings for minimal usage. // Common bindings for minimal usage.
#[cfg(all(feature = "minimal_bindings", feature = "webonly"))] #[cfg(all(feature = "minimal_bindings", feature = "webonly"))]
pub mod minimal_bindings { pub mod minimal_bindings {
+108
View File
@@ -0,0 +1,108 @@
// Handle communication between rust and evaluating javascript
export class Channel {
pending: any[];
waiting: ((data: any) => void)[];
constructor() {
this.pending = [];
this.waiting = [];
}
send(data: any) {
// If there's a waiting callback, call it
if (this.waiting.length > 0) {
this.waiting.shift()(data);
return;
}
// Otherwise queue the data
this.pending.push(data);
}
async recv(): Promise<any> {
return new Promise((resolve, _reject) => {
// If data already exists, resolve immediately
if (this.pending.length > 0) {
resolve(this.pending.shift());
return;
}
// Otherwise queue the resolve callback
this.waiting.push(resolve);
});
}
}
export class WeakDioxusChannel {
inner: WeakRef<DioxusChannel>;
constructor(channel: DioxusChannel) {
this.inner = new WeakRef(channel);
}
// Send data from rust to javascript
rustSend(data: any) {
let channel = this.inner.deref();
if (channel) {
channel.rustSend(data);
}
}
// Receive data sent from javascript in rust
async rustRecv(): Promise<any> {
let channel = this.inner.deref();
if (channel) {
return await channel.rustRecv();
}
}
}
export abstract class DioxusChannel {
// Return a weak reference to this channel
weak(): WeakDioxusChannel {
return new WeakDioxusChannel(this);
}
// Send data from rust to javascript
abstract rustSend(data: any): void;
// Receive data sent from javascript in rust
abstract rustRecv(): Promise<any>;
}
export class WebDioxusChannel extends DioxusChannel {
js_to_rust: Channel;
rust_to_js: Channel;
owner: any;
constructor(owner: any) {
super();
this.owner = owner;
this.js_to_rust = new Channel();
this.rust_to_js = new Channel();
}
// Return a weak reference to this channel
weak(): WeakDioxusChannel {
return new WeakDioxusChannel(this);
}
// Receive message from Rust
async recv() {
return await this.rust_to_js.recv();
}
// Send message to rust.
send(data: any) {
this.js_to_rust.send(data);
}
// Send data from rust to javascript
rustSend(data: any) {
this.rust_to_js.send(data);
}
// Receive data sent from javascript in rust
async rustRecv(): Promise<any> {
return await this.js_to_rust.recv();
}
}
@@ -0,0 +1,86 @@
import { Channel, DioxusChannel, WeakDioxusChannel } from "./eval";
// In dioxus desktop, eval needs to use the window object to store global state because we evaluate separate snippets of javascript in the browser
declare global {
interface Window {
__msg_queues: WeakDioxusChannel[];
finalizationRegistry: FinalizationRegistry<{ id: number }>;
getQuery(request_id: number): WeakDioxusChannel;
createQuery(request_id: number): NativeDioxusChannel;
}
}
// A message that can be sent to the desktop renderer about a query
class QueryParams {
id: number;
data: { method: "drop" | "send"; data?: any };
constructor(id: number, method: "drop" | "send", data?: any) {
this.id = id;
this.data = { method, data };
}
}
window.__msg_queues = window.__msg_queues || [];
// In dioxus desktop, eval is copy so we cannot run a drop handler. Instead, the drop handler is run after the channel is garbage collected in the javascript side
window.finalizationRegistry =
window.finalizationRegistry ||
new FinalizationRegistry(({ id }) => {
// @ts-ignore - wry gives us this
window.ipc.postMessage(
JSON.stringify({
method: "query",
params: new QueryParams(id, "drop"),
})
);
});
// Get a query from the global state
window.getQuery = function (request_id: number): WeakDioxusChannel {
return window.__msg_queues[request_id];
};
// Create a new query (and insert it into the global state)
window.createQuery = function (request_id: number): NativeDioxusChannel {
return new NativeDioxusChannel(request_id);
};
export class NativeDioxusChannel extends DioxusChannel {
rust_to_js: Channel;
request_id: number;
constructor(request_id: number) {
super();
this.rust_to_js = new Channel();
this.request_id = request_id;
window.__msg_queues[request_id] = this.weak();
window.finalizationRegistry.register(this, { id: request_id });
}
// Receive message from Rust
async recv() {
return await this.rust_to_js.recv();
}
// Send message to rust.
send(data: any) {
// @ts-ignore - wry gives us this
window.ipc.postMessage(
JSON.stringify({
method: "query",
params: new QueryParams(this.request_id, "send", data),
})
);
}
// Send data from rust to javascript
rustSend(data: any) {
this.rust_to_js.send(data);
}
// Receive data sent from javascript in rust. This is a no-op in the native interpreter because the rust code runs remotely
async rustRecv(): Promise<any> {}
}
+2 -1
View File
@@ -5,7 +5,8 @@
"ES2015", "ES2015",
"DOM", "DOM",
"dom", "dom",
"dom.iterable" "dom.iterable",
"ESNext"
], ],
"noImplicitAny": true, "noImplicitAny": true,
"removeComments": true, "removeComments": true,
+12
View File
@@ -26,10 +26,22 @@ fn app() -> Element {
let mut eval = eval( let mut eval = eval(
r#" r#"
window.document.title = 'Hello from Dioxus Eval!'; window.document.title = 'Hello from Dioxus Eval!';
// Receive and multiply 10 numbers
for (let i = 0; i < 10; i++) {
let value = await dioxus.recv();
dioxus.send(value*2);
}
dioxus.send("returned eval value"); dioxus.send("returned eval value");
"#, "#,
); );
// Send 10 numbers
for i in 0..10 {
eval.send(serde_json::Value::from(i)).unwrap();
let value = eval.recv().await.unwrap();
assert_eq!(value, serde_json::Value::from(i * 2));
}
let result = eval.recv().await; let result = eval.recv().await;
if let Ok(serde_json::Value::String(string)) = result { if let Ok(serde_json::Value::String(string)) = result {
eval_result.set(string); eval_result.set(string);
+1 -1
View File
@@ -63,7 +63,7 @@ file_engine = [
"async-trait", "async-trait",
] ]
hot_reload = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location"] hot_reload = ["web-sys/MessageEvent", "web-sys/WebSocket", "web-sys/Location"]
eval = ["dioxus-html/eval", "serde-wasm-bindgen", "async-trait"] eval = ["dioxus-html/eval", "dioxus-interpreter-js/eval", "serde-wasm-bindgen", "async-trait"]
[dev-dependencies] [dev-dependencies]
dioxus = { workspace = true } dioxus = { workspace = true }
-41
View File
@@ -1,41 +0,0 @@
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);
}
}
+39 -43
View File
@@ -1,8 +1,10 @@
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator}; use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
use futures_util::StreamExt; use dioxus_interpreter_js::eval::{JSOwner, WeakDioxusChannel, WebDioxusChannel};
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage}; use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
use js_sys::Function; use js_sys::Function;
use serde_json::Value; use serde_json::Value;
use std::future::Future;
use std::pin::Pin;
use std::{rc::Rc, str::FromStr}; use std::{rc::Rc, str::FromStr};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@@ -26,42 +28,35 @@ const PROMISE_WRAPPER: &str = r#"
{JS_CODE} {JS_CODE}
resolve(null); resolve(null);
}); });
"#; "#;
type NextPoll = Pin<Box<dyn Future<Output = Result<serde_json::Value, EvalError>>>>;
/// Represents a web-target's JavaScript evaluator. /// Represents a web-target's JavaScript evaluator.
struct WebEvaluator { struct WebEvaluator {
dioxus: Dioxus, channels: WeakDioxusChannel,
channel_receiver: futures_channel::mpsc::UnboundedReceiver<serde_json::Value>, next_future: Option<NextPoll>,
result: Option<Result<serde_json::Value, EvalError>>, result: Option<Result<serde_json::Value, EvalError>>,
} }
impl WebEvaluator { impl WebEvaluator {
/// Creates a new evaluator for web-based targets. /// Creates a new evaluator for web-based targets.
fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> { fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
let (mut channel_sender, channel_receiver) = futures_channel::mpsc::unbounded();
let owner = UnsyncStorage::owner(); let owner = UnsyncStorage::owner();
let invalid = owner.invalid();
// This Rc cloning mess hurts but it seems to work.. let generational_box = owner.invalid();
let recv_value = Closure::<dyn FnMut(JsValue)>::new(move |data| {
// Drop the owner when the sender is dropped.
let _ = &owner;
match serde_wasm_bindgen::from_value::<serde_json::Value>(data) {
Ok(data) => _ = channel_sender.start_send(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()); // add the drop handler to DioxusChannel so that it gets dropped when the channel is dropped in js
recv_value.forget(); let channels = WebDioxusChannel::new(JSOwner::new(owner));
// The Rust side of the channel is a weak reference to the DioxusChannel
let weak_channels = channels.weak();
// Wrap the evaluated JS in a promise so that wasm can continue running (send/receive data from js) // 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 code = PROMISE_WRAPPER.replace("{JS_CODE}", &js);
let result = match Function::new_with_args("dioxus", &code).call1(&JsValue::NULL, &dioxus) { let result = match Function::new_with_args("dioxus", &code).call1(&JsValue::NULL, &channels)
{
Ok(result) => { Ok(result) => {
if let Ok(stringified) = js_sys::JSON::stringify(&result) { if let Ok(stringified) = js_sys::JSON::stringify(&result) {
if !stringified.is_undefined() && stringified.is_valid_utf16() { if !stringified.is_undefined() && stringified.is_valid_utf16() {
@@ -85,13 +80,13 @@ impl WebEvaluator {
)), )),
}; };
invalid.set(Box::new(Self { generational_box.set(Box::new(Self {
dioxus, channels: weak_channels,
channel_receiver,
result: Some(result), result: Some(result),
}) as Box<dyn Evaluator + 'static>); next_future: None,
}) as Box<dyn Evaluator>);
invalid generational_box
} }
} }
@@ -115,7 +110,7 @@ impl Evaluator for WebEvaluator {
Err(e) => return Err(EvalError::Communication(e.to_string())), Err(e) => return Err(EvalError::Communication(e.to_string())),
}; };
self.dioxus.rustSend(data); self.channels.rust_send(data);
Ok(()) Ok(())
} }
@@ -124,21 +119,22 @@ impl Evaluator for WebEvaluator {
&mut self, &mut self,
context: &mut std::task::Context<'_>, context: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<serde_json::Value, EvalError>> { ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
self.channel_receiver.poll_next_unpin(context).map(|poll| { if self.next_future.is_none() {
poll.ok_or_else(|| { let channels: WebDioxusChannel = self.channels.clone().into();
EvalError::Communication("failed to receive data from js".to_string()) let pinned = Box::pin(async move {
}) let fut = channels.rust_recv();
}) let data = fut.await;
serde_wasm_bindgen::from_value::<serde_json::Value>(data)
.map_err(|err| EvalError::Communication(err.to_string()))
});
self.next_future = Some(pinned);
}
let fut = self.next_future.as_mut().unwrap();
let mut pinned = std::pin::pin!(fut);
let result = pinned.as_mut().poll(context);
if result.is_ready() {
self.next_future = None;
}
result
} }
} }
#[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);
}
+1 -1
View File
@@ -2,7 +2,7 @@ use crate::dom::WebsysDom;
use dioxus_core::prelude::*; use dioxus_core::prelude::*;
use dioxus_core::AttributeValue; use dioxus_core::AttributeValue;
use dioxus_core::WriteMutations; use dioxus_core::WriteMutations;
use dioxus_core::{DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom}; use dioxus_core::{DynamicNode, ElementId};
#[derive(Debug)] #[derive(Debug)]
#[non_exhaustive] #[non_exhaustive]