mirror of
https://github.com/DioxusLabs/dioxus
synced 2025-02-23 00:58:26 +00:00
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:
parent
1d72ef16c4
commit
cbeda0af76
23 changed files with 477 additions and 218 deletions
Cargo.lockCargo.toml
packages
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2507,6 +2507,7 @@ dependencies = [
|
|||
"sledgehammer_bindgen",
|
||||
"sledgehammer_utils",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ futures-channel = "0.3.21"
|
|||
futures-util = { version = "0.3", default-features = false }
|
||||
rustc-hash = "1.1.0"
|
||||
wasm-bindgen = "0.2.92"
|
||||
wasm-bindgen-futures = "0.4.42"
|
||||
html_parser = "0.7.0"
|
||||
thiserror = "1.0.40"
|
||||
prettyplease = { version = "0.2.16", features = ["verbatim"] }
|
||||
|
|
|
@ -17,7 +17,7 @@ dioxus-html = { workspace = true, features = [
|
|||
"mounted",
|
||||
"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"] }
|
||||
generational-box = { workspace = true }
|
||||
|
||||
|
@ -96,3 +96,8 @@ harness = false
|
|||
name = "check_rendering"
|
||||
path = "headless_tests/rendering.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "check_eval"
|
||||
path = "headless_tests/eval.rs"
|
||||
harness = false
|
||||
|
|
64
packages/desktop/headless_tests/eval.rs
Normal file
64
packages/desktop/headless_tests/eval.rs
Normal 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
|
||||
}
|
|
@ -18,7 +18,7 @@ fn use_inner_html(id: &'static str) -> Option<String> {
|
|||
|
||||
let res = eval(&format!(
|
||||
r#"let element = document.getElementById('{}');
|
||||
return element.innerHTML"#,
|
||||
return element.innerHTML"#,
|
||||
id
|
||||
))
|
||||
.await
|
||||
|
|
|
@ -3,7 +3,7 @@ use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
|||
|
||||
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(crate) desktop_ctx: DesktopContext,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
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::NATIVE_JS;
|
||||
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
|
||||
{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}");
|
||||
|
||||
// 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});
|
||||
}}
|
||||
</script>
|
||||
<script type="module">
|
||||
// Include the code for eval
|
||||
{NATIVE_EVAL_JS}
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,45 +7,6 @@ use slab::Slab;
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
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
|
||||
pub(crate) struct SharedSlab<T = ()> {
|
||||
pub slab: Rc<RefCell<Slab<T>>>,
|
||||
|
@ -69,12 +30,10 @@ impl<T> Default for SharedSlab<T> {
|
|||
|
||||
pub(crate) struct QueryEntry {
|
||||
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>,
|
||||
}
|
||||
|
||||
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.
|
||||
#[derive(Clone, Default)]
|
||||
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
|
||||
if let Err(err) = context.webview.evaluate_script(&format!(
|
||||
r#"(function(){{
|
||||
(async (resolve, _reject) => {{
|
||||
{DIOXUS_CODE}
|
||||
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 dioxus = window.createQuery({request_id});
|
||||
let post_error = function(err) {{
|
||||
let returned_value = {{
|
||||
"method":"query",
|
||||
"method": "query",
|
||||
"params": {{
|
||||
"id": {request_id},
|
||||
"data": result,
|
||||
"returned_value": true
|
||||
"data": {{
|
||||
"data": err,
|
||||
"method": "return_error"
|
||||
}}
|
||||
}}
|
||||
}};
|
||||
window.ipc.postMessage(
|
||||
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}");
|
||||
}
|
||||
|
||||
Query {
|
||||
slab: self.active_requests.clone(),
|
||||
id: request_id,
|
||||
receiver: rx,
|
||||
return_receiver: Some(return_rx),
|
||||
|
@ -144,19 +114,26 @@ impl QueryEngine {
|
|||
|
||||
/// Send a query channel message to the correct query
|
||||
pub fn send(&self, data: QueryResult) {
|
||||
let QueryResult {
|
||||
id,
|
||||
data,
|
||||
returned_value,
|
||||
} = data;
|
||||
let QueryResult { id, data } = data;
|
||||
let mut slab = self.active_requests.slab.borrow_mut();
|
||||
if let Some(entry) = slab.get_mut(id) {
|
||||
if returned_value {
|
||||
if let Some(sender) = entry.return_sender.take() {
|
||||
let _ = sender.send(data);
|
||||
match data {
|
||||
QueryResultData::Return { 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> {
|
||||
desktop: DesktopContext,
|
||||
slab: SharedSlab<QueryEntry>,
|
||||
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,
|
||||
phantom: std::marker::PhantomData<V>,
|
||||
}
|
||||
|
@ -183,18 +159,7 @@ impl<V: DeserializeOwned> Query<V> {
|
|||
let queue_id = self.id;
|
||||
|
||||
let data = message.to_string();
|
||||
let script = format!(
|
||||
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});
|
||||
"#
|
||||
);
|
||||
let script = format!(r#"window.getQuery({queue_id}).rustSend({data});"#);
|
||||
|
||||
self.desktop
|
||||
.webview
|
||||
|
@ -211,13 +176,17 @@ impl<V: DeserializeOwned> Query<V> {
|
|||
) -> std::task::Poll<Result<Value, QueryError>> {
|
||||
self.receiver
|
||||
.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
|
||||
pub async fn result(&mut self) -> Result<Value, QueryError> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
@ -228,36 +197,21 @@ impl<V: DeserializeOwned> Query<V> {
|
|||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<Value, QueryError>> {
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
#[non_exhaustive]
|
||||
pub enum QueryError {
|
||||
#[error("Error receiving query result.")]
|
||||
Recv,
|
||||
#[error("Error receiving query result: {0}")]
|
||||
Recv(String),
|
||||
#[error("Error sending message to query: {0}")]
|
||||
Send(String),
|
||||
#[error("Error deserializing query result: {0}")]
|
||||
|
@ -269,7 +223,18 @@ pub enum QueryError {
|
|||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(crate) struct QueryResult {
|
||||
id: usize,
|
||||
data: Value,
|
||||
#[serde(default)]
|
||||
returned_value: bool,
|
||||
data: QueryResultData,
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
|
|||
|
||||
[dependencies]
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
wasm-bindgen-futures = { workspace = true, optional = true }
|
||||
js-sys = { version = "0.3.56", optional = true }
|
||||
web-sys = { version = "0.3.56", optional = true, features = [
|
||||
"Element",
|
||||
|
@ -34,9 +35,11 @@ sledgehammer = ["sledgehammer_bindgen", "sledgehammer_utils"]
|
|||
webonly = [
|
||||
"sledgehammer",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"js-sys",
|
||||
"web-sys",
|
||||
"sledgehammer_bindgen/web",
|
||||
]
|
||||
binary-protocol = ["sledgehammer", "dioxus-core", "dioxus-html"]
|
||||
minimal_bindings = []
|
||||
eval = []
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::process::Command;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::{hash::Hasher, process::Command};
|
||||
|
||||
fn main() {
|
||||
// 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/set_attribute.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
|
||||
let hash = hash_ts_files();
|
||||
|
@ -22,34 +25,30 @@ fn main() {
|
|||
gen_bindings("common", "common");
|
||||
gen_bindings("native", "native");
|
||||
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();
|
||||
}
|
||||
|
||||
/// Hashes the contents of a directory
|
||||
fn hash_ts_files() -> u128 {
|
||||
let mut out = 0;
|
||||
|
||||
fn hash_ts_files() -> u64 {
|
||||
let files = [
|
||||
include_str!("src/ts/common.ts"),
|
||||
include_str!("src/ts/native.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
|
||||
// The location is multiplied by the byte value to make sure that the order of the bytes matters
|
||||
let mut idx = 0;
|
||||
let mut hash = DefaultHasher::new();
|
||||
for file in files {
|
||||
// windows + git does a weird thing with line endings, so we need to normalize them
|
||||
for line in file.lines() {
|
||||
idx += 1;
|
||||
for byte in line.bytes() {
|
||||
idx += 1;
|
||||
out += (byte as u128) * (idx as u128);
|
||||
}
|
||||
hash.write(line.as_bytes());
|
||||
}
|
||||
}
|
||||
out
|
||||
hash.finish()
|
||||
}
|
||||
|
||||
// okay...... so tsc might fail if the user doesn't have it installed
|
||||
|
|
49
packages/interpreter/src/eval.rs
Normal file
49
packages/interpreter/src/eval.rs
Normal 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
packages/interpreter/src/js/eval.js
Normal file
1
packages/interpreter/src/js/eval.js
Normal 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 @@
|
|||
14148301494
|
||||
2770005544568683192
|
1
packages/interpreter/src/js/native_eval.js
Normal file
1
packages/interpreter/src/js/native_eval.js
Normal 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 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};
|
|
@ -20,6 +20,9 @@ pub mod unified_bindings;
|
|||
#[cfg(feature = "sledgehammer")]
|
||||
pub use unified_bindings::*;
|
||||
|
||||
#[cfg(feature = "eval")]
|
||||
pub mod eval;
|
||||
|
||||
// Common bindings for minimal usage.
|
||||
#[cfg(all(feature = "minimal_bindings", feature = "webonly"))]
|
||||
pub mod minimal_bindings {
|
||||
|
|
108
packages/interpreter/src/ts/eval.ts
Normal file
108
packages/interpreter/src/ts/eval.ts
Normal 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();
|
||||
}
|
||||
}
|
86
packages/interpreter/src/ts/native_eval.ts
Normal file
86
packages/interpreter/src/ts/native_eval.ts
Normal file
|
@ -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> {}
|
||||
}
|
|
@ -5,7 +5,8 @@
|
|||
"ES2015",
|
||||
"DOM",
|
||||
"dom",
|
||||
"dom.iterable"
|
||||
"dom.iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"noImplicitAny": true,
|
||||
"removeComments": true,
|
||||
|
|
|
@ -26,10 +26,22 @@ fn app() -> Element {
|
|||
let mut eval = eval(
|
||||
r#"
|
||||
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");
|
||||
"#,
|
||||
);
|
||||
|
||||
// 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;
|
||||
if let Ok(serde_json::Value::String(string)) = result {
|
||||
eval_result.set(string);
|
||||
|
|
|
@ -63,7 +63,7 @@ file_engine = [
|
|||
"async-trait",
|
||||
]
|
||||
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]
|
||||
dioxus = { workspace = true }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
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 js_sys::Function;
|
||||
use serde_json::Value;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::{rc::Rc, str::FromStr};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
|
@ -26,42 +28,35 @@ const PROMISE_WRAPPER: &str = r#"
|
|||
{JS_CODE}
|
||||
resolve(null);
|
||||
});
|
||||
"#;
|
||||
"#;
|
||||
|
||||
type NextPoll = Pin<Box<dyn Future<Output = Result<serde_json::Value, EvalError>>>>;
|
||||
|
||||
/// Represents a web-target's JavaScript evaluator.
|
||||
struct WebEvaluator {
|
||||
dioxus: Dioxus,
|
||||
channel_receiver: futures_channel::mpsc::UnboundedReceiver<serde_json::Value>,
|
||||
channels: WeakDioxusChannel,
|
||||
next_future: Option<NextPoll>,
|
||||
result: Option<Result<serde_json::Value, EvalError>>,
|
||||
}
|
||||
|
||||
impl WebEvaluator {
|
||||
/// Creates a new evaluator for web-based targets.
|
||||
fn create(js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
||||
let (mut channel_sender, channel_receiver) = futures_channel::mpsc::unbounded();
|
||||
let owner = UnsyncStorage::owner();
|
||||
let invalid = owner.invalid();
|
||||
|
||||
// This Rc cloning mess hurts but it seems to work..
|
||||
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 generational_box = owner.invalid();
|
||||
|
||||
let dioxus = Dioxus::new(recv_value.as_ref().unchecked_ref());
|
||||
recv_value.forget();
|
||||
// add the drop handler to DioxusChannel so that it gets dropped when the channel is dropped in js
|
||||
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)
|
||||
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) => {
|
||||
if let Ok(stringified) = js_sys::JSON::stringify(&result) {
|
||||
if !stringified.is_undefined() && stringified.is_valid_utf16() {
|
||||
|
@ -85,13 +80,13 @@ impl WebEvaluator {
|
|||
)),
|
||||
};
|
||||
|
||||
invalid.set(Box::new(Self {
|
||||
dioxus,
|
||||
channel_receiver,
|
||||
generational_box.set(Box::new(Self {
|
||||
channels: weak_channels,
|
||||
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())),
|
||||
};
|
||||
|
||||
self.dioxus.rustSend(data);
|
||||
self.channels.rust_send(data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -124,21 +119,22 @@ impl Evaluator for WebEvaluator {
|
|||
&mut self,
|
||||
context: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||
self.channel_receiver.poll_next_unpin(context).map(|poll| {
|
||||
poll.ok_or_else(|| {
|
||||
EvalError::Communication("failed to receive data from js".to_string())
|
||||
})
|
||||
})
|
||||
if self.next_future.is_none() {
|
||||
let channels: WebDioxusChannel = self.channels.clone().into();
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use crate::dom::WebsysDom;
|
|||
use dioxus_core::prelude::*;
|
||||
use dioxus_core::AttributeValue;
|
||||
use dioxus_core::WriteMutations;
|
||||
use dioxus_core::{DynamicNode, ElementId, ScopeState, TemplateNode, VNode, VirtualDom};
|
||||
use dioxus_core::{DynamicNode, ElementId};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
|
|
Loading…
Add table
Reference in a new issue