2
0
Fork 0
mirror of https://github.com/DioxusLabs/dioxus synced 2025-02-23 00:58:26 +00:00

Refactor and fix eval channels ()

* 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 Almloff 2024-04-26 10:55:48 -05:00 committed by GitHub
parent 1d72ef16c4
commit cbeda0af76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 477 additions and 218 deletions

1
Cargo.lock generated
View file

@ -2507,6 +2507,7 @@ dependencies = [
"sledgehammer_bindgen",
"sledgehammer_utils",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]

View file

@ -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"] }

View file

@ -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

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
}

View file

@ -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

View file

@ -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,
}

View file

@ -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>
"#
)
}

View file

@ -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,
}

View file

@ -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 = []

View file

@ -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

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;
}

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};

View file

@ -1 +1 @@
14148301494
2770005544568683192

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 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};

View file

@ -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 {

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();
}
}

View 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> {}
}

View file

@ -5,7 +5,8 @@
"ES2015",
"DOM",
"dom",
"dom.iterable"
"dom.iterable",
"ESNext"
],
"noImplicitAny": true,
"removeComments": true,

View file

@ -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);

View file

@ -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 }

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);
}
}

View file

@ -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);
}

View file

@ -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]