chore: simplify liveview abstraction

This commit is contained in:
Jonathan Kelley 2022-12-15 18:46:59 -08:00
parent 44dde38c63
commit 7790d2c065
8 changed files with 142 additions and 82 deletions

View file

@ -280,9 +280,6 @@ impl VirtualDom {
/// Whenever the VirtualDom "works", it will re-render this scope /// Whenever the VirtualDom "works", it will re-render this scope
pub fn mark_dirty(&mut self, id: ScopeId) { pub fn mark_dirty(&mut self, id: ScopeId) {
let height = self.scopes[id.0].height; let height = self.scopes[id.0].height;
println!("marking scope {} dirty with height {}", id.0, height);
self.dirty_scopes.insert(DirtyScope { height, id }); self.dirty_scopes.insert(DirtyScope { height, id });
} }

View file

@ -27,6 +27,7 @@ pub struct HtmlEvent {
pub element: ElementId, pub element: ElementId,
pub name: String, pub name: String,
pub data: EventData, pub data: EventData,
pub bubbles: bool,
} }
impl HtmlEvent { impl HtmlEvent {
@ -85,6 +86,7 @@ fn test_back_and_forth() {
element: ElementId(0), element: ElementId(0),
data: EventData::Mouse(MouseData::default()), data: EventData::Mouse(MouseData::default()),
name: "click".to_string(), name: "click".to_string(),
bubbles: true,
}; };
println!("{}", serde_json::to_string_pretty(&data).unwrap()); println!("{}", serde_json::to_string_pretty(&data).unwrap());

View file

@ -345,7 +345,10 @@ class Interpreter {
break; break;
case "NewEventListener": case "NewEventListener":
// this handler is only provided on desktop implementations since this // this handler is only provided on desktop implementations since this
// method is not used by the web implementation // method is not used by the web implementationa
let bubbles = event_bubbles(edit.name);
let handler = (event) => { let handler = (event) => {
let target = event.target; let target = event.target;
if (target != null) { if (target != null) {
@ -430,11 +433,12 @@ class Interpreter {
name: edit.name, name: edit.name,
element: parseInt(realId), element: parseInt(realId),
data: contents, data: contents,
bubbles: bubbles,
}) })
); );
} }
}; };
this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler); this.NewEventListener(edit.name, edit.id, bubbles, handler);
break; break;
} }
} }

View file

@ -45,7 +45,9 @@ async fn main() {
.and(warp::any().map(move || view.clone())) .and(warp::any().map(move || view.clone()))
.map(move |ws: Ws, view: LiveView| { .map(move |ws: Ws, view: LiveView| {
println!("Got a connection!"); println!("Got a connection!");
ws.on_upgrade(|ws| view.upgrade_warp(ws, app)) ws.on_upgrade(|ws| async move {
let _ = view.upgrade_warp(ws, app).await;
})
}); });
println!("Listening on http://{}", addr); println!("Listening on http://{}", addr);

View file

@ -1,12 +1,14 @@
use crate::{LiveView, LiveViewError}; use crate::{liveview_eventloop, LiveView, LiveViewError};
use dioxus_core::prelude::*; use dioxus_core::prelude::*;
use dioxus_html::HtmlEvent;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use std::time::Duration;
use warp::ws::{Message, WebSocket}; use warp::ws::{Message, WebSocket};
impl LiveView { impl LiveView {
pub async fn upgrade_warp(self, ws: WebSocket, app: fn(Scope<()>) -> Element) { pub async fn upgrade_warp(
self,
ws: WebSocket,
app: fn(Scope<()>) -> Element,
) -> Result<(), LiveViewError> {
self.upgrade_warp_with_props(ws, app, ()).await self.upgrade_warp_with_props(ws, app, ()).await
} }
@ -15,60 +17,37 @@ impl LiveView {
ws: WebSocket, ws: WebSocket,
app: fn(Scope<T>) -> Element, app: fn(Scope<T>) -> Element,
props: T, props: T,
) { ) -> Result<(), LiveViewError> {
self.pool let (ws_tx, ws_rx) = ws.split();
.spawn_pinned(move || liveview_eventloop(app, props, ws))
.await;
}
}
async fn liveview_eventloop<T>( let ws_tx = ws_tx
app: Component<T>, .with(transform_warp)
props: T, .sink_map_err(|_| LiveViewError::SendingFailed);
mut ws: WebSocket,
) -> Result<(), LiveViewError>
where
T: Send + 'static,
{
let mut vdom = VirtualDom::new_with_props(app, props);
let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
ws.send(Message::text(edits)).await.unwrap();
loop { let ws_rx = ws_rx.map(transform_warp_rx);
tokio::select! {
// poll any futures or suspense
_ = vdom.wait_for_work() => {}
evt = ws.next() => { match self
match evt { .pool
Some(Ok(evt)) => { .spawn_pinned(move || liveview_eventloop(app, props, ws_tx, ws_rx))
if let Ok(evt) = evt.to_str() { .await
// desktop uses this wrapper struct thing {
#[derive(serde::Deserialize)] Ok(Ok(_)) => Ok(()),
struct IpcMessage { Ok(Err(e)) => Err(e),
params: HtmlEvent Err(_) => Err(LiveViewError::SendingFailed),
}
let event: IpcMessage = serde_json::from_str(evt).unwrap();
let event = event.params;
let bubbles = event.bubbles();
vdom.handle_event(&event.name, event.data.into_any(), event.element, bubbles);
}
}
Some(Err(_e)) => {
// log this I guess?
// when would we get an error here?
}
None => return Ok(()),
}
}
} }
let edits = vdom
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
.await;
ws.send(Message::text(serde_json::to_string(&edits).unwrap()))
.await?;
} }
} }
fn transform_warp_rx(f: Result<Message, warp::Error>) -> Result<String, LiveViewError> {
// destructure the message into the buffer we got from warp
let msg = f.map_err(|_| LiveViewError::SendingFailed)?.into_bytes();
// transform it back into a string, saving us the allocation
let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?;
Ok(msg)
}
async fn transform_warp(message: String) -> Result<Message, warp::Error> {
Ok(Message::text(message))
}

View file

@ -1,3 +1,30 @@
pub mod adapters {
#[cfg(feature = "warp")]
pub mod warp_adapter;
#[cfg(feature = "axum")]
pub mod axum_adapter;
#[cfg(feature = "salvo")]
pub mod salvo_adapter;
}
pub mod pool;
use futures_util::{SinkExt, StreamExt};
pub use pool::*;
pub trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}
impl<T> WebsocketTx for T where T: SinkExt<String, Error = LiveViewError> {}
pub trait WebsocketRx: StreamExt<Item = Result<String, LiveViewError>> {}
impl<T> WebsocketRx for T where T: StreamExt<Item = Result<String, LiveViewError>> {}
#[derive(Debug, thiserror::Error)]
pub enum LiveViewError {
#[error("warp error")]
SendingFailed,
}
use dioxus_interpreter_js::INTERPRETER_JS; use dioxus_interpreter_js::INTERPRETER_JS;
static MAIN_JS: &str = include_str!("./main.js"); static MAIN_JS: &str = include_str!("./main.js");
@ -13,23 +40,3 @@ pub fn interpreter_glue(url: &str) -> String {
"# "#
) )
} }
pub mod adapters {
#[cfg(feature = "warp")]
pub mod warp_adapter;
#[cfg(feature = "axum")]
pub mod axum_adapter;
#[cfg(feature = "salvo")]
pub mod salvo_adapter;
}
pub mod pool;
pub use pool::*;
#[derive(Debug, thiserror::Error)]
pub enum LiveViewError {
#[error("Connection Failed")]
Warp(#[from] warp::Error),
}

View file

@ -4,7 +4,7 @@ function main() {
if (root != null) { if (root != null) {
// create a new ipc // create a new ipc
window.ipc = new IPC(root); window.ipc = new IPC(root);
window.ipc.send(serializeIpcMessage("initialize")); window.ipc.postMessage(serializeIpcMessage("initialize"));
} }
} }
@ -24,6 +24,7 @@ class IPC {
}; };
this.ws.onmessage = (event) => { this.ws.onmessage = (event) => {
console.log("Received message: ", event.data);
let edits = JSON.parse(event.data); let edits = JSON.parse(event.data);
window.interpreter.handleEdits(edits); window.interpreter.handleEdits(edits);
}; };

View file

@ -1,3 +1,8 @@
use crate::LiveViewError;
use dioxus_core::prelude::*;
use dioxus_html::HtmlEvent;
use futures_util::{pin_mut, SinkExt, StreamExt};
use std::time::Duration;
use tokio_util::task::LocalPoolHandle; use tokio_util::task::LocalPoolHandle;
#[derive(Clone)] #[derive(Clone)]
@ -18,3 +23,66 @@ impl LiveView {
} }
} }
} }
/// The primary event loop for the VirtualDom waiting for user input
///
/// This function makes it easy to integrate Dioxus LiveView with any socket-based framework.
///
/// As long as your framework can provide a Sink and Stream of Strings, you can use this function.
///
/// You might need to transform the error types of the web backend into the LiveView error type.
pub async fn liveview_eventloop<T>(
app: Component<T>,
props: T,
ws_tx: impl SinkExt<String, Error = LiveViewError>,
ws_rx: impl StreamExt<Item = Result<String, LiveViewError>>,
) -> Result<(), LiveViewError>
where
T: Send + 'static,
{
let mut vdom = VirtualDom::new_with_props(app, props);
// todo: use an efficient binary packed format for this
let edits = serde_json::to_string(&vdom.rebuild()).unwrap();
// pin the futures so we can use select!
pin_mut!(ws_tx);
pin_mut!(ws_rx);
ws_tx.send(edits).await?;
// 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,
}
loop {
tokio::select! {
// poll any futures or suspense
_ = vdom.wait_for_work() => {}
evt = ws_rx.next() => {
match evt {
Some(Ok(evt)) => {
let event: IpcMessage = serde_json::from_str(&evt).unwrap();
let event = event.params;
vdom.handle_event(&event.name, event.data.into_any(), event.element, event.bubbles);
}
Some(Err(_e)) => {
// log this I guess?
// when would we get an error here?
}
None => return Ok(()),
}
}
}
let edits = vdom
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
.await;
ws_tx.send(serde_json::to_string(&edits).unwrap()).await?;
}
}