implement a query engine for liveview

This commit is contained in:
Evan Almloff committed 2023-03-23 16:52:29 -05:00
1 parent fa9f0d0f6c
commit 8243dfe00d
7 files changed
+323 -16

No files matched your search

+5
View File
@@ -7,11 +7,13 @@ use thiserror::Error;
use tokio::sync::broadcast::error::RecvError;
use wry::webview::WebView;
/// Tracks what query ids are currently active
#[derive(Default, Clone)]
struct SharedSlab {
slab: Rc<RefCell<Slab<()>>>,
}
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone)]
pub(crate) struct QueryEngine {
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
@@ -29,6 +31,7 @@ impl Default for QueryEngine {
}
impl QueryEngine {
/// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id.
pub fn new_query<V: DeserializeOwned>(&self, script: &str, webview: &WebView) -> Query<V> {
let request_id = self.active_requests.slab.borrow_mut().insert(());
@@ -56,6 +59,7 @@ impl QueryEngine {
}
}
/// Send a query result
pub fn send(&self, data: QueryResult) {
let _ = self.sender.send(data);
}
@@ -69,6 +73,7 @@ pub(crate) struct Query<V: DeserializeOwned> {
}
impl<V: DeserializeOwned> Query<V> {
/// Resolve the query
pub async fn resolve(mut self) -> Result<V, QueryError> {
let result = loop {
match self.reciever.recv().await {
+2
View File
@@ -14,6 +14,8 @@ license = "MIT/Apache-2.0"
[dependencies]
thiserror = "1.0.38"
log = "0.4.14"
slab = "0.4"
futures-util = { version = "0.3.25", default-features = false, features = [
"sink",
] }
+125
View File
@@ -0,0 +1,125 @@
use dioxus_core::ElementId;
use dioxus_html::{geometry::euclid::Rect, MountedResult, RenderedElementBacking};
use tokio::sync::mpsc::UnboundedSender;
use crate::query::QueryEngine;
/// A mounted element passed to onmounted events
pub struct LiveviewElement {
id: ElementId,
query_tx: UnboundedSender<String>,
query: QueryEngine,
}
impl LiveviewElement {
pub(crate) fn new(id: ElementId, tx: UnboundedSender<String>, query: QueryEngine) -> Self {
Self {
id,
query_tx: tx,
query,
}
}
}
impl RenderedElementBacking for LiveviewElement {
fn get_raw_element(&self) -> dioxus_html::MountedResult<&dyn std::any::Any> {
Ok(self)
}
fn get_client_rect(
&self,
) -> std::pin::Pin<
Box<
dyn futures_util::Future<
Output = dioxus_html::MountedResult<dioxus_html::geometry::euclid::Rect<f64, f64>>,
>,
>,
> {
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
let fut = self
.query
.new_query::<Option<Rect<f64, f64>>>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(Some(rect)) => Ok(rect),
Ok(None) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn scroll_to(
&self,
behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.ScrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
);
let fut = self
.query
.new_query::<bool>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
fn set_focus(
&self,
focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.SetFocus({}, {});",
self.id.0, focus
);
let fut = self
.query
.new_query::<bool>(&script, &self.query_tx)
.resolve();
Box::pin(async move {
match fut.await {
Ok(true) => Ok(()),
Ok(false) => MountedResult::Err(dioxus_html::MountedError::OperationFailed(
Box::new(DesktopQueryError::FailedToQuery),
)),
Err(err) => {
MountedResult::Err(dioxus_html::MountedError::OperationFailed(Box::new(err)))
}
}
})
}
}
#[derive(Debug)]
enum DesktopQueryError {
FailedToQuery,
}
impl std::fmt::Display for DesktopQueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DesktopQueryError::FailedToQuery => write!(f, "Failed to query the element"),
}
}
}
impl std::error::Error for DesktopQueryError {}
+2
View File
@@ -18,7 +18,9 @@ pub mod adapters {
pub use adapters::*;
mod element;
pub mod pool;
mod query;
use futures_util::{SinkExt, StreamExt};
pub use pool::*;
+12 -4
View File
@@ -26,11 +26,19 @@ class IPC {
// todo: retry the connection
};
ws.onmessage = (event) => {
ws.onmessage = (message) => {
// Ignore pongs
if (event.data != "__pong__") {
let edits = JSON.parse(event.data);
window.interpreter.handleEdits(edits);
if (message.data != "__pong__") {
const event = JSON.parse(message.data);
switch (event.type) {
case "edits":
let edits = event.data;
window.interpreter.handleEdits(edits);
break;
case "query":
Function("Eval", `"use strict";${event.data};`)();
break;
}
}
};
+64 -12
View File
@@ -1,8 +1,13 @@
use crate::LiveViewError;
use dioxus_core::prelude::*;
use dioxus_html::HtmlEvent;
use crate::{
element::LiveviewElement,
query::{QueryEngine, QueryResult},
LiveViewError,
};
use dioxus_core::{prelude::*, Mutations};
use dioxus_html::{EventData, HtmlEvent, MountedData};
use futures_util::{pin_mut, SinkExt, StreamExt};
use std::time::Duration;
use serde::Serialize;
use std::{rc::Rc, time::Duration};
use tokio_util::task::LocalPoolHandle;
#[derive(Clone)]
@@ -115,7 +120,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
};
// todo: use an efficient binary packed format for this
let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap();
// pin the futures so we can use select!
pin_mut!(ws);
@@ -123,11 +128,19 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
// send the initial render to the client
ws.send(edits).await?;
// Create the a proxy for query engine
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
let query_engine = QueryEngine::default();
// desktop uses this wrapper struct thing around the actual event itself
// this is sorta driven by tao/wry
#[derive(serde::Deserialize)]
struct IpcMessage {
params: HtmlEvent,
#[derive(serde::Deserialize, Debug)]
#[serde(tag = "method", content = "params")]
enum IpcMessage {
#[serde(rename = "user_event")]
Event(HtmlEvent),
#[serde(rename = "query")]
Query(QueryResult),
}
loop {
@@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
ws.send("__pong__".to_string()).await?;
}
Some(Ok(evt)) => {
if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(evt) {
vdom.handle_event(&params.name, params.data.into_any(), params.element, params.bubbles);
if let Ok(message) = serde_json::from_str::<IpcMessage>(evt) {
match message {
IpcMessage::Event(evt) => {
// Intercept the mounted event and insert a custom element type
if let EventData::Mounted = &evt.data {
let element = LiveviewElement::new(evt.element, query_tx.clone(), query_engine.clone());
vdom.handle_event(
&evt.name,
Rc::new(MountedData::new(element)),
evt.element,
evt.bubbles,
);
}
else{
vdom.handle_event(
&evt.name,
evt.data.into_any(),
evt.element,
evt.bubbles,
);
}
}
IpcMessage::Query(result) => {
query_engine.send(result);
},
}
}
}
// log this I guess? when would we get an error here?
Some(Err(_e)) => {},
Some(Err(_e)) => {}
None => return Ok(()),
}
}
// handle any new queries
Some(query) = query_rx.recv() => {
ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?;
}
Some(msg) = hot_reload_wait => {
#[cfg(all(feature = "hot-reload", debug_assertions))]
match msg{
@@ -176,6 +218,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
.await;
ws.send(serde_json::to_string(&edits).unwrap()).await?;
ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap())
.await?;
}
}
#[derive(Serialize)]
#[serde(tag = "type", content = "data")]
enum ClientUpdate<'a> {
#[serde(rename = "edits")]
Edits(Mutations<'a>),
#[serde(rename = "query")]
Query(String),
}
+113
View File
@@ -0,0 +1,113 @@
use std::{cell::RefCell, rc::Rc};
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use slab::Slab;
use thiserror::Error;
use tokio::sync::{broadcast::error::RecvError, mpsc::UnboundedSender};
/// Tracks what query ids are currently active
#[derive(Default, Clone)]
struct SharedSlab {
slab: Rc<RefCell<Slab<()>>>,
}
/// Handles sending and receiving arbitrary queries from the webview. Queries can be resolved non-sequentially, so we use ids to track them.
#[derive(Clone)]
pub(crate) struct QueryEngine {
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
active_requests: SharedSlab,
}
impl Default for QueryEngine {
fn default() -> Self {
let (sender, _) = tokio::sync::broadcast::channel(8);
Self {
sender: Rc::new(sender),
active_requests: SharedSlab::default(),
}
}
}
impl QueryEngine {
/// Creates a new query and returns a handle to it. The query will be resolved when the webview returns a result with the same id.
pub fn new_query<V: DeserializeOwned>(
&self,
script: &str,
tx: &UnboundedSender<String>,
) -> Query<V> {
let request_id = self.active_requests.slab.borrow_mut().insert(());
// start the query
// We embed the return of the eval in a function so we can send it back to the main thread
if let Err(err) = tx.send(format!(
r#"window.ipc.postMessage(
JSON.stringify({{
"method":"query",
"params": {{
"id": {request_id},
"data": (function(){{{script}}})()
}}
}})
);"#
)) {
log::warn!("Query error: {err}");
}
Query {
slab: self.active_requests.clone(),
id: request_id,
reciever: self.sender.subscribe(),
phantom: std::marker::PhantomData,
}
}
/// Send a query result
pub fn send(&self, data: QueryResult) {
let _ = self.sender.send(data);
}
}
pub(crate) struct Query<V: DeserializeOwned> {
slab: SharedSlab,
id: usize,
reciever: tokio::sync::broadcast::Receiver<QueryResult>,
phantom: std::marker::PhantomData<V>,
}
impl<V: DeserializeOwned> Query<V> {
/// Resolve the query
pub async fn resolve(mut self) -> Result<V, QueryError> {
let result = loop {
match self.reciever.recv().await {
Ok(result) => {
if result.id == self.id {
break V::deserialize(result.data).map_err(QueryError::DeserializeError);
}
}
Err(err) => {
break Err(QueryError::RecvError(err));
}
}
};
// Remove the query from the slab
self.slab.slab.borrow_mut().remove(self.id);
result
}
}
#[derive(Error, Debug)]
pub enum QueryError {
#[error("Error receiving query result: {0}")]
RecvError(RecvError),
#[error("Error deserializing query result: {0}")]
DeserializeError(serde_json::Error),
}
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct QueryResult {
id: usize,
data: Value,
}