Refactor and fix eval channels (#2302)

* wip

* pull out eval into the interpreter

* fix web eval

* fix DioxusChannel name

* properly drop dioxus channel

* use typescript dioxus chanel in desktop

* add more comments to native eval

* add desktop headless eval tests

* expand web playwright eval tests

* fix web headless tests

* fix default hasher path

* run eval tests on windows

* restore desktop query drop code

* remove data from drop desktop query message

* catch syntax errors in desktop eval

* catch js runtime errors in desktop

* fix typo interprerter -> interpreter

---------

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

No files matched your search

+1 -1
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,
}
+6 -1
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>
"#
)
}
+78 -113
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,
}