浏览代码

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>
Evan Almloff 1 年之前
父节点
当前提交
cbeda0af76

+ 1 - 0
Cargo.lock

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

+ 1 - 0
Cargo.toml

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

+ 6 - 1
packages/desktop/Cargo.toml

@@ -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 - 0
packages/desktop/headless_tests/eval.rs

@@ -0,0 +1,64 @@
+use dioxus::prelude::*;
+use dioxus_desktop::window;
+use serde::Deserialize;
+
+#[path = "./utils.rs"]
+mod utils;
+
+pub fn main() {
+    utils::check_app_exits(app);
+}
+
+static EVALS_RECEIVED: GlobalSignal<usize> = Signal::global(|| 0);
+static EVALS_RETURNED: GlobalSignal<usize> = Signal::global(|| 0);
+
+fn app() -> Element {
+    // Double 100 values in the value
+    use_future(|| async {
+        let mut eval = eval(
+            r#"for (let i = 0; i < 100; i++) {
+            let value = await dioxus.recv();
+            dioxus.send(value*2);
+        }"#,
+        );
+        for i in 0..100 {
+            eval.send(serde_json::Value::from(i)).unwrap();
+            let value = eval.recv().await.unwrap();
+            assert_eq!(value, serde_json::Value::from(i * 2));
+            EVALS_RECEIVED.with_mut(|x| *x += 1);
+        }
+    });
+
+    // Make sure returning no value resolves the future
+    use_future(|| async {
+        let eval = eval(r#"return;"#);
+
+        eval.await.unwrap();
+        EVALS_RETURNED.with_mut(|x| *x += 1);
+    });
+
+    // Return a value from the future
+    use_future(|| async {
+        let eval = eval(
+            r#"
+        return [1, 2, 3];
+        "#,
+        );
+
+        assert_eq!(
+            Vec::<i32>::deserialize(&eval.await.unwrap()).unwrap(),
+            vec![1, 2, 3]
+        );
+        EVALS_RETURNED.with_mut(|x| *x += 1);
+    });
+
+    use_memo(|| {
+        println!("expected 100 evals received found {}", EVALS_RECEIVED());
+        println!("expected 2 eval returned found {}", EVALS_RETURNED());
+        if EVALS_RECEIVED() == 100 && EVALS_RETURNED() == 2 {
+            window().close();
+        }
+    });
+
+    None
+}

+ 1 - 1
packages/desktop/headless_tests/rendering.rs

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

+ 1 - 1
packages/desktop/src/eval.rs

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

+ 6 - 1
packages/desktop/src/protocol.rs

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

+ 78 - 113
packages/desktop/src/query.rs

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

+ 3 - 0
packages/interpreter/Cargo.toml

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

+ 12 - 13
packages/interpreter/build.rs

@@ -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 - 0
packages/interpreter/src/eval.rs

@@ -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 - 0
packages/interpreter/src/js/eval.js

@@ -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
packages/interpreter/src/js/hash.txt

@@ -1 +1 @@
-14148301494
+2770005544568683192

+ 1 - 0
packages/interpreter/src/js/native_eval.js

@@ -0,0 +1 @@
+class Channel{pending;waiting;constructor(){this.pending=[],this.waiting=[]}send(data){if(this.waiting.length>0){this.waiting.shift()(data);return}this.pending.push(data)}async recv(){return new Promise((resolve,_reject)=>{if(this.pending.length>0){resolve(this.pending.shift());return}this.waiting.push(resolve)})}}class WeakDioxusChannel{inner;constructor(channel){this.inner=new WeakRef(channel)}rustSend(data){let channel=this.inner.deref();if(channel)channel.rustSend(data)}async rustRecv(){let channel=this.inner.deref();if(channel)return await channel.rustRecv()}}class DioxusChannel{weak(){return new WeakDioxusChannel(this)}}class QueryParams{id;data;constructor(id,method,data){this.id=id,this.data={method,data}}}window.__msg_queues=window.__msg_queues||[];window.finalizationRegistry=window.finalizationRegistry||new FinalizationRegistry(({id})=>{window.ipc.postMessage(JSON.stringify({method:"query",params:new QueryParams(id,"drop")}))});window.getQuery=function(request_id){return window.__msg_queues[request_id]};window.createQuery=function(request_id){return new NativeDioxusChannel(request_id)};class NativeDioxusChannel extends DioxusChannel{rust_to_js;request_id;constructor(request_id){super();this.rust_to_js=new Channel,this.request_id=request_id,window.__msg_queues[request_id]=this.weak(),window.finalizationRegistry.register(this,{id:request_id})}async recv(){return await this.rust_to_js.recv()}send(data){window.ipc.postMessage(JSON.stringify({method:"query",params:new QueryParams(this.request_id,"send",data)}))}rustSend(data){this.rust_to_js.send(data)}async rustRecv(){}}export{NativeDioxusChannel};

+ 3 - 0
packages/interpreter/src/lib.rs

@@ -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 - 0
packages/interpreter/src/ts/eval.ts

@@ -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 - 0
packages/interpreter/src/ts/native_eval.ts

@@ -0,0 +1,86 @@
+import { Channel, DioxusChannel, WeakDioxusChannel } from "./eval";
+
+// In dioxus desktop, eval needs to use the window object to store global state because we evaluate separate snippets of javascript in the browser
+declare global {
+  interface Window {
+    __msg_queues: WeakDioxusChannel[];
+    finalizationRegistry: FinalizationRegistry<{ id: number }>;
+
+    getQuery(request_id: number): WeakDioxusChannel;
+
+    createQuery(request_id: number): NativeDioxusChannel;
+  }
+}
+
+// A message that can be sent to the desktop renderer about a query
+class QueryParams {
+  id: number;
+  data: { method: "drop" | "send"; data?: any };
+
+  constructor(id: number, method: "drop" | "send", data?: any) {
+    this.id = id;
+    this.data = { method, data };
+  }
+}
+
+window.__msg_queues = window.__msg_queues || [];
+// In dioxus desktop, eval is copy so we cannot run a drop handler. Instead, the drop handler is run after the channel is garbage collected in the javascript side
+window.finalizationRegistry =
+  window.finalizationRegistry ||
+  new FinalizationRegistry(({ id }) => {
+    // @ts-ignore - wry gives us this
+    window.ipc.postMessage(
+      JSON.stringify({
+        method: "query",
+        params: new QueryParams(id, "drop"),
+      })
+    );
+  });
+
+// Get a query from the global state
+window.getQuery = function (request_id: number): WeakDioxusChannel {
+  return window.__msg_queues[request_id];
+};
+
+// Create a new query (and insert it into the global state)
+window.createQuery = function (request_id: number): NativeDioxusChannel {
+  return new NativeDioxusChannel(request_id);
+};
+
+export class NativeDioxusChannel extends DioxusChannel {
+  rust_to_js: Channel;
+  request_id: number;
+
+  constructor(request_id: number) {
+    super();
+    this.rust_to_js = new Channel();
+    this.request_id = request_id;
+
+    window.__msg_queues[request_id] = this.weak();
+    window.finalizationRegistry.register(this, { id: request_id });
+  }
+
+  // Receive message from Rust
+  async recv() {
+    return await this.rust_to_js.recv();
+  }
+
+  // Send message to rust.
+  send(data: any) {
+    // @ts-ignore - wry gives us this
+    window.ipc.postMessage(
+      JSON.stringify({
+        method: "query",
+        params: new QueryParams(this.request_id, "send", data),
+      })
+    );
+  }
+
+  // Send data from rust to javascript
+  rustSend(data: any) {
+    this.rust_to_js.send(data);
+  }
+
+  // Receive data sent from javascript in rust. This is a no-op in the native interpreter because the rust code runs remotely
+  async rustRecv(): Promise<any> {}
+}

+ 2 - 1
packages/interpreter/tsconfig.json

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

+ 12 - 0
packages/playwright-tests/web/src/main.rs

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

+ 1 - 1
packages/web/Cargo.toml

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

+ 0 - 41
packages/web/src/eval.js

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

+ 40 - 44
packages/web/src/eval.rs

@@ -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 dioxus = Dioxus::new(recv_value.as_ref().unchecked_ref());
-        recv_value.forget();
+        let generational_box = owner.invalid();
+
+        // 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);
-}

+ 1 - 1
packages/web/src/rehydrate.rs

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