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 tokio::sync::broadcast::error::RecvError;
|
||||||
use wry::webview::WebView;
|
use wry::webview::WebView;
|
||||||
|
|
||||||
|
/// Tracks what query ids are currently active
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
struct SharedSlab {
|
struct SharedSlab {
|
||||||
slab: Rc<RefCell<Slab<()>>>,
|
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)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct QueryEngine {
|
pub(crate) struct QueryEngine {
|
||||||
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
|
sender: Rc<tokio::sync::broadcast::Sender<QueryResult>>,
|
||||||
@@ -29,6 +31,7 @@ impl Default for QueryEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
pub fn new_query<V: DeserializeOwned>(&self, script: &str, webview: &WebView) -> Query<V> {
|
||||||
let request_id = self.active_requests.slab.borrow_mut().insert(());
|
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) {
|
pub fn send(&self, data: QueryResult) {
|
||||||
let _ = self.sender.send(data);
|
let _ = self.sender.send(data);
|
||||||
}
|
}
|
||||||
@@ -69,6 +73,7 @@ pub(crate) struct Query<V: DeserializeOwned> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<V: DeserializeOwned> Query<V> {
|
impl<V: DeserializeOwned> Query<V> {
|
||||||
|
/// Resolve the query
|
||||||
pub async fn resolve(mut self) -> Result<V, QueryError> {
|
pub async fn resolve(mut self) -> Result<V, QueryError> {
|
||||||
let result = loop {
|
let result = loop {
|
||||||
match self.reciever.recv().await {
|
match self.reciever.recv().await {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ license = "MIT/Apache-2.0"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0.38"
|
thiserror = "1.0.38"
|
||||||
|
log = "0.4.14"
|
||||||
|
slab = "0.4"
|
||||||
futures-util = { version = "0.3.25", default-features = false, features = [
|
futures-util = { version = "0.3.25", default-features = false, features = [
|
||||||
"sink",
|
"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::*;
|
pub use adapters::*;
|
||||||
|
|
||||||
|
mod element;
|
||||||
pub mod pool;
|
pub mod pool;
|
||||||
|
mod query;
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
pub use pool::*;
|
pub use pool::*;
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,19 @@ class IPC {
|
|||||||
// todo: retry the connection
|
// todo: retry the connection
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (message) => {
|
||||||
// Ignore pongs
|
// Ignore pongs
|
||||||
if (event.data != "__pong__") {
|
if (message.data != "__pong__") {
|
||||||
let edits = JSON.parse(event.data);
|
const event = JSON.parse(message.data);
|
||||||
window.interpreter.handleEdits(edits);
|
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 crate::{
|
||||||
use dioxus_core::prelude::*;
|
element::LiveviewElement,
|
||||||
use dioxus_html::HtmlEvent;
|
query::{QueryEngine, QueryResult},
|
||||||
|
LiveViewError,
|
||||||
|
};
|
||||||
|
use dioxus_core::{prelude::*, Mutations};
|
||||||
|
use dioxus_html::{EventData, HtmlEvent, MountedData};
|
||||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
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;
|
use tokio_util::task::LocalPoolHandle;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[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
|
// 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 the futures so we can use select!
|
||||||
pin_mut!(ws);
|
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
|
// send the initial render to the client
|
||||||
ws.send(edits).await?;
|
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
|
// desktop uses this wrapper struct thing around the actual event itself
|
||||||
// this is sorta driven by tao/wry
|
// this is sorta driven by tao/wry
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize, Debug)]
|
||||||
struct IpcMessage {
|
#[serde(tag = "method", content = "params")]
|
||||||
params: HtmlEvent,
|
enum IpcMessage {
|
||||||
|
#[serde(rename = "user_event")]
|
||||||
|
Event(HtmlEvent),
|
||||||
|
#[serde(rename = "query")]
|
||||||
|
Query(QueryResult),
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -147,16 +160,45 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
|||||||
ws.send("__pong__".to_string()).await?;
|
ws.send("__pong__".to_string()).await?;
|
||||||
}
|
}
|
||||||
Some(Ok(evt)) => {
|
Some(Ok(evt)) => {
|
||||||
if let Ok(IpcMessage { params }) = serde_json::from_str::<IpcMessage>(evt) {
|
if let Ok(message) = serde_json::from_str::<IpcMessage>(evt) {
|
||||||
vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles);
|
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?
|
// log this I guess? when would we get an error here?
|
||||||
Some(Err(_e)) => {},
|
Some(Err(_e)) => {}
|
||||||
None => return Ok(()),
|
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 => {
|
Some(msg) = hot_reload_wait => {
|
||||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
match msg{
|
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)))
|
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
|
||||||
.await;
|
.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