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:
1 parent
1d72ef16c4
commit
cbeda0af76
23 files changed
+477
-218
No files matched your search
Generated
+1
@@ -2507,6 +2507,7 @@ dependencies = [
|
|||||||
"sledgehammer_bindgen",
|
"sledgehammer_bindgen",
|
||||||
"sledgehammer_utils",
|
"sledgehammer_utils",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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!(
|
let res = eval(&format!(
|
||||||
r#"let element = document.getElementById('{}');
|
r#"let element = document.getElementById('{}');
|
||||||
return element.innerHTML"#,
|
return element.innerHTML"#,
|
||||||
id
|
id
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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 = []
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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")]
|
#[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 {
|
||||||
|
|||||||
@@ -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> {}
|
||||||
|
}
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
"ES2015",
|
"ES2015",
|
||||||
"DOM",
|
"DOM",
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable"
|
"dom.iterable",
|
||||||
|
"ESNext"
|
||||||
],
|
],
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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]
|
||||||
|
|||||||
Reference in new issue
Block a user