implement a query engine for liveview
This commit is contained in:
1 parent
fa9f0d0f6c
commit
8243dfe00d
7 files changed
+323
-16
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
] }
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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(¶ms.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),
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
Reference in new issue
Block a user