Selaa lähdekoodia

chore: simplify liveview abstraction

Jonathan Kelley 2 vuotta sitten
vanhempi
commit
7790d2c065

+ 0 - 3
packages/core/src/virtual_dom.rs

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

+ 2 - 0
packages/html/src/transit.rs

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

+ 6 - 2
packages/interpreter/src/interpreter.js

@@ -345,7 +345,10 @@ class Interpreter {
         break;
       case "NewEventListener":
         // 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 target = event.target;
           if (target != null) {
@@ -430,11 +433,12 @@ class Interpreter {
                 name: edit.name,
                 element: parseInt(realId),
                 data: contents,
+                bubbles: bubbles,
               })
             );
           }
         };
-        this.NewEventListener(edit.name, edit.id, event_bubbles(edit.name), handler);
+        this.NewEventListener(edit.name, edit.id, bubbles, handler);
         break;
     }
   }

+ 3 - 1
packages/liveview/examples/warp.rs

@@ -45,7 +45,9 @@ async fn main() {
         .and(warp::any().map(move || view.clone()))
         .map(move |ws: Ws, view: LiveView| {
             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);

+ 33 - 54
packages/liveview/src/adapters/warp_adapter.rs

@@ -1,12 +1,14 @@
-use crate::{LiveView, LiveViewError};
+use crate::{liveview_eventloop, LiveView, LiveViewError};
 use dioxus_core::prelude::*;
-use dioxus_html::HtmlEvent;
 use futures_util::{SinkExt, StreamExt};
-use std::time::Duration;
 use warp::ws::{Message, WebSocket};
 
 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
     }
 
@@ -15,60 +17,37 @@ impl LiveView {
         ws: WebSocket,
         app: fn(Scope<T>) -> Element,
         props: T,
-    ) {
-        self.pool
-            .spawn_pinned(move || liveview_eventloop(app, props, ws))
-            .await;
+    ) -> Result<(), LiveViewError> {
+        let (ws_tx, ws_rx) = ws.split();
+
+        let ws_tx = ws_tx
+            .with(transform_warp)
+            .sink_map_err(|_| LiveViewError::SendingFailed);
+
+        let ws_rx = ws_rx.map(transform_warp_rx);
+
+        match self
+            .pool
+            .spawn_pinned(move || liveview_eventloop(app, props, ws_tx, ws_rx))
+            .await
+        {
+            Ok(Ok(_)) => Ok(()),
+            Ok(Err(e)) => Err(e),
+            Err(_) => Err(LiveViewError::SendingFailed),
+        }
     }
 }
 
-async fn liveview_eventloop<T>(
-    app: Component<T>,
-    props: T,
-    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 {
-        tokio::select! {
-            // poll any futures or suspense
-            _ = vdom.wait_for_work() => {}
+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();
 
-            evt = ws.next() => {
-                match evt {
-                    Some(Ok(evt)) => {
-                        if let Ok(evt) = evt.to_str() {
-                            // desktop uses this wrapper struct thing
-                            #[derive(serde::Deserialize)]
-                            struct IpcMessage {
-                                params: HtmlEvent
-                            }
+    // transform it back into a string, saving us the allocation
+    let msg = String::from_utf8(msg).map_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;
+    Ok(msg)
+}
 
-        ws.send(Message::text(serde_json::to_string(&edits).unwrap()))
-            .await?;
-    }
+async fn transform_warp(message: String) -> Result<Message, warp::Error> {
+    Ok(Message::text(message))
 }

+ 25 - 18
packages/liveview/src/lib.rs

@@ -1,19 +1,3 @@
-use dioxus_interpreter_js::INTERPRETER_JS;
-static MAIN_JS: &str = include_str!("./main.js");
-
-pub fn interpreter_glue(url: &str) -> String {
-    format!(
-        r#"
-<script>
-    var WS_ADDR = "{url}";
-    {INTERPRETER_JS}
-    {MAIN_JS}
-    main();
-</script>
-    "#
-    )
-}
-
 pub mod adapters {
     #[cfg(feature = "warp")]
     pub mod warp_adapter;
@@ -26,10 +10,33 @@ pub mod adapters {
 }
 
 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("Connection Failed")]
-    Warp(#[from] warp::Error),
+    #[error("warp error")]
+    SendingFailed,
+}
+
+use dioxus_interpreter_js::INTERPRETER_JS;
+static MAIN_JS: &str = include_str!("./main.js");
+
+pub fn interpreter_glue(url: &str) -> String {
+    format!(
+        r#"
+<script>
+    var WS_ADDR = "{url}";
+    {INTERPRETER_JS}
+    {MAIN_JS}
+    main();
+</script>
+    "#
+    )
 }

+ 2 - 1
packages/liveview/src/main.js

@@ -4,7 +4,7 @@ function main() {
   if (root != null) {
     // create a new ipc
     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) => {
+      console.log("Received message: ", event.data);
       let edits = JSON.parse(event.data);
       window.interpreter.handleEdits(edits);
     };

+ 68 - 0
packages/liveview/src/pool.rs

@@ -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;
 
 #[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?;
+    }
+}