From aa71c111cdfc853b2168e8fcb3509f7969eb93a0 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 19 Dec 2022 13:11:49 -0800 Subject: [PATCH] feat: introduce simpler adapters for liveview --- packages/liveview/Cargo.toml | 20 +- packages/liveview/examples/axum.rs | 49 +- packages/liveview/examples/salvo.rs | 99 +- packages/liveview/examples/warp.rs | 75 +- .../liveview/src/adapters/axum_adapter.rs | 107 +- .../liveview/src/adapters/salvo_adapter.rs | 119 +-- .../liveview/src/adapters/warp_adapter.rs | 122 +-- packages/liveview/src/events.rs | 207 ---- packages/liveview/src/interpreter.js | 973 ------------------ packages/liveview/src/lib.rs | 77 +- packages/liveview/src/main.js | 36 + packages/liveview/src/pool.rs | 123 +++ 12 files changed, 409 insertions(+), 1598 deletions(-) delete mode 100644 packages/liveview/src/events.rs delete mode 100644 packages/liveview/src/interpreter.js create mode 100644 packages/liveview/src/main.js create mode 100644 packages/liveview/src/pool.rs diff --git a/packages/liveview/Cargo.toml b/packages/liveview/Cargo.toml index 90f956289..ee75f71de 100644 --- a/packages/liveview/Cargo.toml +++ b/packages/liveview/Cargo.toml @@ -10,7 +10,6 @@ description = "Build server-side apps with Dioxus" license = "MIT/Apache-2.0" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -28,7 +27,7 @@ tokio-util = { version = "0.7.0", features = ["full"] } dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" } dioxus-core = { path = "../core", features = ["serialize"], version = "^0.2.1" } - +dioxus-interpreter-js = { path = "../interpreter" } # warp warp = { version = "0.3", optional = true } @@ -39,6 +38,9 @@ tower = { version = "0.4.12", optional = true } # salvo salvo = { version = "0.32.0", optional = true, features = ["ws"] } +thiserror = "1.0.37" +uuid = { version = "1.2.2", features = ["v4"] } +anyhow = "1.0.66" [dev-dependencies] tokio = { version = "1", features = ["full"] } @@ -49,4 +51,16 @@ salvo = { version = "0.32.0", features = ["affix", "ws"] } tower = "0.4.12" [features] -default = [] \ No newline at end of file +default = ["salvo"] + +[[example]] +name = "axum" +required-features = ["axum"] + +[[example]] +name = "salvo" +required-features = ["salvo"] + +[[example]] +name = "warp" +required-features = ["warp"] diff --git a/packages/liveview/examples/axum.rs b/packages/liveview/examples/axum.rs index 450af30ef..410c0e9a4 100644 --- a/packages/liveview/examples/axum.rs +++ b/packages/liveview/examples/axum.rs @@ -1,32 +1,53 @@ -#[cfg(not(feature = "axum"))] -fn main() {} +use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; +use dioxus::prelude::*; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello axum! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} -#[cfg(feature = "axum")] #[tokio::main] async fn main() { - use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router}; - use dioxus_core::{Element, LazyNodes, Scope}; pretty_env_logger::init(); - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - let addr: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into(); - let view = dioxus_liveview::new(addr); - let body = view.body("Dioxus Liveview"); + let view = dioxus_liveview::LiveViewPool::new(); let app = Router::new() - .route("/", get(move || async { Html(body) })) .route( - "/app", + "/", + get(move || async move { + Html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + )) + }), + ) + .route( + "/ws", get(move |ws: WebSocketUpgrade| async move { ws.on_upgrade(move |socket| async move { - view.upgrade_axum(socket, app).await; + _ = view.launch(dioxus_liveview::axum_socket(socket), app).await; }) }), ); + + println!("Listening on http://{}", addr); + axum::Server::bind(&addr.to_string().parse().unwrap()) .serve(app.into_make_service()) .await diff --git a/packages/liveview/examples/salvo.rs b/packages/liveview/examples/salvo.rs index 132a62633..cfe7a7ffd 100644 --- a/packages/liveview/examples/salvo.rs +++ b/packages/liveview/examples/salvo.rs @@ -1,55 +1,70 @@ -#[cfg(not(feature = "salvo"))] -fn main() {} +use dioxus::prelude::*; +use dioxus_liveview::LiveViewPool; +use salvo::extra::affix; +use salvo::extra::ws::WsHandler; +use salvo::prelude::*; +use std::net::SocketAddr; +use std::sync::Arc; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello salvo! {num}" + button { onclick: move |_| num += 1, "Increment" } + } + }) +} -#[cfg(feature = "salvo")] #[tokio::main] async fn main() { - use std::sync::Arc; - - use dioxus_core::{Element, LazyNodes, Scope}; - use dioxus_liveview as liveview; - use dioxus_liveview::Liveview; - use salvo::extra::affix; - use salvo::extra::ws::WsHandler; - use salvo::prelude::*; - - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); + + let view = LiveViewPool::new(); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); let router = Router::new() .hoop(affix::inject(Arc::new(view))) .get(index) - .push(Router::with_path("app").get(connect)); + .push(Router::with_path("ws").get(connect)); + + println!("Listening on http://{}", addr); + Server::new(TcpListener::bind(addr)).serve(router).await; +} - #[handler] - fn index(depot: &mut Depot, res: &mut Response) { - let view = depot.obtain::>().unwrap(); - let body = view.body("Dioxus LiveView"); - res.render(Text::Html(body)); - } +#[handler] +fn index(_depot: &mut Depot, res: &mut Response) { + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); + res.render(Text::Html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws")) + ))); +} - #[handler] - async fn connect( - req: &mut Request, - depot: &mut Depot, - res: &mut Response, - ) -> Result<(), StatusError> { - let view = depot.obtain::>().unwrap().clone(); - let fut = WsHandler::new().handle(req, res)?; - let fut = async move { - if let Some(ws) = fut.await { - view.upgrade_salvo(ws, app).await; - } - }; - tokio::task::spawn(fut); - Ok(()) - } +#[handler] +async fn connect( + req: &mut Request, + depot: &mut Depot, + res: &mut Response, +) -> Result<(), StatusError> { + let view = depot.obtain::>().unwrap().clone(); + let fut = WsHandler::new().handle(req, res)?; + + tokio::spawn(async move { + if let Some(ws) = fut.await { + _ = view.launch(dioxus_liveview::salvo_socket(ws), app).await; + } + }); + + Ok(()) } diff --git a/packages/liveview/examples/warp.rs b/packages/liveview/examples/warp.rs index 7b2ddfef5..7e759ea90 100644 --- a/packages/liveview/examples/warp.rs +++ b/packages/liveview/examples/warp.rs @@ -1,35 +1,56 @@ -#[cfg(not(feature = "warp"))] -fn main() {} +use dioxus::prelude::*; +use dioxus_liveview::adapters::warp_adapter::warp_socket; +use dioxus_liveview::LiveViewPool; +use std::net::SocketAddr; +use warp::ws::Ws; +use warp::Filter; + +fn app(cx: Scope) -> Element { + let mut num = use_state(cx, || 0); + + cx.render(rsx! { + div { + "hello warp! {num}" + button { + onclick: move |_| num += 1, + "Increment" + } + } + }) +} -#[cfg(feature = "warp")] #[tokio::main] async fn main() { - use dioxus_core::{Element, LazyNodes, Scope}; - use dioxus_liveview as liveview; - use warp::ws::Ws; - use warp::Filter; - - fn app(cx: Scope) -> Element { - cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) - } - pretty_env_logger::init(); - let addr = ([127, 0, 0, 1], 3030); + let addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); - // todo: compactify this routing under one liveview::app method - let view = liveview::new(addr); - let body = view.body("Dioxus LiveView"); + let index = warp::path::end().map(move || { + warp::reply::html(format!( + r#" + + + Dioxus LiveView with Warp +
+ {glue} + + "#, + glue = dioxus_liveview::interpreter_glue(&format!("ws://{addr}/ws/")) + )) + }); - let routes = warp::path::end() - .map(move || warp::reply::html(body.clone())) - .or(warp::path("app") - .and(warp::ws()) - .and(warp::any().map(move || view.clone())) - .map(|ws: Ws, view: liveview::Liveview| { - ws.on_upgrade(|socket| async move { - view.upgrade_warp(socket, app).await; - }) - })); - warp::serve(routes).run(addr).await; + let pool = LiveViewPool::new(); + + let ws = warp::path("ws") + .and(warp::ws()) + .and(warp::any().map(move || pool.clone())) + .map(move |ws: Ws, pool: LiveViewPool| { + ws.on_upgrade(|ws| async move { + let _ = pool.launch(warp_socket(ws), app).await; + }) + }); + + println!("Listening on http://{}", addr); + + warp::serve(index.or(ws)).run(addr).await; } diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index c60b267ea..646cde38e 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -1,94 +1,23 @@ -use crate::events; +use crate::{LiveViewError, LiveViewSocket}; use axum::extract::ws::{Message, WebSocket}; -use dioxus_core::prelude::*; -use futures_util::{ - future::{select, Either}, - pin_mut, SinkExt, StreamExt, -}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; +use futures_util::{SinkExt, StreamExt}; -impl crate::Liveview { - pub async fn upgrade_axum(&self, ws: WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - - pub async fn upgrade_axum_with_props( - &self, - ws: WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - socket: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - let (mut user_ws_tx, mut user_ws_rx) = socket.split(); - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - let vdom_fut = pool.clone().spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - let edits = vdom.rebuild(); - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - loop { - let new_event = { - let vdom_fut = vdom.wait_for_work(); - pin_mut!(vdom_fut); - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - loop { - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_text().map(events::parse_ipc_message) { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::Text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - vdom_fut.abort(); +fn transform_rx(message: Result) -> Result { + message + .map_err(|_| LiveViewError::SendingFailed)? + .into_text() + .map_err(|_| LiveViewError::SendingFailed) +} + +async fn transform_tx(message: String) -> Result { + Ok(Message::Text(message)) } diff --git a/packages/liveview/src/adapters/salvo_adapter.rs b/packages/liveview/src/adapters/salvo_adapter.rs index 8a28784b8..19cb528c7 100644 --- a/packages/liveview/src/adapters/salvo_adapter.rs +++ b/packages/liveview/src/adapters/salvo_adapter.rs @@ -1,110 +1,25 @@ -use crate::events; -use dioxus_core::prelude::*; -use futures_util::{pin_mut, SinkExt, StreamExt}; +use futures_util::{SinkExt, StreamExt}; use salvo::extra::ws::{Message, WebSocket}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; -impl crate::Liveview { - pub async fn upgrade_salvo(&self, ws: salvo::extra::ws::WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - pub async fn upgrade_salvo_with_props( - &self, - ws: salvo::extra::ws::WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +use crate::{LiveViewError, LiveViewSocket}; + +/// Convert a salvo websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - ws: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - // Use a counter to assign a new unique ID for this user. +fn transform_rx(message: Result) -> Result { + let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?; - // Split the socket into a sender and receive of messages. - let (mut user_ws_tx, mut user_ws_rx) = ws.split(); + let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?; - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); + Ok(msg) +} - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - - let vdom_fut = pool.spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - - let edits = vdom.rebuild(); - - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - - loop { - use futures_util::future::{select, Either}; - - let new_event = { - let vdom_fut = vdom.wait_for_work(); - - pin_mut!(vdom_fut); - - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - - loop { - use futures_util::future::{select, Either}; - - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - if msg.method == "user_event" { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - - vdom_fut.abort(); +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) } diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index 398c7cdd9..e5c821ce3 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -1,110 +1,28 @@ -use crate::events; -use dioxus_core::prelude::*; -use futures_util::{pin_mut, SinkExt, StreamExt}; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; -use tokio_util::task::LocalPoolHandle; +use crate::{LiveViewError, LiveViewSocket}; +use futures_util::{SinkExt, StreamExt}; use warp::ws::{Message, WebSocket}; -impl crate::Liveview { - pub async fn upgrade_warp(&self, ws: warp::ws::WebSocket, app: fn(Scope) -> Element) { - connect(ws, self.pool.clone(), app, ()).await; - } - pub async fn upgrade_warp_with_props( - &self, - ws: warp::ws::WebSocket, - app: fn(Scope) -> Element, - props: T, - ) where - T: Send + Sync + 'static, - { - connect(ws, self.pool.clone(), app, props).await; - } +/// Convert a warp websocket into a LiveViewSocket +/// +/// This is required to launch a LiveView app using the warp web framework +pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket { + ws.map(transform_rx) + .with(transform_tx) + .sink_map_err(|_| LiveViewError::SendingFailed) } -pub async fn connect( - ws: WebSocket, - pool: LocalPoolHandle, - app: fn(Scope) -> Element, - props: T, -) where - T: Send + Sync + 'static, -{ - // Use a counter to assign a new unique ID for this user. +fn transform_rx(message: Result) -> Result { + // destructure the message into the buffer we got from warp + let msg = message + .map_err(|_| LiveViewError::SendingFailed)? + .into_bytes(); - // Split the socket into a sender and receive of messages. - let (mut user_ws_tx, mut user_ws_rx) = ws.split(); + // transform it back into a string, saving us the allocation + let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?; - let (event_tx, event_rx) = mpsc::unbounded_channel(); - let (edits_tx, edits_rx) = mpsc::unbounded_channel(); + Ok(msg) +} - let mut edits_rx = UnboundedReceiverStream::new(edits_rx); - let mut event_rx = UnboundedReceiverStream::new(event_rx); - - let vdom_fut = pool.spawn_pinned(move || async move { - let mut vdom = VirtualDom::new_with_props(app, props); - - let edits = vdom.rebuild(); - - let serialized = serde_json::to_string(&edits.edits).unwrap(); - edits_tx.send(serialized).unwrap(); - - loop { - use futures_util::future::{select, Either}; - - let new_event = { - let vdom_fut = vdom.wait_for_work(); - - pin_mut!(vdom_fut); - - match select(event_rx.next(), vdom_fut).await { - Either::Left((l, _)) => l, - Either::Right((_, _)) => None, - } - }; - - if let Some(new_event) = new_event { - vdom.handle_message(dioxus_core::SchedulerMsg::Event(new_event)); - } else { - let mutations = vdom.work_with_deadline(|| false); - for mutation in mutations { - let edits = serde_json::to_string(&mutation.edits).unwrap(); - edits_tx.send(edits).unwrap(); - } - } - } - }); - - loop { - use futures_util::future::{select, Either}; - - match select(user_ws_rx.next(), edits_rx.next()).await { - Either::Left((l, _)) => { - if let Some(Ok(msg)) = l { - if let Ok(Some(msg)) = msg.to_str().map(events::parse_ipc_message) { - if msg.method == "user_event" { - let user_event = events::trigger_from_serialized(msg.params); - event_tx.send(user_event).unwrap(); - } - } else { - break; - } - } else { - break; - } - } - Either::Right((edits, _)) => { - if let Some(edits) = edits { - // send the edits to the client - if user_ws_tx.send(Message::text(edits)).await.is_err() { - break; - } - } else { - break; - } - } - } - } - - vdom_fut.abort(); +async fn transform_tx(message: String) -> Result { + Ok(Message::text(message)) } diff --git a/packages/liveview/src/events.rs b/packages/liveview/src/events.rs deleted file mode 100644 index 2c5f97cc7..000000000 --- a/packages/liveview/src/events.rs +++ /dev/null @@ -1,207 +0,0 @@ -#![allow(dead_code)] - -//! Convert a serialized event to an event trigger - -use std::any::Any; -use std::sync::Arc; - -use dioxus_core::ElementId; -// use dioxus_html::event_bubbles; -use dioxus_html::events::*; - -#[derive(serde::Serialize, serde::Deserialize)] -pub(crate) struct IpcMessage { - pub method: String, - pub params: serde_json::Value, -} - -pub(crate) fn parse_ipc_message(payload: &str) -> Option { - match serde_json::from_str(payload) { - Ok(message) => Some(message), - Err(_) => None, - } -} - -#[derive(serde::Serialize, serde::Deserialize)] -struct ImEvent { - event: String, - mounted_dom_id: ElementId, - contents: serde_json::Value, -} - -pub fn trigger_from_serialized(_val: serde_json::Value) { - todo!() - // let ImEvent { - // event, - // mounted_dom_id, - // contents, - // } = serde_json::from_value(val).unwrap(); - - // let mounted_dom_id = Some(mounted_dom_id); - - // let name = event_name_from_type(&event); - // let event = make_synthetic_event(&event, contents); - - // UserEvent { - // name, - // scope_id: None, - // element: mounted_dom_id, - // data: event, - // bubbles: event_bubbles(name), - // } -} - -fn make_synthetic_event(name: &str, val: serde_json::Value) -> Arc { - match name { - "copy" | "cut" | "paste" => { - // - Arc::new(ClipboardData {}) - } - "compositionend" | "compositionstart" | "compositionupdate" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "keydown" | "keypress" | "keyup" => { - let evt = serde_json::from_value::(val).unwrap(); - Arc::new(evt) - } - "focus" | "blur" | "focusout" | "focusin" => { - // - Arc::new(FocusData {}) - } - - // todo: these handlers might get really slow if the input box gets large and allocation pressure is heavy - // don't have a good solution with the serialized event problem - "change" | "input" | "invalid" | "reset" | "submit" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" - | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" - | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" - | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - "select" => { - // - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "touchcancel" | "touchend" | "touchmove" | "touchstart" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "scroll" => Arc::new(()), - - "wheel" => Arc::new(serde_json::from_value::(val).unwrap()), - - "animationstart" | "animationend" | "animationiteration" => { - Arc::new(serde_json::from_value::(val).unwrap()) - } - - "transitionend" => Arc::new(serde_json::from_value::(val).unwrap()), - - "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" - | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" - | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" - | "timeupdate" | "volumechange" | "waiting" => { - // - Arc::new(MediaData {}) - } - - "toggle" => Arc::new(ToggleData {}), - - _ => Arc::new(()), - } -} - -fn event_name_from_type(typ: &str) -> &'static str { - match typ { - "copy" => "copy", - "cut" => "cut", - "paste" => "paste", - "compositionend" => "compositionend", - "compositionstart" => "compositionstart", - "compositionupdate" => "compositionupdate", - "keydown" => "keydown", - "keypress" => "keypress", - "keyup" => "keyup", - "focus" => "focus", - "focusout" => "focusout", - "focusin" => "focusin", - "blur" => "blur", - "change" => "change", - "input" => "input", - "invalid" => "invalid", - "reset" => "reset", - "submit" => "submit", - "click" => "click", - "contextmenu" => "contextmenu", - "doubleclick" => "doubleclick", - "drag" => "drag", - "dragend" => "dragend", - "dragenter" => "dragenter", - "dragexit" => "dragexit", - "dragleave" => "dragleave", - "dragover" => "dragover", - "dragstart" => "dragstart", - "drop" => "drop", - "mousedown" => "mousedown", - "mouseenter" => "mouseenter", - "mouseleave" => "mouseleave", - "mousemove" => "mousemove", - "mouseout" => "mouseout", - "mouseover" => "mouseover", - "mouseup" => "mouseup", - "pointerdown" => "pointerdown", - "pointermove" => "pointermove", - "pointerup" => "pointerup", - "pointercancel" => "pointercancel", - "gotpointercapture" => "gotpointercapture", - "lostpointercapture" => "lostpointercapture", - "pointerenter" => "pointerenter", - "pointerleave" => "pointerleave", - "pointerover" => "pointerover", - "pointerout" => "pointerout", - "select" => "select", - "touchcancel" => "touchcancel", - "touchend" => "touchend", - "touchmove" => "touchmove", - "touchstart" => "touchstart", - "scroll" => "scroll", - "wheel" => "wheel", - "animationstart" => "animationstart", - "animationend" => "animationend", - "animationiteration" => "animationiteration", - "transitionend" => "transitionend", - "abort" => "abort", - "canplay" => "canplay", - "canplaythrough" => "canplaythrough", - "durationchange" => "durationchange", - "emptied" => "emptied", - "encrypted" => "encrypted", - "ended" => "ended", - "error" => "error", - "loadeddata" => "loadeddata", - "loadedmetadata" => "loadedmetadata", - "loadstart" => "loadstart", - "pause" => "pause", - "play" => "play", - "playing" => "playing", - "progress" => "progress", - "ratechange" => "ratechange", - "seeked" => "seeked", - "seeking" => "seeking", - "stalled" => "stalled", - "suspend" => "suspend", - "timeupdate" => "timeupdate", - "volumechange" => "volumechange", - "waiting" => "waiting", - "toggle" => "toggle", - _ => { - panic!("unsupported event type") - } - } -} diff --git a/packages/liveview/src/interpreter.js b/packages/liveview/src/interpreter.js deleted file mode 100644 index 7e99745f2..000000000 --- a/packages/liveview/src/interpreter.js +++ /dev/null @@ -1,973 +0,0 @@ -function main() { - let root = window.document.getElementById("main"); - - if (root != null) { - // create a new ipc - window.ipc = new IPC(root); - - window.ipc.send(serializeIpcMessage("initialize")); - } -} - -class IPC { - constructor(root) { - // connect to the websocket - window.interpreter = new Interpreter(root); - - this.ws = new WebSocket(WS_ADDR); - - this.ws.onopen = () => { - console.log("Connected to the websocket"); - }; - - this.ws.onerror = (err) => { - console.error("Error: ", err); - }; - - this.ws.onmessage = (event) => { - let edits = JSON.parse(event.data); - window.interpreter.handleEdits(edits); - }; - } - - send(msg) { - this.ws.send(msg); - } -} - -class ListenerMap { - constructor(root) { - // bubbling events can listen at the root element - this.global = {}; - // non bubbling events listen at the element the listener was created at - this.local = {}; - this.root = root; - } - - create(event_name, element, handler, bubbles) { - if (bubbles) { - if (this.global[event_name] === undefined) { - this.global[event_name] = {}; - this.global[event_name].active = 1; - this.global[event_name].callback = handler; - this.root.addEventListener(event_name, handler); - } else { - this.global[event_name].active++; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - if (!this.local[id]) { - this.local[id] = {}; - } - this.local[id][event_name] = handler; - element.addEventListener(event_name, handler); - } - } - - remove(element, event_name, bubbles) { - if (bubbles) { - this.global[event_name].active--; - if (this.global[event_name].active === 0) { - this.root.removeEventListener(event_name, this.global[event_name].callback); - delete this.global[event_name]; - } - } - else { - const id = element.getAttribute("data-dioxus-id"); - delete this.local[id][event_name]; - if (this.local[id].length === 0) { - delete this.local[id]; - } - element.removeEventListener(event_name, handler); - } - } -} - -class Interpreter { - constructor(root) { - this.root = root; - this.lastNode = root; - this.listeners = new ListenerMap(root); - this.handlers = {}; - this.nodes = [root]; - this.parents = []; - } - checkAppendParent() { - if (this.parents.length > 0) { - const lastParent = this.parents[this.parents.length - 1]; - lastParent[1]--; - if (lastParent[1] === 0) { - this.parents.pop(); - } - lastParent[0].appendChild(this.lastNode); - } - } - AppendChildren(root, children) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - for (let i = 0; i < children.length; i++) { - node.appendChild(this.nodes[children[i]]); - } - } - ReplaceWith(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.replaceWith(...els); - } - InsertAfter(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.after(...els); - } - InsertBefore(root, nodes) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - let els = []; - for (let i = 0; i < nodes.length; i++) { - els.push(this.nodes[nodes[i]]) - } - node.before(...els); - } - Remove(root) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (node !== undefined) { - node.remove(); - } - } - CreateTextNode(text, root) { - this.lastNode = document.createTextNode(text); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - } - CreateElement(tag, root, children) { - this.lastNode = document.createElement(tag); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - if (children > 0) { - this.parents.push([this.lastNode, children]); - } - } - CreateElementNs(tag, root, ns, children) { - this.lastNode = document.createElementNS(ns, tag); - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - if (children > 0) { - this.parents.push([this.lastNode, children]); - } - } - CreatePlaceholder(root) { - this.lastNode = document.createElement("pre"); - this.lastNode.hidden = true; - this.checkAppendParent(); - if (root != null) { - this.nodes[root] = this.lastNode; - } - } - NewEventListener(event_name, root, handler, bubbles) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.setAttribute("data-dioxus-id", `${root}`); - this.listeners.create(event_name, node, handler, bubbles); - } - RemoveEventListener(root, event_name, bubbles) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.removeAttribute(`data-dioxus-id`); - this.listeners.remove(node, event_name, bubbles); - } - SetText(root, text) { - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - node.data = text; - } - SetAttribute(root, field, value, ns) { - const name = field; - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (ns === "style") { - // @ts-ignore - node.style[name] = value; - } else if (ns != null || ns != undefined) { - node.setAttributeNS(ns, name, value); - } else { - switch (name) { - case "value": - if (value !== node.value) { - node.value = value; - } - break; - case "checked": - node.checked = value === "true"; - break; - case "selected": - node.selected = value === "true"; - break; - case "dangerous_inner_html": - node.innerHTML = value; - break; - default: - // https://github.com/facebook/react/blob/8b88ac2592c5f555f315f9440cbb665dd1e7457a/packages/react-dom/src/shared/DOMProperty.js#L352-L364 - if (value === "false" && bool_attrs.hasOwnProperty(name)) { - node.removeAttribute(name); - } else { - node.setAttribute(name, value); - } - } - } - } - RemoveAttribute(root, field, ns) { - const name = field; - let node; - if (root == null) { - node = this.lastNode; - } else { - node = this.nodes[root]; - } - if (ns == "style") { - node.style.removeProperty(name); - } else if (ns !== null || ns !== undefined) { - node.removeAttributeNS(ns, name); - } else if (name === "value") { - node.value = ""; - } else if (name === "checked") { - node.checked = false; - } else if (name === "selected") { - node.selected = false; - } else if (name === "dangerous_inner_html") { - node.innerHTML = ""; - } else { - node.removeAttribute(name); - } - } - CloneNode(old, new_id) { - let node; - if (old === null) { - node = this.lastNode; - } else { - node = this.nodes[old]; - } - this.nodes[new_id] = node.cloneNode(true); - } - CloneNodeChildren(old, new_ids) { - let node; - if (old === null) { - node = this.lastNode; - } else { - node = this.nodes[old]; - } - const old_node = node.cloneNode(true); - let i = 0; - for (let node = old_node.firstChild; i < new_ids.length; node = node.nextSibling) { - this.nodes[new_ids[i++]] = node; - } - } - FirstChild() { - this.lastNode = this.lastNode.firstChild; - } - NextSibling() { - this.lastNode = this.lastNode.nextSibling; - } - ParentNode() { - this.lastNode = this.lastNode.parentNode; - } - StoreWithId(id) { - this.nodes[id] = this.lastNode; - } - SetLastNode(root) { - this.lastNode = this.nodes[root]; - } - handleEdits(edits) { - for (let edit of edits) { - this.handleEdit(edit); - } - } - handleEdit(edit) { - switch (edit.type) { - case "PushRoot": - this.PushRoot(edit.root); - break; - case "AppendChildren": - this.AppendChildren(edit.root, edit.children); - break; - case "ReplaceWith": - this.ReplaceWith(edit.root, edit.nodes); - break; - case "InsertAfter": - this.InsertAfter(edit.root, edit.nodes); - break; - case "InsertBefore": - this.InsertBefore(edit.root, edit.nodes); - break; - case "Remove": - this.Remove(edit.root); - break; - case "CreateTextNode": - this.CreateTextNode(edit.text, edit.root); - break; - case "CreateElement": - this.CreateElement(edit.tag, edit.root, edit.children); - break; - case "CreateElementNs": - this.CreateElementNs(edit.tag, edit.root, edit.ns, edit.children); - break; - case "CreatePlaceholder": - this.CreatePlaceholder(edit.root); - break; - case "RemoveEventListener": - this.RemoveEventListener(edit.root, edit.event_name); - break; - case "NewEventListener": - // this handler is only provided on desktop implementations since this - // method is not used by the web implementation - let handler = (event) => { - let target = event.target; - if (target != null) { - let realId = target.getAttribute(`data-dioxus-id`); - let shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - if (event.type === "click") { - // todo call prevent default if it's the right type of event - if (shouldPreventDefault !== `onclick`) { - if (target.tagName === "A") { - event.preventDefault(); - const href = target.getAttribute("href"); - if (href !== "" && href !== null && href !== undefined) { - window.ipc.postMessage( - serializeIpcMessage("browser_open", { href }) - ); - } - } - } - - // also prevent buttons from submitting - if (target.tagName === "BUTTON" && event.type == "submit") { - event.preventDefault(); - } - } - // walk the tree to find the real element - while (realId == null) { - // we've reached the root we don't want to send an event - if (target.parentElement === null) { - return; - } - - target = target.parentElement; - realId = target.getAttribute(`data-dioxus-id`); - } - - shouldPreventDefault = target.getAttribute( - `dioxus-prevent-default` - ); - - let contents = serialize_event(event); - - if (shouldPreventDefault === `on${event.type}`) { - event.preventDefault(); - } - - if (event.type === "submit") { - event.preventDefault(); - } - - if ( - target.tagName === "FORM" && - (event.type === "submit" || event.type === "input") - ) { - for (let x = 0; x < target.elements.length; x++) { - let element = target.elements[x]; - let name = element.getAttribute("name"); - if (name != null) { - if (element.getAttribute("type") === "checkbox") { - // @ts-ignore - contents.values[name] = element.checked ? "true" : "false"; - } else if (element.getAttribute("type") === "radio") { - if (element.checked) { - contents.values[name] = element.value; - } - } else { - // @ts-ignore - contents.values[name] = - element.value ?? element.textContent; - } - } - } - } - - if (realId === null) { - return; - } - realId = parseInt(realId); - window.ipc.send( - serializeIpcMessage("user_event", { - event: edit.event_name, - mounted_dom_id: realId, - contents: contents, - }) - ); - } - }; - this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name)); - - break; - case "SetText": - this.SetText(edit.root, edit.text); - break; - case "SetAttribute": - this.SetAttribute(edit.root, edit.field, edit.value, edit.ns); - break; - case "RemoveAttribute": - this.RemoveAttribute(edit.root, edit.name, edit.ns); - break; - case "CloneNode": - this.CloneNode(edit.id, edit.new_id); - break; - case "CloneNodeChildren": - this.CloneNodeChildren(edit.id, edit.new_ids); - break; - case "FirstChild": - this.FirstChild(); - break; - case "NextSibling": - this.NextSibling(); - break; - case "ParentNode": - this.ParentNode(); - break; - case "StoreWithId": - this.StoreWithId(BigInt(edit.id)); - break; - case "SetLastNode": - this.SetLastNode(BigInt(edit.id)); - break; - } - } -} - -function serialize_event(event) { - switch (event.type) { - case "copy": - case "cut": - case "past": { - return {}; - } - case "compositionend": - case "compositionstart": - case "compositionupdate": { - let { data } = event; - return { - data, - }; - } - case "keydown": - case "keypress": - case "keyup": { - let { - charCode, - key, - altKey, - ctrlKey, - metaKey, - keyCode, - shiftKey, - location, - repeat, - which, - code, - } = event; - return { - char_code: charCode, - key: key, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - key_code: keyCode, - shift_key: shiftKey, - location: location, - repeat: repeat, - which: which, - code, - }; - } - case "focus": - case "blur": { - return {}; - } - case "change": { - let target = event.target; - let value; - if (target.type === "checkbox" || target.type === "radio") { - value = target.checked ? "true" : "false"; - } else { - value = target.value ?? target.textContent; - } - return { - value: value, - values: {}, - }; - } - case "input": - case "invalid": - case "reset": - case "submit": { - let target = event.target; - let value = target.value ?? target.textContent; - - if (target.type === "checkbox") { - value = target.checked ? "true" : "false"; - } - - return { - value: value, - values: {}, - }; - } - case "click": - case "contextmenu": - case "doubleclick": - case "dblclick": - case "drag": - case "dragend": - case "dragenter": - case "dragexit": - case "dragleave": - case "dragover": - case "dragstart": - case "drop": - case "mousedown": - case "mouseenter": - case "mouseleave": - case "mousemove": - case "mouseout": - case "mouseover": - case "mouseup": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - offsetX, - offsetY, - pageX, - pageY, - screenX, - screenY, - shiftKey, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - offset_x: offsetX, - offset_y: offsetY, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - }; - } - case "pointerdown": - case "pointermove": - case "pointerup": - case "pointercancel": - case "gotpointercapture": - case "lostpointercapture": - case "pointerenter": - case "pointerleave": - case "pointerover": - case "pointerout": { - const { - altKey, - button, - buttons, - clientX, - clientY, - ctrlKey, - metaKey, - pageX, - pageY, - screenX, - screenY, - shiftKey, - pointerId, - width, - height, - pressure, - tangentialPressure, - tiltX, - tiltY, - twist, - pointerType, - isPrimary, - } = event; - return { - alt_key: altKey, - button: button, - buttons: buttons, - client_x: clientX, - client_y: clientY, - ctrl_key: ctrlKey, - meta_key: metaKey, - page_x: pageX, - page_y: pageY, - screen_x: screenX, - screen_y: screenY, - shift_key: shiftKey, - pointer_id: pointerId, - width: width, - height: height, - pressure: pressure, - tangential_pressure: tangentialPressure, - tilt_x: tiltX, - tilt_y: tiltY, - twist: twist, - pointer_type: pointerType, - is_primary: isPrimary, - }; - } - case "select": { - return {}; - } - case "touchcancel": - case "touchend": - case "touchmove": - case "touchstart": { - const { altKey, ctrlKey, metaKey, shiftKey } = event; - return { - // changed_touches: event.changedTouches, - // target_touches: event.targetTouches, - // touches: event.touches, - alt_key: altKey, - ctrl_key: ctrlKey, - meta_key: metaKey, - shift_key: shiftKey, - }; - } - case "scroll": { - return {}; - } - case "wheel": { - const { deltaX, deltaY, deltaZ, deltaMode } = event; - return { - delta_x: deltaX, - delta_y: deltaY, - delta_z: deltaZ, - delta_mode: deltaMode, - }; - } - case "animationstart": - case "animationend": - case "animationiteration": { - const { animationName, elapsedTime, pseudoElement } = event; - return { - animation_name: animationName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "transitionend": { - const { propertyName, elapsedTime, pseudoElement } = event; - return { - property_name: propertyName, - elapsed_time: elapsedTime, - pseudo_element: pseudoElement, - }; - } - case "abort": - case "canplay": - case "canplaythrough": - case "durationchange": - case "emptied": - case "encrypted": - case "ended": - case "error": - case "loadeddata": - case "loadedmetadata": - case "loadstart": - case "pause": - case "play": - case "playing": - case "progress": - case "ratechange": - case "seeked": - case "seeking": - case "stalled": - case "suspend": - case "timeupdate": - case "volumechange": - case "waiting": { - return {}; - } - case "toggle": { - return {}; - } - default: { - return {}; - } - } -} -function serializeIpcMessage(method, params = {}) { - return JSON.stringify({ method, params }); -} -const bool_attrs = { - allowfullscreen: true, - allowpaymentrequest: true, - async: true, - autofocus: true, - autoplay: true, - checked: true, - controls: true, - default: true, - defer: true, - disabled: true, - formnovalidate: true, - hidden: true, - ismap: true, - itemscope: true, - loop: true, - multiple: true, - muted: true, - nomodule: true, - novalidate: true, - open: true, - playsinline: true, - readonly: true, - required: true, - reversed: true, - selected: true, - truespeed: true, -}; - -function is_element_node(node) { - return node.nodeType == 1; -} - -function event_bubbles(event) { - switch (event) { - case "copy": - return true; - case "cut": - return true; - case "paste": - return true; - case "compositionend": - return true; - case "compositionstart": - return true; - case "compositionupdate": - return true; - case "keydown": - return true; - case "keypress": - return true; - case "keyup": - return true; - case "focus": - return false; - case "focusout": - return true; - case "focusin": - return true; - case "blur": - return false; - case "change": - return true; - case "input": - return true; - case "invalid": - return true; - case "reset": - return true; - case "submit": - return true; - case "click": - return true; - case "contextmenu": - return true; - case "doubleclick": - return true; - case "dblclick": - return true; - case "drag": - return true; - case "dragend": - return true; - case "dragenter": - return false; - case "dragexit": - return false; - case "dragleave": - return true; - case "dragover": - return true; - case "dragstart": - return true; - case "drop": - return true; - case "mousedown": - return true; - case "mouseenter": - return false; - case "mouseleave": - return false; - case "mousemove": - return true; - case "mouseout": - return true; - case "scroll": - return false; - case "mouseover": - return true; - case "mouseup": - return true; - case "pointerdown": - return true; - case "pointermove": - return true; - case "pointerup": - return true; - case "pointercancel": - return true; - case "gotpointercapture": - return true; - case "lostpointercapture": - return true; - case "pointerenter": - return false; - case "pointerleave": - return false; - case "pointerover": - return true; - case "pointerout": - return true; - case "select": - return true; - case "touchcancel": - return true; - case "touchend": - return true; - case "touchmove": - return true; - case "touchstart": - return true; - case "wheel": - return true; - case "abort": - return false; - case "canplay": - return false; - case "canplaythrough": - return false; - case "durationchange": - return false; - case "emptied": - return false; - case "encrypted": - return true; - case "ended": - return false; - case "error": - return false; - case "loadeddata": - return false; - case "loadedmetadata": - return false; - case "loadstart": - return false; - case "pause": - return false; - case "play": - return false; - case "playing": - return false; - case "progress": - return false; - case "ratechange": - return false; - case "seeked": - return false; - case "seeking": - return false; - case "stalled": - return false; - case "suspend": - return false; - case "timeupdate": - return false; - case "volumechange": - return false; - case "waiting": - return false; - case "animationstart": - return true; - case "animationend": - return true; - case "animationiteration": - return true; - case "transitionend": - return true; - case "toggle": - return true; - } -} \ No newline at end of file diff --git a/packages/liveview/src/lib.rs b/packages/liveview/src/lib.rs index 540d29974..ffddf0272 100644 --- a/packages/liveview/src/lib.rs +++ b/packages/liveview/src/lib.rs @@ -1,56 +1,55 @@ -#![allow(dead_code)] - -pub(crate) mod events; pub mod adapters { #[cfg(feature = "warp")] pub mod warp_adapter; + #[cfg(feature = "warp")] + pub use warp_adapter::*; #[cfg(feature = "axum")] pub mod axum_adapter; + #[cfg(feature = "axum")] + pub use axum_adapter::*; #[cfg(feature = "salvo")] pub mod salvo_adapter; + + #[cfg(feature = "salvo")] + pub use salvo_adapter::*; } -use std::net::SocketAddr; +pub use adapters::*; -use tokio_util::task::LocalPoolHandle; +pub mod pool; +use futures_util::{SinkExt, StreamExt}; +pub use pool::*; -#[derive(Clone)] -pub struct Liveview { - pool: LocalPoolHandle, - addr: String, +pub trait WebsocketTx: SinkExt {} +impl WebsocketTx for T where T: SinkExt {} + +pub trait WebsocketRx: StreamExt> {} +impl WebsocketRx for T where T: StreamExt> {} + +#[derive(Debug, thiserror::Error)] +pub enum LiveViewError { + #[error("warp error")] + SendingFailed, } -impl Liveview { - pub fn body(&self, header: &str) -> String { - format!( - r#" - - - - {header} - - -
- - -"#, - addr = self.addr, - interpreter = include_str!("../src/interpreter.js") - ) - } -} +use dioxus_interpreter_js::INTERPRETER_JS; +static MAIN_JS: &str = include_str!("./main.js"); -pub fn new(addr: impl Into) -> Liveview { - let addr: SocketAddr = addr.into(); - - Liveview { - pool: LocalPoolHandle::new(16), - addr: addr.to_string(), - } +/// This script that gets injected into your app connects this page to the websocket endpoint +/// +/// Once the endpoint is connected, it will send the initial state of the app, and then start +/// processing user events and returning edits to the liveview instance +pub fn interpreter_glue(url: &str) -> String { + format!( + r#" + + "# + ) } diff --git a/packages/liveview/src/main.js b/packages/liveview/src/main.js new file mode 100644 index 000000000..e37ac7b67 --- /dev/null +++ b/packages/liveview/src/main.js @@ -0,0 +1,36 @@ +function main() { + let root = window.document.getElementById("main"); + + if (root != null) { + // create a new ipc + window.ipc = new IPC(root); + window.ipc.postMessage(serializeIpcMessage("initialize")); + } +} + +class IPC { + constructor(root) { + // connect to the websocket + window.interpreter = new Interpreter(root); + + this.ws = new WebSocket(WS_ADDR); + + this.ws.onopen = () => { + console.log("Connected to the websocket"); + }; + + this.ws.onerror = (err) => { + console.error("Error: ", err); + }; + + this.ws.onmessage = (event) => { + console.log("Received message: ", event.data); + let edits = JSON.parse(event.data); + window.interpreter.handleEdits(edits); + }; + } + + postMessage(msg) { + this.ws.send(msg); + } +} diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs new file mode 100644 index 000000000..c769e1371 --- /dev/null +++ b/packages/liveview/src/pool.rs @@ -0,0 +1,123 @@ +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)] +pub struct LiveViewPool { + pub(crate) pool: LocalPoolHandle, +} + +impl Default for LiveViewPool { + fn default() -> Self { + Self::new() + } +} + +impl LiveViewPool { + pub fn new() -> Self { + LiveViewPool { + pool: LocalPoolHandle::new(16), + } + } + + pub async fn launch( + &self, + ws: impl LiveViewSocket, + app: fn(Scope<()>) -> Element, + ) -> Result<(), LiveViewError> { + self.launch_with_props(ws, app, ()).await + } + + pub async fn launch_with_props( + &self, + ws: impl LiveViewSocket, + app: fn(Scope) -> Element, + props: T, + ) -> Result<(), LiveViewError> { + match self.pool.spawn_pinned(move || run(app, props, ws)).await { + Ok(Ok(_)) => Ok(()), + Ok(Err(e)) => Err(e), + Err(_) => Err(LiveViewError::SendingFailed), + } + } +} + +/// A LiveViewSocket is a Sink and Stream of Strings that Dioxus uses to communicate with the client +pub trait LiveViewSocket: + SinkExt + + StreamExt> + + Send + + 'static +{ +} + +impl LiveViewSocket for S where + S: SinkExt + + StreamExt> + + Send + + 'static +{ +} + +/// 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 run( + app: Component, + props: T, + ws: impl LiveViewSocket, +) -> 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); + + // send the initial render to the client + ws.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.next() => { + match evt { + Some(Ok(evt)) => { + if let Ok(IpcMessage { params }) = serde_json::from_str::(&evt) { + vdom.handle_event(¶ms.name, params.data.into_any(), params.element, params.bubbles); + } + } + // log this I guess? when would we get an error here? + Some(Err(_e)) => {}, + None => return Ok(()), + } + } + } + + let edits = vdom + .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) + .await; + + ws.send(serde_json::to_string(&edits).unwrap()).await?; + } +}