Merge branch 'master' into events-2

This commit is contained in:
Evan Almloff committed 2024-01-04 19:02:00 -06:00
commit dccfba12d8
209 files changed
+6270 -2789

No files matched your search

@@ -20,5 +20,5 @@ 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::Text(String::from_utf8_lossy(&message).to_string()))
Ok(Message::Binary(message))
}
@@ -0,0 +1,25 @@
use crate::{LiveViewError, LiveViewSocket};
use rocket::futures::{SinkExt, StreamExt};
use rocket_ws::{result::Error, stream::DuplexStream, Message};
/// Convert a rocket websocket into a LiveViewSocket
///
/// This is required to launch a LiveView app using the rocket web framework
pub fn rocket_socket(stream: DuplexStream) -> impl LiveViewSocket {
stream
.map(transform_rx)
.with(transform_tx)
.sink_map_err(|_| LiveViewError::SendingFailed)
}
fn transform_rx(message: Result<Message, Error>) -> Result<Vec<u8>, LiveViewError> {
message
.map_err(|_| LiveViewError::SendingFailed)?
.into_text()
.map(|s| s.into_bytes())
.map_err(|_| LiveViewError::SendingFailed)
}
async fn transform_tx(message: Vec<u8>) -> Result<Message, Error> {
Ok(Message::Text(String::from_utf8_lossy(&message).to_string()))
}
+3 -3
View File
@@ -30,7 +30,7 @@ impl RenderedElementBacking for LiveviewElement {
>,
>,
> {
let script = format!("return window.interpreter.GetClientRect({});", self.id.0);
let script = format!("return window.interpreter.getClientRect({});", self.id.0);
let fut = self
.query
@@ -54,7 +54,7 @@ impl RenderedElementBacking for LiveviewElement {
behavior: dioxus_html::ScrollBehavior,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.ScrollTo({}, {});",
"return window.interpreter.scrollTo({}, {});",
self.id.0,
serde_json::to_string(&behavior).expect("Failed to serialize ScrollBehavior")
);
@@ -78,7 +78,7 @@ impl RenderedElementBacking for LiveviewElement {
focus: bool,
) -> std::pin::Pin<Box<dyn futures_util::Future<Output = dioxus_html::MountedResult<()>>>> {
let script = format!(
"return window.interpreter.SetFocus({}, {});",
"return window.interpreter.setFocus({}, {});",
self.id.0, focus
);
+7 -58
View File
@@ -18,6 +18,11 @@ pub mod adapters {
#[cfg(feature = "salvo")]
pub use salvo_adapter::*;
#[cfg(feature = "rocket")]
pub mod rocket_adapter;
#[cfg(feature = "rocket")]
pub use rocket_adapter::*;
}
pub use adapters::*;
@@ -42,58 +47,7 @@ pub enum LiveViewError {
SendingFailed,
}
use once_cell::sync::Lazy;
static INTERPRETER_JS: Lazy<String> = Lazy::new(|| {
let interpreter = dioxus_interpreter_js::INTERPRETER_JS;
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 = serializeIpcMessage("user_event", {
name: name,
element: parseInt(realId),
data: contents,
bubbles,
});
window.ipc.postMessage(message);
}
read_files();
return;
}
}"#;
let interpreter = interpreter.replace("/*POST_EVENT_SERIALIZATION*/", serialize_file_uploads);
interpreter.replace("import { setAttributeInner } from \"./common.js\";", "")
});
static COMMON_JS: Lazy<String> = Lazy::new(|| {
let common = dioxus_interpreter_js::COMMON_JS;
common.replace("export", "")
});
static MAIN_JS: &str = include_str!("./main.js");
static MINIFIED: &str = include_str!("./minified.js");
/// This script that gets injected into your app connects this page to the websocket endpoint
///
@@ -131,8 +85,6 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
"return path;"
};
let js = &*INTERPRETER_JS;
let common = &*COMMON_JS;
format!(
r#"
<script>
@@ -141,10 +93,7 @@ pub fn interpreter_glue(url_or_path: &str) -> String {
}}
var WS_ADDR = __dioxusGetWsUrl("{url_or_path}");
{js}
{common}
{MAIN_JS}
main();
{MINIFIED}
</script>
"#
)
+31 -16
View File
@@ -1,3 +1,5 @@
const config = new InterpreterConfig(false);
function main() {
let root = window.document.getElementById("main");
if (root != null) {
@@ -7,10 +9,9 @@ function main() {
class IPC {
constructor(root) {
// connect to the websocket
window.interpreter = new Interpreter(root, new InterpreterConfig(false));
let ws = new WebSocket(WS_ADDR);
window.interpreter.initialize(root);
const ws = new WebSocket(WS_ADDR);
ws.binaryType = "arraybuffer";
function ping() {
ws.send("__ping__");
@@ -19,7 +20,7 @@ class IPC {
ws.onopen = () => {
// we ping every 30 seconds to keep the websocket alive
setInterval(ping, 30000);
ws.send(serializeIpcMessage("initialize"));
ws.send(window.interpreter.serializeIpcMessage("initialize"));
};
ws.onerror = (err) => {
@@ -27,17 +28,29 @@ class IPC {
};
ws.onmessage = (message) => {
// Ignore pongs
if (message.data != "__pong__") {
const event = JSON.parse(message.data);
switch (event.type) {
case "edits":
let edits = event.data;
window.interpreter.handleEdits(edits);
break;
case "query":
Function("Eval", `"use strict";${event.data};`)();
break;
const u8view = new Uint8Array(message.data);
const binaryFrame = u8view[0] == 1;
const messageData = message.data.slice(1)
// The first byte tells the shim if this is a binary of text frame
if (binaryFrame) {
// binary frame
run_from_bytes(messageData);
}
else {
// text frame
let decoder = new TextDecoder("utf-8");
// Using decode method to get string output
let str = decoder.decode(messageData);
// Ignore pongs
if (str != "__pong__") {
const event = JSON.parse(str);
switch (event.type) {
case "query":
Function("Eval", `"use strict";${event.data};`)();
break;
}
}
}
};
@@ -49,3 +62,5 @@ class IPC {
this.ws.send(msg);
}
}
main();
+168 -19
View File
@@ -5,9 +5,11 @@ use crate::{
query::{QueryEngine, QueryResult},
LiveViewError,
};
use dioxus_core::{prelude::*, Mutations};
use dioxus_html::{EventData, HtmlEvent, MountedData, PlatformEventData};
use dioxus_core::{prelude::*, BorrowedAttributeValue, Mutations};
use dioxus_html::{event_bubbles,EventData, HtmlEvent, MountedData, PlatformEventData};
use dioxus_interpreter_js::binary_protocol::Channel;
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;
@@ -111,7 +113,7 @@ impl<S> LiveViewSocket for S where
///
/// 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.
/// As long as your framework can provide a Sink and Stream of Bytes, 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(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), LiveViewError> {
@@ -124,20 +126,31 @@ 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;
// 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());
// todo: use an efficient binary packed format for this
let edits = serde_json::to_string(&ClientUpdate::Edits(vdom.rebuild())).unwrap();
// pin the futures so we can use select!
pin_mut!(ws);
// send the initial render to the client
ws.send(edits.into_bytes()).await?;
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,
)
} {
// 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
@@ -164,7 +177,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
match evt.as_ref().map(|o| o.as_deref()) {
// respond with a pong every ping to keep the websocket alive
Some(Ok(b"__ping__")) => {
ws.send(b"__pong__".to_vec()).await?;
ws.send(text_frame("__pong__")).await?;
}
Some(Ok(evt)) => {
if let Ok(message) = serde_json::from_str::<IpcMessage>(&String::from_utf8_lossy(evt)) {
@@ -202,7 +215,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
// handle any new queries
Some(query) = query_rx.recv() => {
ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?;
ws.send(text_frame(&serde_json::to_string(&ClientUpdate::Query(query)).unwrap())).await?;
}
Some(msg) = hot_reload_wait => {
@@ -224,20 +237,156 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
.render_with_deadline(tokio::time::sleep(Duration::from_millis(10)))
.await;
ws.send(
serde_json::to_string(&ClientUpdate::Edits(edits))
.unwrap()
.into_bytes(),
)
.await?;
if let Some(edits) = {
apply_edits(
edits,
&mut edit_channel,
&mut templates,
&mut max_template_count,
)
} {
ws.send(edits).await?;
}
}
}
fn text_frame(text: &str) -> Vec<u8> {
let mut bytes = vec![0];
bytes.extend(text.as_bytes());
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),
}
}
// 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)
}
#[derive(Serialize)]
#[serde(tag = "type", content = "data")]
enum ClientUpdate<'a> {
#[serde(rename = "edits")]
Edits(Mutations<'a>),
enum ClientUpdate {
#[serde(rename = "query")]
Query(String),
}
+1 -4
View File
@@ -169,10 +169,7 @@ pub(crate) struct Query<V: DeserializeOwned> {
impl<V: DeserializeOwned> Query<V> {
/// Resolve the query
pub async fn resolve(mut self) -> Result<V, QueryError> {
match self.receiver.recv().await {
Some(result) => V::deserialize(result).map_err(QueryError::Deserialize),
None => Err(QueryError::Recv(RecvError::Closed)),
}
V::deserialize(self.result().await?).map_err(QueryError::Deserialize)
}
/// Send a message to the query