Merge pull request #1791 from ealmloff/breaking
Remove scope, use_state, use_ref, bump allocator and make everything 'static
This commit is contained in:
517 files changed
+17126
-20669
No files matched your search
@@ -1,5 +1,15 @@
|
||||
use crate::{LiveViewError, LiveViewSocket};
|
||||
use axum::extract::ws::{Message, WebSocket};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{interpreter_glue, LiveViewError, LiveViewSocket, LiveviewRouter};
|
||||
use axum::{
|
||||
extract::{
|
||||
ws::{Message, WebSocket},
|
||||
WebSocketUpgrade,
|
||||
},
|
||||
response::Html,
|
||||
routing::*,
|
||||
Router,
|
||||
};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
|
||||
/// Convert an Axum WebSocket into a `LiveViewSocket`.
|
||||
@@ -22,3 +32,60 @@ fn transform_rx(message: Result<Message, axum::Error>) -> Result<Vec<u8>, LiveVi
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, axum::Error> {
|
||||
Ok(Message::Binary(message))
|
||||
}
|
||||
|
||||
impl LiveviewRouter for Router {
|
||||
fn create_default_liveview_router() -> Self {
|
||||
Router::new()
|
||||
}
|
||||
|
||||
fn with_virtual_dom(
|
||||
self,
|
||||
route: &str,
|
||||
app: impl Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let view = crate::LiveViewPool::new();
|
||||
|
||||
let ws_path = format!("{}/ws", route);
|
||||
let title = crate::app_title();
|
||||
|
||||
let index_page_with_glue = move |glue: &str| {
|
||||
Html(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>{title}</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#,
|
||||
))
|
||||
};
|
||||
|
||||
let app = Arc::new(app);
|
||||
|
||||
self.route(
|
||||
&ws_path,
|
||||
get(move |ws: WebSocketUpgrade| async move {
|
||||
let app = app.clone();
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
_ = view
|
||||
.launch_virtualdom(axum_socket(socket), move || app())
|
||||
.await;
|
||||
})
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
route,
|
||||
get(move || async move { index_page_with_glue(&interpreter_glue(&ws_path)) }),
|
||||
)
|
||||
}
|
||||
|
||||
async fn start(self, address: impl Into<std::net::SocketAddr>) {
|
||||
if let Err(err) = axum::Server::bind(&address.into())
|
||||
.serve(self.into_make_service())
|
||||
.await
|
||||
{
|
||||
eprintln!("Failed to start axum server: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use std::future::Future;
|
||||
|
||||
use dioxus_core::{Element, VirtualDom};
|
||||
|
||||
#[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::*;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub mod rocket_adapter;
|
||||
#[cfg(feature = "rocket")]
|
||||
pub use rocket_adapter::*;
|
||||
|
||||
/// A trait for servers that can be used to host a LiveView app.
|
||||
pub trait LiveviewRouter {
|
||||
/// Create a new router.
|
||||
fn create_default_liveview_router() -> Self;
|
||||
|
||||
/// Add a liveview route to the server from a component
|
||||
fn with_app(self, route: &str, app: fn() -> Element) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.with_virtual_dom(route, move || VirtualDom::new(app))
|
||||
}
|
||||
|
||||
/// Add a liveview route to the server from a virtual dom.
|
||||
fn with_virtual_dom(
|
||||
self,
|
||||
route: &str,
|
||||
app: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> Self;
|
||||
|
||||
/// Start the server on an address.
|
||||
fn start(self, address: impl Into<std::net::SocketAddr>) -> impl Future<Output = ()>;
|
||||
}
|
||||
@@ -1,6 +1,13 @@
|
||||
use crate::LiveViewPool;
|
||||
use crate::LiveviewRouter;
|
||||
use crate::{LiveViewError, LiveViewSocket};
|
||||
use rocket::futures::{SinkExt, StreamExt};
|
||||
use rocket::response::content::RawHtml;
|
||||
use rocket::{get, routes, State};
|
||||
use rocket_ws::Channel;
|
||||
use rocket_ws::WebSocket;
|
||||
use rocket_ws::{result::Error, stream::DuplexStream, Message};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Convert a Rocket WebSocket into a `LiveViewSocket`.
|
||||
///
|
||||
@@ -21,5 +28,86 @@ fn transform_rx(message: Result<Message, Error>) -> Result<Vec<u8>, LiveViewErro
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, Error> {
|
||||
Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
|
||||
Ok(Message::Binary(message))
|
||||
}
|
||||
|
||||
impl LiveviewRouter for rocket::Rocket<rocket::Build> {
|
||||
fn create_default_liveview_router() -> Self {
|
||||
Self::build()
|
||||
}
|
||||
|
||||
fn with_virtual_dom(
|
||||
self,
|
||||
route: &str,
|
||||
app: impl Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
#[get("/")]
|
||||
async fn index(request: &rocket::route::Route) -> RawHtml<String> {
|
||||
let route = request.uri.base();
|
||||
|
||||
let glue = crate::interpreter_glue(&format!("{route}/ws",));
|
||||
|
||||
let title = crate::app_title();
|
||||
|
||||
RawHtml(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>{title}</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#
|
||||
))
|
||||
}
|
||||
|
||||
#[get("/ws")]
|
||||
fn ws(ws: WebSocket, app: &State<LiveviewApp>) -> Channel<'static> {
|
||||
let app = app.inner();
|
||||
let pool = app.pool.clone();
|
||||
let app = app.app.clone();
|
||||
|
||||
ws.channel(move |stream| {
|
||||
Box::pin(async move {
|
||||
let _ = pool
|
||||
.launch_virtualdom(crate::rocket_socket(stream), move || app())
|
||||
.await;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
struct LiveviewApp {
|
||||
app: Arc<dyn Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static>,
|
||||
pool: LiveViewPool,
|
||||
}
|
||||
|
||||
let app = Arc::new(app);
|
||||
|
||||
let view = crate::LiveViewPool::new();
|
||||
|
||||
self.manage(LiveviewApp {
|
||||
app: app,
|
||||
pool: view,
|
||||
})
|
||||
.mount(route, routes![index, ws])
|
||||
}
|
||||
|
||||
async fn start(self, address: impl Into<std::net::SocketAddr>) {
|
||||
let address = address.into();
|
||||
|
||||
let figment = self
|
||||
.figment()
|
||||
.clone()
|
||||
.merge((rocket::Config::PORT, address.port()))
|
||||
.merge((rocket::Config::ADDRESS, address.ip()));
|
||||
|
||||
self.configure(figment)
|
||||
.ignite()
|
||||
.await
|
||||
.expect("Failed to ignite rocket")
|
||||
.launch()
|
||||
.await
|
||||
.expect("Failed to launch rocket");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use salvo::ws::{Message, WebSocket};
|
||||
|
||||
use crate::LiveViewPool;
|
||||
use crate::LiveviewRouter;
|
||||
use crate::{LiveViewError, LiveViewSocket};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use salvo::conn::TcpListener;
|
||||
use salvo::http::StatusError;
|
||||
use salvo::websocket::WebSocketUpgrade;
|
||||
use salvo::websocket::{Message, WebSocket};
|
||||
use salvo::writing::Text;
|
||||
use salvo::Listener;
|
||||
use salvo::Server;
|
||||
use salvo::{handler, Depot, Request, Response, Router};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Convert a Salvo WebSocket into a `LiveViewSocket`.
|
||||
///
|
||||
@@ -19,5 +28,81 @@ fn transform_rx(message: Result<Message, salvo::Error>) -> Result<Vec<u8>, LiveV
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, salvo::Error> {
|
||||
Ok(Message::text(String::from_utf8_lossy(&message).to_string()))
|
||||
Ok(Message::binary(message))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LiveviewApp {
|
||||
app: Arc<dyn Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static>,
|
||||
pool: Arc<LiveViewPool>,
|
||||
}
|
||||
|
||||
impl LiveviewRouter for Router {
|
||||
fn create_default_liveview_router() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
|
||||
fn with_virtual_dom(
|
||||
self,
|
||||
route: &str,
|
||||
app: impl Fn() -> dioxus_core::prelude::VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
let app = Arc::new(app);
|
||||
|
||||
let view = crate::LiveViewPool::new();
|
||||
|
||||
self.push(
|
||||
Router::with_path(route)
|
||||
.hoop(salvo::affix::inject(LiveviewApp {
|
||||
app: app,
|
||||
pool: Arc::new(view),
|
||||
}))
|
||||
.get(index)
|
||||
.push(Router::with_path("ws").get(connect)),
|
||||
)
|
||||
}
|
||||
|
||||
async fn start(self, address: impl Into<std::net::SocketAddr>) {
|
||||
let address = address.into();
|
||||
|
||||
let acceptor = TcpListener::new(address).bind().await;
|
||||
Server::new(acceptor).serve(self).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[handler]
|
||||
fn index(req: &mut Request, res: &mut Response) {
|
||||
let base = req.uri().path();
|
||||
let title = crate::app_title();
|
||||
|
||||
res.render(Text::Html(format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head> <title>{title}</title> </head>
|
||||
<body> <div id="main"></div> </body>
|
||||
{glue}
|
||||
</html>
|
||||
"#,
|
||||
glue = crate::interpreter_glue(&format!("{base}/ws"))
|
||||
)));
|
||||
}
|
||||
|
||||
#[handler]
|
||||
async fn connect(
|
||||
req: &mut Request,
|
||||
depot: &mut Depot,
|
||||
res: &mut Response,
|
||||
) -> Result<(), StatusError> {
|
||||
let app = depot.obtain::<LiveviewApp>().unwrap().clone();
|
||||
let view = app.pool.clone();
|
||||
let app = app.app.clone();
|
||||
|
||||
WebSocketUpgrade::new()
|
||||
.upgrade(req, res, |ws| async move {
|
||||
_ = view
|
||||
.launch_virtualdom(crate::salvo_socket(ws), move || app())
|
||||
.await;
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -21,5 +21,5 @@ fn transform_rx(message: Result<Message, warp::Error>) -> Result<Vec<u8>, LiveVi
|
||||
}
|
||||
|
||||
async fn transform_tx(message: Vec<u8>) -> Result<Message, warp::Error> {
|
||||
Ok(Message::text(String::from_utf8_lossy(&message).to_string()))
|
||||
Ok(Message::binary(message))
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use dioxus_cli_config::CURRENT_CONFIG;
|
||||
use dioxus_core::VirtualDom;
|
||||
|
||||
use crate::LiveviewRouter;
|
||||
|
||||
pub(crate) fn app_title() -> String {
|
||||
CURRENT_CONFIG
|
||||
.as_ref()
|
||||
.map(|c| c.dioxus_config.web.app.title.clone())
|
||||
.unwrap_or_else(|_| "Dioxus Liveview App".to_string())
|
||||
}
|
||||
|
||||
/// A configuration for the LiveView server.
|
||||
pub struct Config<R: LiveviewRouter> {
|
||||
router: R,
|
||||
address: std::net::SocketAddr,
|
||||
route: String,
|
||||
}
|
||||
|
||||
impl<R: LiveviewRouter> Default for Config<R> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
router: R::create_default_liveview_router(),
|
||||
address: ([127, 0, 0, 1], 8080).into(),
|
||||
route: "/".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: LiveviewRouter> Config<R> {
|
||||
/// Set the route to use for the LiveView server.
|
||||
pub fn route(mut self, route: impl Into<String>) -> Self {
|
||||
self.route = route.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new configuration for the LiveView server.
|
||||
pub fn with_app(mut self, app: fn() -> dioxus_core::prelude::Element) -> Self {
|
||||
self.router = self.router.with_app(&self.route, app);
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new configuration for the LiveView server.
|
||||
pub fn with_virtual_dom(
|
||||
mut self,
|
||||
virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.router = self.router.with_virtual_dom(&self.route, virtual_dom);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the address to listen on.
|
||||
pub fn address(mut self, address: impl Into<std::net::SocketAddr>) -> Self {
|
||||
self.address = address.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Launch the LiveView server.
|
||||
pub async fn launch(self) {
|
||||
println!("{} started on http://{}", app_title(), self.address);
|
||||
self.router.start(self.address).await
|
||||
}
|
||||
}
|
||||
@@ -1,61 +1,62 @@
|
||||
#![allow(clippy::await_holding_refcell_ref)]
|
||||
|
||||
use async_trait::async_trait;
|
||||
use dioxus_core::ScopeState;
|
||||
use dioxus_core::ScopeId;
|
||||
use dioxus_html::prelude::{EvalError, EvalProvider, Evaluator};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::query::{Query, QueryEngine};
|
||||
|
||||
/// Provides the DesktopEvalProvider through [`cx.provide_context`].
|
||||
pub fn init_eval(cx: &ScopeState) {
|
||||
let query = cx.consume_context::<QueryEngine>().unwrap();
|
||||
pub fn init_eval() {
|
||||
let query = ScopeId::ROOT.consume_context::<QueryEngine>().unwrap();
|
||||
let provider: Rc<dyn EvalProvider> = Rc::new(DesktopEvalProvider { query });
|
||||
cx.provide_context(provider);
|
||||
ScopeId::ROOT.provide_context(provider);
|
||||
}
|
||||
|
||||
/// Reprents the desktop-target's provider of evaluators.
|
||||
/// Reprints the desktop-target's provider of evaluators.
|
||||
pub struct DesktopEvalProvider {
|
||||
query: QueryEngine,
|
||||
}
|
||||
|
||||
impl EvalProvider for DesktopEvalProvider {
|
||||
fn new_evaluator(&self, js: String) -> Result<Rc<dyn Evaluator>, EvalError> {
|
||||
Ok(Rc::new(DesktopEvaluator::new(self.query.clone(), js)))
|
||||
fn new_evaluator(&self, js: String) -> Result<GenerationalBox<Box<dyn Evaluator>>, EvalError> {
|
||||
Ok(DesktopEvaluator::create(self.query.clone(), js))
|
||||
}
|
||||
}
|
||||
|
||||
/// Reprents a desktop-target's JavaScript evaluator.
|
||||
pub(crate) struct DesktopEvaluator {
|
||||
query: Rc<RefCell<Query<serde_json::Value>>>,
|
||||
query: Query<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl DesktopEvaluator {
|
||||
/// Creates a new evaluator for desktop-based targets.
|
||||
pub fn new(query: QueryEngine, js: String) -> Self {
|
||||
let query = query.new_query(&js);
|
||||
pub fn create(query_engine: QueryEngine, js: String) -> GenerationalBox<Box<dyn Evaluator>> {
|
||||
let query = query_engine.new_query(&js);
|
||||
// We create a generational box that is owned by the query slot so that when we drop the query slot, the generational box is also dropped.
|
||||
let owner = UnsyncStorage::owner();
|
||||
let query_id = query.id;
|
||||
let query = owner.insert(Box::new(DesktopEvaluator { query }) as Box<dyn Evaluator>);
|
||||
query_engine.active_requests.slab.borrow_mut()[query_id].owner = Some(owner);
|
||||
|
||||
Self {
|
||||
query: Rc::new(RefCell::new(query)),
|
||||
}
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl Evaluator for DesktopEvaluator {
|
||||
/// # Panics
|
||||
/// This will panic if the query is currently being awaited.
|
||||
async fn join(&self) -> Result<serde_json::Value, EvalError> {
|
||||
fn poll_join(
|
||||
&mut self,
|
||||
context: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||
self.query
|
||||
.borrow_mut()
|
||||
.result()
|
||||
.await
|
||||
.poll_result(context)
|
||||
.map_err(|e| EvalError::Communication(e.to_string()))
|
||||
}
|
||||
|
||||
/// Sends a message to the evaluated JavaScript.
|
||||
fn send(&self, data: serde_json::Value) -> Result<(), EvalError> {
|
||||
if let Err(e) = self.query.borrow_mut().send(data) {
|
||||
if let Err(e) = self.query.send(data) {
|
||||
return Err(EvalError::Communication(e.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
@@ -65,11 +66,12 @@ impl Evaluator for DesktopEvaluator {
|
||||
///
|
||||
/// # Panics
|
||||
/// This will panic if the query is currently being awaited.
|
||||
async fn recv(&self) -> Result<serde_json::Value, EvalError> {
|
||||
fn poll_recv(
|
||||
&mut self,
|
||||
context: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
|
||||
self.query
|
||||
.borrow_mut()
|
||||
.recv()
|
||||
.await
|
||||
.poll_recv(context)
|
||||
.map_err(|e| EvalError::Communication(e.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use dioxus_core::*;
|
||||
use std::any::Any;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub type Config = crate::Config<axum::Router>;
|
||||
#[cfg(feature = "salvo")]
|
||||
pub type Config = crate::Config<salvo::Router>;
|
||||
#[cfg(feature = "rocket")]
|
||||
pub type Config = crate::Config<rocket::Rocket<rocket::Build>>;
|
||||
|
||||
/// Launches the WebView and runs the event loop, with configuration and root props.
|
||||
pub fn launch(
|
||||
root: fn() -> Element,
|
||||
contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
|
||||
platform_config: Config,
|
||||
) {
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
platform_config
|
||||
.with_virtual_dom(move || {
|
||||
let mut virtual_dom = VirtualDom::new(root);
|
||||
|
||||
for context in &contexts {
|
||||
virtual_dom.insert_any_root_context(context());
|
||||
}
|
||||
|
||||
virtual_dom
|
||||
})
|
||||
.launch()
|
||||
.await;
|
||||
});
|
||||
}
|
||||
@@ -2,29 +2,8 @@
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
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::*;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
pub mod rocket_adapter;
|
||||
#[cfg(feature = "rocket")]
|
||||
pub use rocket_adapter::*;
|
||||
}
|
||||
|
||||
mod adapters;
|
||||
#[allow(unused_imports)]
|
||||
pub use adapters::*;
|
||||
|
||||
mod element;
|
||||
@@ -32,8 +11,11 @@ pub mod pool;
|
||||
mod query;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
pub use pool::*;
|
||||
mod config;
|
||||
mod eval;
|
||||
mod events;
|
||||
pub use config::*;
|
||||
pub mod launch;
|
||||
|
||||
pub trait WebsocketTx: SinkExt<String, Error = LiveViewError> {}
|
||||
impl<T> WebsocketTx for T where T: SinkExt<String, Error = LiveViewError> {}
|
||||
@@ -47,7 +29,67 @@ pub enum LiveViewError {
|
||||
SendingFailed,
|
||||
}
|
||||
|
||||
static MINIFIED: &str = include_str!("./minified.js");
|
||||
fn handle_edits_code() -> String {
|
||||
use dioxus_interpreter_js::binary_protocol::SLEDGEHAMMER_JS;
|
||||
use minify_js::{minify, Session, TopLevelMode};
|
||||
|
||||
let serialize_file_uploads = r#"if (
|
||||
target.tagName === "INPUT" &&
|
||||
(event.type === "change" || event.type === "input")
|
||||
) {
|
||||
const type = target.getAttribute("type");
|
||||
if (type === "file") {
|
||||
async function read_files() {
|
||||
const files = target.files;
|
||||
const file_contents = {};
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
|
||||
file_contents[file.name] = Array.from(
|
||||
new Uint8Array(await file.arrayBuffer())
|
||||
);
|
||||
}
|
||||
let file_engine = {
|
||||
files: file_contents,
|
||||
};
|
||||
contents.files = file_engine;
|
||||
|
||||
if (realId === null) {
|
||||
return;
|
||||
}
|
||||
const message = window.interpreter.serializeIpcMessage("user_event", {
|
||||
name: name,
|
||||
element: parseInt(realId),
|
||||
data: contents,
|
||||
bubbles,
|
||||
});
|
||||
window.ipc.postMessage(message);
|
||||
}
|
||||
read_files();
|
||||
return;
|
||||
}
|
||||
}"#;
|
||||
let mut interpreter = SLEDGEHAMMER_JS
|
||||
.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads)
|
||||
.replace("export", "");
|
||||
while let Some(import_start) = interpreter.find("import") {
|
||||
let import_end = interpreter[import_start..]
|
||||
.find(|c| c == ';' || c == '\n')
|
||||
.map(|i| i + import_start)
|
||||
.unwrap_or_else(|| interpreter.len());
|
||||
interpreter.replace_range(import_start..import_end, "");
|
||||
}
|
||||
|
||||
let main_js = include_str!("./main.js");
|
||||
|
||||
let js = format!("{interpreter}\n{main_js}");
|
||||
|
||||
let session = Session::new();
|
||||
let mut out = Vec::new();
|
||||
minify(&session, TopLevelMode::Module, js.as_bytes(), &mut out).unwrap();
|
||||
String::from_utf8(out).unwrap()
|
||||
}
|
||||
|
||||
/// This script that gets injected into your app connects this page to the websocket endpoint
|
||||
///
|
||||
@@ -85,6 +127,8 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
|
||||
"return path;"
|
||||
};
|
||||
|
||||
let handle_edits = handle_edits_code();
|
||||
|
||||
format!(
|
||||
r#"
|
||||
<script>
|
||||
@@ -93,7 +137,7 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
|
||||
}}
|
||||
|
||||
var WS_ADDR = __dioxusGetWsUrl("{url_or_path}");
|
||||
{MINIFIED}
|
||||
{handle_edits}
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
|
||||
+26
-155
@@ -5,11 +5,10 @@ use crate::{
|
||||
query::{QueryEngine, QueryResult},
|
||||
LiveViewError,
|
||||
};
|
||||
use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations};
|
||||
use dioxus_html::{event_bubbles, EventData, HtmlEvent, PlatformEventData};
|
||||
use dioxus_interpreter_js::binary_protocol::Channel;
|
||||
use dioxus_core::prelude::*;
|
||||
use dioxus_html::{EventData, HtmlEvent, PlatformEventData};
|
||||
use dioxus_interpreter_js::MutationState;
|
||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Serialize;
|
||||
use std::{rc::Rc, time::Duration};
|
||||
use tokio_util::task::LocalPoolHandle;
|
||||
@@ -38,15 +37,15 @@ impl LiveViewPool {
|
||||
pub async fn launch(
|
||||
&self,
|
||||
ws: impl LiveViewSocket,
|
||||
app: fn(Scope<()>) -> Element,
|
||||
app: fn() -> Element,
|
||||
) -> Result<(), LiveViewError> {
|
||||
self.launch_with_props(ws, app, ()).await
|
||||
self.launch_with_props(ws, |app| app(), app).await
|
||||
}
|
||||
|
||||
pub async fn launch_with_props<T: Send + 'static>(
|
||||
pub async fn launch_with_props<T: Clone + Send + 'static>(
|
||||
&self,
|
||||
ws: impl LiveViewSocket,
|
||||
app: fn(Scope<T>) -> Element,
|
||||
app: fn(T) -> Element,
|
||||
props: T,
|
||||
) -> Result<(), LiveViewError> {
|
||||
self.launch_virtualdom(ws, move || VirtualDom::new_with_props(app, props))
|
||||
@@ -126,27 +125,22 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
||||
rx
|
||||
};
|
||||
|
||||
let mut templates: FxHashMap<String, u16> = Default::default();
|
||||
let mut max_template_count = 0;
|
||||
let mut mutations = MutationState::default();
|
||||
|
||||
// Create the a proxy for query engine
|
||||
let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let query_engine = QueryEngine::new(query_tx);
|
||||
vdom.base_scope().provide_context(query_engine.clone());
|
||||
init_eval(vdom.base_scope());
|
||||
vdom.in_runtime(|| {
|
||||
ScopeId::ROOT.provide_context(query_engine.clone());
|
||||
init_eval();
|
||||
});
|
||||
|
||||
// pin the futures so we can use select!
|
||||
pin_mut!(ws);
|
||||
|
||||
let mut edit_channel = Channel::default();
|
||||
if let Some(edits) = {
|
||||
let mutations = vdom.rebuild();
|
||||
apply_edits(
|
||||
mutations,
|
||||
&mut edit_channel,
|
||||
&mut templates,
|
||||
&mut max_template_count,
|
||||
)
|
||||
vdom.rebuild(&mut mutations);
|
||||
take_edits(&mut mutations)
|
||||
} {
|
||||
// send the initial render to the client
|
||||
ws.send(edits).await?;
|
||||
@@ -233,18 +227,16 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
|
||||
}
|
||||
}
|
||||
|
||||
let edits = vdom
|
||||
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
|
||||
.await;
|
||||
// wait for suspense to resolve in a 10ms window
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_millis(10)) => {}
|
||||
_ = vdom.wait_for_suspense() => {}
|
||||
}
|
||||
|
||||
if let Some(edits) = {
|
||||
apply_edits(
|
||||
edits,
|
||||
&mut edit_channel,
|
||||
&mut templates,
|
||||
&mut max_template_count,
|
||||
)
|
||||
} {
|
||||
// render the vdom
|
||||
vdom.render_immediate(&mut mutations);
|
||||
|
||||
if let Some(edits) = take_edits(&mut mutations) {
|
||||
ws.send(edits).await?;
|
||||
}
|
||||
}
|
||||
@@ -256,132 +248,11 @@ fn text_frame(text: &str) -> Vec<u8> {
|
||||
bytes
|
||||
}
|
||||
|
||||
fn add_template(
|
||||
template: &Template<'static>,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &mut u16,
|
||||
) {
|
||||
for root in template.roots.iter() {
|
||||
create_template_node(channel, root);
|
||||
templates.insert(template.name.to_owned(), *max_template_count);
|
||||
}
|
||||
channel.add_templates(*max_template_count, template.roots.len() as u16);
|
||||
|
||||
*max_template_count += 1
|
||||
}
|
||||
|
||||
fn create_template_node(channel: &mut Channel, v: &'static TemplateNode<'static>) {
|
||||
use TemplateNode::*;
|
||||
match v {
|
||||
Element {
|
||||
tag,
|
||||
namespace,
|
||||
attrs,
|
||||
children,
|
||||
..
|
||||
} => {
|
||||
// Push the current node onto the stack
|
||||
match namespace {
|
||||
Some(ns) => channel.create_element_ns(tag, ns),
|
||||
None => channel.create_element(tag),
|
||||
}
|
||||
// Set attributes on the current node
|
||||
for attr in *attrs {
|
||||
if let TemplateAttribute::Static {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
} = attr
|
||||
{
|
||||
channel.set_top_attribute(name, value, namespace.unwrap_or_default())
|
||||
}
|
||||
}
|
||||
// Add each child to the stack
|
||||
for child in *children {
|
||||
create_template_node(channel, child);
|
||||
}
|
||||
// Add all children to the parent
|
||||
channel.append_children_to_top(children.len() as u16);
|
||||
}
|
||||
Text { text } => channel.create_raw_text(text),
|
||||
DynamicText { .. } => channel.create_raw_text("p"),
|
||||
Dynamic { .. } => channel.add_placeholder(),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_edits(
|
||||
mutations: Mutations,
|
||||
channel: &mut Channel,
|
||||
templates: &mut FxHashMap<String, u16>,
|
||||
max_template_count: &mut u16,
|
||||
) -> Option<Vec<u8>> {
|
||||
use dioxus_core::Mutation::*;
|
||||
if mutations.templates.is_empty() && mutations.edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
for template in mutations.templates {
|
||||
add_template(&template, channel, templates, max_template_count);
|
||||
}
|
||||
for edit in mutations.edits {
|
||||
match edit {
|
||||
AppendChildren { id, m } => channel.append_children(id.0 as u32, m as u16),
|
||||
AssignId { path, id } => channel.assign_id(path, id.0 as u32),
|
||||
CreatePlaceholder { id } => channel.create_placeholder(id.0 as u32),
|
||||
CreateTextNode { value, id } => channel.create_text_node(value, id.0 as u32),
|
||||
HydrateText { path, value, id } => channel.hydrate_text(path, value, id.0 as u32),
|
||||
LoadTemplate { name, index, id } => {
|
||||
if let Some(tmpl_id) = templates.get(name) {
|
||||
channel.load_template(*tmpl_id, index as u16, id.0 as u32)
|
||||
}
|
||||
}
|
||||
ReplaceWith { id, m } => channel.replace_with(id.0 as u32, m as u16),
|
||||
ReplacePlaceholder { path, m } => channel.replace_placeholder(path, m as u16),
|
||||
InsertAfter { id, m } => channel.insert_after(id.0 as u32, m as u16),
|
||||
InsertBefore { id, m } => channel.insert_before(id.0 as u32, m as u16),
|
||||
SetAttribute {
|
||||
name,
|
||||
value,
|
||||
id,
|
||||
ns,
|
||||
} => match value {
|
||||
BorrowedAttributeValue::Text(txt) => {
|
||||
channel.set_attribute(id.0 as u32, name, txt, ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Float(f) => {
|
||||
channel.set_attribute(id.0 as u32, name, &f.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Int(n) => {
|
||||
channel.set_attribute(id.0 as u32, name, &n.to_string(), ns.unwrap_or_default())
|
||||
}
|
||||
BorrowedAttributeValue::Bool(b) => channel.set_attribute(
|
||||
id.0 as u32,
|
||||
name,
|
||||
if b { "true" } else { "false" },
|
||||
ns.unwrap_or_default(),
|
||||
),
|
||||
BorrowedAttributeValue::None => {
|
||||
channel.remove_attribute(id.0 as u32, name, ns.unwrap_or_default())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
SetText { value, id } => channel.set_text(id.0 as u32, value),
|
||||
NewEventListener { name, id, .. } => {
|
||||
channel.new_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
RemoveEventListener { name, id } => {
|
||||
channel.remove_event_listener(name, id.0 as u32, event_bubbles(name) as u8)
|
||||
}
|
||||
Remove { id } => channel.remove(id.0 as u32),
|
||||
PushRoot { id } => channel.push_root(id.0 as u32),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_edits(mutations: &mut MutationState) -> Option<Vec<u8>> {
|
||||
// Add an extra one at the beginning to tell the shim this is a binary frame
|
||||
let mut bytes = vec![1];
|
||||
bytes.extend(channel.export_memory());
|
||||
channel.reset();
|
||||
Some(bytes)
|
||||
mutations.write_memory_into(&mut bytes);
|
||||
(bytes.len() > 1).then_some(bytes)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use futures_util::FutureExt;
|
||||
use generational_box::{Owner, UnsyncStorage};
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::Value;
|
||||
use slab::Slab;
|
||||
@@ -42,8 +44,8 @@ let dioxus = {
|
||||
|
||||
/// Tracks what query ids are currently active
|
||||
|
||||
struct SharedSlab<T = ()> {
|
||||
slab: Rc<RefCell<Slab<T>>>,
|
||||
pub(crate) struct SharedSlab<T = ()> {
|
||||
pub(crate) slab: Rc<RefCell<Slab<T>>>,
|
||||
}
|
||||
|
||||
impl<T> Clone for SharedSlab<T> {
|
||||
@@ -62,9 +64,10 @@ impl<T> Default for SharedSlab<T> {
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryEntry {
|
||||
pub(crate) struct QueryEntry {
|
||||
channel_sender: tokio::sync::mpsc::UnboundedSender<Value>,
|
||||
return_sender: Option<tokio::sync::oneshot::Sender<Value>>,
|
||||
pub(crate) owner: Option<Owner<UnsyncStorage>>,
|
||||
}
|
||||
|
||||
const QUEUE_NAME: &str = "__msg_queues";
|
||||
@@ -72,7 +75,7 @@ const QUEUE_NAME: &str = "__msg_queues";
|
||||
/// 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 {
|
||||
active_requests: SharedSlab<QueryEntry>,
|
||||
pub(crate) active_requests: SharedSlab<QueryEntry>,
|
||||
query_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ impl QueryEngine {
|
||||
let request_id = self.active_requests.slab.borrow_mut().insert(QueryEntry {
|
||||
channel_sender: tx,
|
||||
return_sender: Some(return_tx),
|
||||
owner: None,
|
||||
});
|
||||
|
||||
// start the query
|
||||
@@ -162,7 +166,7 @@ pub(crate) struct Query<V: DeserializeOwned> {
|
||||
query_engine: QueryEngine,
|
||||
pub receiver: tokio::sync::mpsc::UnboundedReceiver<Value>,
|
||||
pub return_receiver: Option<tokio::sync::oneshot::Receiver<Value>>,
|
||||
id: usize,
|
||||
pub id: usize,
|
||||
phantom: std::marker::PhantomData<V>,
|
||||
}
|
||||
|
||||
@@ -198,12 +202,14 @@ impl<V: DeserializeOwned> Query<V> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Receive a message from the query
|
||||
pub async fn recv(&mut self) -> Result<Value, QueryError> {
|
||||
/// Poll the query for a message
|
||||
pub fn poll_recv(
|
||||
&mut self,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<Value, QueryError>> {
|
||||
self.receiver
|
||||
.recv()
|
||||
.await
|
||||
.ok_or(QueryError::Recv(RecvError::Closed))
|
||||
.poll_recv(cx)
|
||||
.map(|result| result.ok_or(QueryError::Recv(RecvError::Closed)))
|
||||
}
|
||||
|
||||
/// Receive the result of the query
|
||||
@@ -215,6 +221,19 @@ impl<V: DeserializeOwned> Query<V> {
|
||||
None => Err(QueryError::Finished),
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll the query for a result
|
||||
pub fn poll_result(
|
||||
&mut self,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<Value, QueryError>> {
|
||||
match self.return_receiver.as_mut() {
|
||||
Some(receiver) => receiver
|
||||
.poll_unpin(cx)
|
||||
.map_err(|_| QueryError::Recv(RecvError::Closed)),
|
||||
None => std::task::Poll::Ready(Err(QueryError::Finished)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: DeserializeOwned> Drop for Query<V> {
|
||||
|
||||
Reference in new issue
Block a user