Switch to a pool of dynamic values for hot reloading (#2705)

* create the dynamic value pool

* assign ids to dynamic formatted segments

* separate the rendering and literal pools

* rsx output compiles again

* more examples compiling with new rsx expansion

* update template body explanation

* all workspace examples compile

* fix formatted segments in keys

* start hot reload diffing

* fix component literal hot reloading

* start integrate new hot reloading with the CLI

* simple hot reloads working

* Fix hot reloading blocks with components

* implement hot reloading for if chains

* Fix hot reloading after a template requires a full rebuild

* Fix hot reloading any attribute values

* remove unsafe from hot reload utils

* Fix hot reloading empty rsx

* add more hot reloading tests

* reorganize hot reload module

* fix web hydration

* fix empty rsx nodes in autoformatting

* fix tests

* remove path sorting logic from core

* make template names more consistent in debug mode

* fix quote_as_hot_reload_literal for explicitly typed literals

* fix can_be_shorthand for string literals

* fix formatted single dynamic expression

* Fix usize component properties and playwright tests

* remove default implementation for TemplateBody

* add a bunch more comments for diffing, scoring and why this scoring system is optimal
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-07-30 17:16:27 +00:00
1 parent f3ca1484a1
commit 34bdcd15cf
40 files changed
+2288 -1897

No files matched your search

+1 -1
View File
@@ -467,7 +467,7 @@ impl VNode {
dom: &VirtualDom,
to: &mut impl WriteMutations,
) -> usize {
let template = self.template.get();
let template = self.template;
let mount = dom.mounts.get(self.mount.get().0).unwrap();
+3 -3
View File
@@ -103,11 +103,11 @@ impl VirtualDom {
to: &mut impl WriteMutations,
mut template: Template,
) {
if self.templates.contains_key(template.name) {
if self.templates.contains(&template.name) {
return;
}
_ = self.templates.insert(template.name, template);
_ = self.templates.insert(template.name);
// If it's all dynamic nodes, then we don't need to register it
if !template.is_completely_dynamic() {
@@ -124,7 +124,7 @@ impl VirtualDom {
/// - for appending children we can use AppendChildren
#[allow(dead_code)]
fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
let template = node.template.get();
let template = node.template;
let path = template.node_paths[idx];
// use a loop to index every static node's children until the path has run out
+18 -52
View File
@@ -1,5 +1,5 @@
use crate::innerlude::MountId;
use crate::{Attribute, AttributeValue, DynamicNode::*, Template};
use crate::{Attribute, AttributeValue, DynamicNode::*};
use crate::{VNode, VirtualDom, WriteMutations};
use core::iter::Peekable;
@@ -21,21 +21,6 @@ impl VNode {
// The node we are diffing from should always be mounted
debug_assert!(dom.mounts.get(self.mount.get().0).is_some() || to.is_none());
// If hot reloading is enabled, we need to make sure we're using the latest template
#[cfg(debug_assertions)]
{
let name = new.template.get().name;
if let Some(template) = dom.templates.get(name).cloned() {
new.template.set(template);
if template != self.template.get() {
let mount_id = self.mount.get();
let parent = dom.mounts[mount_id.0].parent;
self.replace(std::slice::from_ref(new), parent, dom, to);
return;
}
}
}
// If the templates are different by name, we need to replace the entire template
if self.templates_are_different(new) {
return self.light_diff_templates(new, dom, to);
@@ -120,7 +105,7 @@ impl VNode {
&self,
root_idx: usize,
) -> Option<(usize, &DynamicNode)> {
self.template.get().roots[root_idx]
self.template.roots[root_idx]
.dynamic_id()
.map(|id| (id, &self.dynamic_nodes[id]))
}
@@ -148,7 +133,7 @@ impl VNode {
pub(crate) fn find_last_element(&self, dom: &VirtualDom) -> ElementId {
let mount = &dom.mounts[self.mount.get().0];
let last_root_index = self.template.get().roots.len() - 1;
let last_root_index = self.template.roots.len() - 1;
match self.get_dynamic_root_node_and_id(last_root_index) {
// This node is static, just get the root id
None | Some((_, Placeholder(_) | Text(_))) => mount.root_ids[last_root_index],
@@ -272,7 +257,7 @@ impl VNode {
destroy_component_state: bool,
replace_with: Option<usize>,
) {
let roots = self.template.get().roots;
let roots = self.template.roots;
for (idx, node) in roots.iter().enumerate() {
let last_node = idx == roots.len() - 1;
if let Some(id) = node.dynamic_id() {
@@ -305,7 +290,7 @@ impl VNode {
dom: &mut VirtualDom,
destroy_component_state: bool,
) {
let template = self.template.get();
let template = self.template;
for (idx, dyn_node) in self.dynamic_nodes.iter().enumerate() {
let path_len = template.node_paths.get(idx).map(|path| path.len());
// Roots are cleaned up automatically above and nodes with a empty path are placeholders
@@ -361,14 +346,14 @@ impl VNode {
}
fn templates_are_different(&self, other: &VNode) -> bool {
let self_node_name = self.template.get().id();
let other_node_name = other.template.get().id();
let self_node_name = self.template.id();
let other_node_name = other.template.id();
self_node_name != other_node_name
}
pub(super) fn reclaim_attributes(&self, mount: MountId, dom: &mut VirtualDom) {
let mut next_id = None;
for (idx, path) in self.template.get().attr_paths.iter().enumerate() {
for (idx, path) in self.template.attr_paths.iter().enumerate() {
// We clean up the roots in the next step, so don't worry about them here
if path.len() <= 1 {
continue;
@@ -399,7 +384,7 @@ impl VNode {
let mut old_attributes_iter = old_attrs.iter().peekable();
let mut new_attributes_iter = new_attrs.iter().peekable();
let attribute_id = dom.mounts[mount_id.0].mounted_attributes[idx];
let path = self.template.get().attr_paths[idx];
let path = self.template.attr_paths[idx];
loop {
match (old_attributes_iter.peek(), new_attributes_iter.peek()) {
@@ -566,21 +551,6 @@ impl VNode {
}
}
/// Get the most up to date template for this rsx block
#[allow(unused)]
pub(crate) fn template(&self, dom: &VirtualDom) -> Template {
// check for a overridden template
#[cfg(debug_assertions)]
{
let template = self.template.get();
if let Some(new_template) = dom.templates.get(template.name) {
self.template.set(*new_template);
}
};
self.template.get()
}
/// Create this rsx block. This will create scopes from components that this rsx block contains, but it will not write anything to the DOM.
pub(crate) fn create(
&self,
@@ -589,7 +559,7 @@ impl VNode {
mut to: Option<&mut impl WriteMutations>,
) -> usize {
// Get the most up to date template
let template = self.template(dom);
let template = self.template;
// Initialize the mount information for this vnode if it isn't already mounted
if !self.mount.get().mounted() {
@@ -616,13 +586,9 @@ impl VNode {
}
// Walk the roots, creating nodes and assigning IDs
// nodes in an iterator of (dynamic_node_index, path)
let nodes_sorted = template.breadth_first_node_paths();
let attrs_sorted = template.breadth_first_attribute_paths();
let mut nodes = nodes_sorted.peekable();
let mut attrs = attrs_sorted.peekable();
// nodes in an iterator of (dynamic_node_index, path) and attrs in an iterator of (attr_index, path)
let mut nodes = template.node_paths.iter().copied().enumerate().peekable();
let mut attrs = template.attr_paths.iter().copied().enumerate().peekable();
// Get the mounted id of this block
// At this point, we should have already mounted the block
@@ -690,7 +656,7 @@ impl VNode {
fn reference_to_dynamic_node(&self, mount: MountId, dynamic_node_id: usize) -> ElementRef {
ElementRef {
path: ElementPath {
path: self.template.get().node_paths[dynamic_node_id],
path: self.template.node_paths[dynamic_node_id],
},
mount,
}
@@ -834,7 +800,7 @@ impl VNode {
let this_id = dom.next_element();
dom.mounts[mount.0].root_ids[root_idx] = this_id;
to.load_template(self.template.get().name, root_idx, this_id);
to.load_template(self.template.name, root_idx, this_id);
this_id
}
@@ -874,7 +840,7 @@ impl VNode {
dom: &mut VirtualDom,
) -> (ElementId, &'static [u8]) {
// Add the mutation to the list
let path = self.template.get().node_paths[idx];
let path = self.template.node_paths[idx];
// Allocate a dynamic element reference for this text node
let new_id = mount.mount_node(idx, dom);
@@ -940,8 +906,8 @@ fn matching_components<'a>(
left: &'a VNode,
right: &'a VNode,
) -> Option<Vec<(&'a VComponent, &'a VComponent)>> {
let left_node = left.template.get();
let right_node = right.template.get();
let left_node = left.template;
let right_node = right.template;
if left_node.roots.len() != right_node.roots.len() {
return None;
}
+417 -19
View File
@@ -1,15 +1,26 @@
#[doc(hidden)]
use std::{
any::{Any, TypeId},
hash::{Hash, Hasher},
};
#[cfg(feature = "serialize")]
use crate::nodes::deserialize_string_leaky;
use crate::{
Attribute, AttributeValue, DynamicNode, Template, TemplateAttribute, TemplateNode, VNode, VText,
};
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone)]
pub struct HotreloadedLiteral {
pub name: String,
pub value: HotReloadLiteral,
}
#[doc(hidden)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone)]
pub enum HotReloadLiteral {
Fmted(FmtedSegments),
@@ -18,12 +29,53 @@ pub enum HotReloadLiteral {
Bool(bool),
}
#[doc(hidden)]
impl HotReloadLiteral {
pub fn as_fmted(&self) -> Option<&FmtedSegments> {
match self {
Self::Fmted(segments) => Some(segments),
_ => None,
}
}
pub fn as_float(&self) -> Option<f64> {
match self {
Self::Float(f) => Some(*f),
_ => None,
}
}
pub fn as_int(&self) -> Option<i64> {
match self {
Self::Int(i) => Some(*i),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
}
impl Hash for HotReloadLiteral {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::Fmted(segments) => segments.hash(state),
Self::Float(f) => f.to_bits().hash(state),
Self::Int(i) => i.hash(state),
Self::Bool(b) => b.hash(state),
}
}
}
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
#[derive(Debug, PartialEq, Eq, Clone)]
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct FmtedSegments {
pub segments: Vec<FmtSegment>,
pub(crate) segments: Vec<FmtSegment>,
}
impl FmtedSegments {
@@ -32,33 +84,23 @@ impl FmtedSegments {
}
/// Render the formatted string by stitching together the segments
pub fn render_with(&self, dynamic_nodes: Vec<String>) -> String {
pub(crate) fn render_with(&self, dynamic_text: &[String]) -> String {
let mut out = String::new();
for segment in &self.segments {
match segment {
FmtSegment::Literal { value } => out.push_str(value),
FmtSegment::Dynamic { id } => out.push_str(&dynamic_nodes[*id]),
FmtSegment::Dynamic { id } => out.push_str(&dynamic_text[*id]),
}
}
out
}
/// Update the segments with new segments
///
/// this will change how we render the formatted string
pub fn update_segments(&mut self, new_segments: Vec<FmtSegment>) {
self.segments = new_segments;
}
}
#[cfg(feature = "serialize")]
use crate::nodes::deserialize_string_leaky;
#[doc(hidden)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, PartialEq, Eq, Clone)]
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum FmtSegment {
Literal {
#[cfg_attr(
@@ -71,3 +113,359 @@ pub enum FmtSegment {
id: usize,
},
}
// let __pool = DynamicValuePool::new(
// vec![...],
// vec![...],
// vec![...],
// );
// VNode::new(
// None,
// Template {
// name: "...",
// roots: &[...],
// node_paths: &[..],
// attr_paths: &[...],
// },
// Box::new([...]),
// Box::new([...]),
// )
// Open questions:
// - How do we handle type coercion for different sized component property integers?
// - Should non-string hot literals go through the centralized pool?
// - Should formatted strings be a runtime concept?
#[doc(hidden)]
pub struct DynamicLiteralPool {
dynamic_text: Box<[String]>,
}
impl DynamicLiteralPool {
pub fn new(dynamic_text: Vec<String>) -> Self {
Self {
dynamic_text: dynamic_text.into_boxed_slice(),
}
}
pub fn get_component_property<'a, T>(
&self,
id: usize,
hot_reload: &'a HotReloadedTemplate,
f: impl FnOnce(&'a HotReloadLiteral) -> Option<T>,
) -> Option<T> {
f(hot_reload.component_values.get(id)?)
}
/// Get a component property of a specific type at the component property index
pub fn component_property<T: 'static>(
&mut self,
id: usize,
hot_reload: &HotReloadedTemplate,
// We pass in the original value for better type inference
// For example, if the original literal is `0i128`, we know the output must be the type `i128`
_coherse_type: T,
) -> T {
fn assert_type<T: 'static, T2: 'static>(t: T) -> T2 {
*(Box::new(t) as Box<dyn Any>).downcast::<T2>().unwrap()
}
let grab_float = || {
self.get_component_property(id, hot_reload, HotReloadLiteral::as_float).expect("Expected a float component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
};
let grab_int = || {
self.get_component_property(id, hot_reload, HotReloadLiteral::as_int).expect("Expected an int component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
};
let grab_bool = || {
self.get_component_property(id, hot_reload, HotReloadLiteral::as_bool).expect("Expected a bool component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
};
let grab_fmted = || {
self.get_component_property(id, hot_reload, |fmted| HotReloadLiteral::as_fmted(fmted).map(|segments| self.render_formatted(segments))).expect("Expected a string component property. This is probably caused by a bug in dioxus hot reloading. Please report this issue.")
};
match TypeId::of::<T>() {
// Any string types that accept a literal
_ if TypeId::of::<String>() == TypeId::of::<T>() => assert_type(grab_fmted()),
_ if TypeId::of::<&'static str>() == TypeId::of::<T>() => {
assert_type(Box::leak(grab_fmted().into_boxed_str()) as &'static str)
}
// Any integer types that accept a literal
_ if TypeId::of::<i128>() == TypeId::of::<T>() => assert_type(grab_int() as i128),
_ if TypeId::of::<i64>() == TypeId::of::<T>() => assert_type(grab_int()),
_ if TypeId::of::<i32>() == TypeId::of::<T>() => assert_type(grab_int() as i32),
_ if TypeId::of::<i16>() == TypeId::of::<T>() => assert_type(grab_int() as i16),
_ if TypeId::of::<i8>() == TypeId::of::<T>() => assert_type(grab_int() as i8),
_ if TypeId::of::<isize>() == TypeId::of::<T>() => assert_type(grab_int() as isize),
_ if TypeId::of::<u128>() == TypeId::of::<T>() => assert_type(grab_int() as u128),
_ if TypeId::of::<u64>() == TypeId::of::<T>() => assert_type(grab_int() as u64),
_ if TypeId::of::<u32>() == TypeId::of::<T>() => assert_type(grab_int() as u32),
_ if TypeId::of::<u16>() == TypeId::of::<T>() => assert_type(grab_int() as u16),
_ if TypeId::of::<u8>() == TypeId::of::<T>() => assert_type(grab_int() as u8),
_ if TypeId::of::<usize>() == TypeId::of::<T>() => assert_type(grab_int() as usize),
// Any float types that accept a literal
_ if TypeId::of::<f64>() == TypeId::of::<T>() => assert_type(grab_float()),
_ if TypeId::of::<f32>() == TypeId::of::<T>() => assert_type(grab_float() as f32),
// Any bool types that accept a literal
_ if TypeId::of::<bool>() == TypeId::of::<T>() => assert_type(grab_bool()),
_ => panic!("Unsupported component property type"),
}
}
pub fn render_formatted(&self, segments: &FmtedSegments) -> String {
segments.render_with(&self.dynamic_text)
}
}
#[doc(hidden)]
pub struct DynamicValuePool {
dynamic_attributes: Box<[Box<[Attribute]>]>,
dynamic_nodes: Box<[DynamicNode]>,
literal_pool: DynamicLiteralPool,
}
impl DynamicValuePool {
pub fn new(
dynamic_nodes: Vec<DynamicNode>,
dynamic_attributes: Vec<Box<[Attribute]>>,
literal_pool: DynamicLiteralPool,
) -> Self {
Self {
dynamic_attributes: dynamic_attributes.into_boxed_slice(),
dynamic_nodes: dynamic_nodes.into_boxed_slice(),
literal_pool,
}
}
pub fn render_with(&mut self, hot_reload: &HotReloadedTemplate) -> VNode {
// Get the node_paths from a depth first traversal of the template
let node_paths = hot_reload.node_paths();
let attr_paths = hot_reload.attr_paths();
let template = Template {
name: hot_reload.name,
roots: hot_reload.roots,
node_paths,
attr_paths,
};
let key = hot_reload
.key
.as_ref()
.map(|key| self.literal_pool.render_formatted(key));
let dynamic_nodes = hot_reload
.dynamic_nodes
.iter()
.map(|node| self.render_dynamic_node(node))
.collect();
let dynamic_attrs = hot_reload
.dynamic_attributes
.iter()
.map(|attr| self.render_attribute(attr))
.collect();
VNode::new(key, template, dynamic_nodes, dynamic_attrs)
}
fn render_dynamic_node(&mut self, node: &HotReloadDynamicNode) -> DynamicNode {
match node {
// If the node is dynamic, take it from the pool and return it
HotReloadDynamicNode::Dynamic(id) => self.dynamic_nodes[*id].clone(),
// Otherwise, format the text node and return it
HotReloadDynamicNode::Formatted(segments) => DynamicNode::Text(VText {
value: self.literal_pool.render_formatted(segments),
}),
}
}
fn render_attribute(&mut self, attr: &HotReloadDynamicAttribute) -> Box<[Attribute]> {
match attr {
HotReloadDynamicAttribute::Dynamic(id) => self.dynamic_attributes[*id].clone(),
HotReloadDynamicAttribute::Named(NamedAttribute {
name,
namespace,
value,
}) => Box::new([Attribute {
name,
namespace: *namespace,
value: match value {
HotReloadAttributeValue::Literal(HotReloadLiteral::Fmted(segments)) => {
AttributeValue::Text(self.literal_pool.render_formatted(segments))
}
HotReloadAttributeValue::Literal(HotReloadLiteral::Float(f)) => {
AttributeValue::Float(*f)
}
HotReloadAttributeValue::Literal(HotReloadLiteral::Int(i)) => {
AttributeValue::Int(*i)
}
HotReloadAttributeValue::Literal(HotReloadLiteral::Bool(b)) => {
AttributeValue::Bool(*b)
}
HotReloadAttributeValue::Dynamic(id) => {
self.dynamic_attributes[*id][0].value.clone()
}
},
volatile: false,
}]),
}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
pub struct HotReloadTemplateWithLocation {
pub location: String,
pub template: HotReloadedTemplate,
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct HotReloadedTemplate {
pub name: &'static str,
pub key: Option<FmtedSegments>,
pub dynamic_nodes: Vec<HotReloadDynamicNode>,
pub dynamic_attributes: Vec<HotReloadDynamicAttribute>,
pub component_values: Vec<HotReloadLiteral>,
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "crate::nodes::deserialize_leaky")
)]
pub roots: &'static [TemplateNode],
}
impl HotReloadedTemplate {
pub fn new(
name: &'static str,
key: Option<FmtedSegments>,
dynamic_nodes: Vec<HotReloadDynamicNode>,
dynamic_attributes: Vec<HotReloadDynamicAttribute>,
component_values: Vec<HotReloadLiteral>,
roots: &'static [TemplateNode],
) -> Self {
Self {
name,
key,
dynamic_nodes,
dynamic_attributes,
component_values,
roots,
}
}
fn node_paths(&self) -> &'static [&'static [u8]] {
fn add_node_paths(
roots: &[TemplateNode],
node_paths: &mut Vec<&'static [u8]>,
current_path: Vec<u8>,
) {
for (idx, node) in roots.iter().enumerate() {
let mut path = current_path.clone();
path.push(idx as u8);
match node {
TemplateNode::Element { children, .. } => {
add_node_paths(children, node_paths, path);
}
TemplateNode::Text { .. } => {}
TemplateNode::Dynamic { id } => {
debug_assert_eq!(node_paths.len(), *id);
node_paths.push(Box::leak(path.into_boxed_slice()));
}
}
}
}
let mut node_paths = Vec::new();
add_node_paths(self.roots, &mut node_paths, Vec::new());
let leaked: &'static [&'static [u8]] = Box::leak(node_paths.into_boxed_slice());
leaked
}
fn attr_paths(&self) -> &'static [&'static [u8]] {
fn add_attr_paths(
roots: &[TemplateNode],
attr_paths: &mut Vec<&'static [u8]>,
current_path: Vec<u8>,
) {
for (idx, node) in roots.iter().enumerate() {
let mut path = current_path.clone();
path.push(idx as u8);
if let TemplateNode::Element {
children, attrs, ..
} = node
{
for attr in *attrs {
if let TemplateAttribute::Dynamic { id } = attr {
debug_assert_eq!(attr_paths.len(), *id);
attr_paths.push(Box::leak(path.clone().into_boxed_slice()));
}
}
add_attr_paths(children, attr_paths, path);
}
}
}
let mut attr_paths = Vec::new();
add_attr_paths(self.roots, &mut attr_paths, Vec::new());
let leaked: &'static [&'static [u8]] = Box::leak(attr_paths.into_boxed_slice());
leaked
}
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
pub enum HotReloadDynamicNode {
Dynamic(usize),
Formatted(FmtedSegments),
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
pub enum HotReloadDynamicAttribute {
Dynamic(usize),
Named(NamedAttribute),
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct NamedAttribute {
/// The name of this attribute.
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "crate::nodes::deserialize_string_leaky")
)]
name: &'static str,
/// The namespace of this attribute. Does not exist in the HTML spec
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "crate::nodes::deserialize_option_leaky")
)]
namespace: Option<&'static str>,
value: HotReloadAttributeValue,
}
impl NamedAttribute {
pub fn new(
name: &'static str,
namespace: Option<&'static str>,
value: HotReloadAttributeValue,
) -> Self {
Self {
name,
namespace,
value,
}
}
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Clone, Hash)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(bound(deserialize = "'de: 'static")))]
pub enum HotReloadAttributeValue {
Literal(HotReloadLiteral),
Dynamic(usize),
}
+4 -1
View File
@@ -34,8 +34,11 @@ mod hotreload_utils;
pub mod internal {
pub use crate::properties::verify_component_called_as_component;
#[doc(hidden)]
pub use crate::hotreload_utils::{
FmtSegment, FmtedSegments, HotReloadLiteral, HotreloadedLiteral,
DynamicLiteralPool, DynamicValuePool, FmtSegment, FmtedSegments, HotReloadAttributeValue,
HotReloadDynamicAttribute, HotReloadDynamicNode, HotReloadLiteral,
HotReloadTemplateWithLocation, HotReloadedTemplate, HotreloadedLiteral, NamedAttribute,
};
}
+22 -109
View File
@@ -6,7 +6,6 @@ use crate::{
properties::ComponentFunction,
};
use crate::{Properties, ScopeId, VirtualDom};
use core::panic;
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use std::vec;
@@ -122,7 +121,7 @@ pub struct VNodeInner {
pub key: Option<String>,
/// The static nodes and static descriptor of the template
pub template: Cell<Template>,
pub template: Template,
/// The dynamic nodes in the template
pub dynamic_nodes: Box<[DynamicNode]>,
@@ -252,12 +251,12 @@ impl VNode {
key: None,
dynamic_nodes: Box::new([DynamicNode::Placeholder(Default::default())]),
dynamic_attrs: Box::new([]),
template: Cell::new(Template {
template: Template {
name: "packages/core/nodes.rs:198:0:0",
roots: &[TemplateNode::Dynamic { id: 0 }],
node_paths: &[&[0]],
attr_paths: &[],
}),
},
})
})
.clone()
@@ -278,7 +277,7 @@ impl VNode {
Self {
vnode: Rc::new(VNodeInner {
key,
template: Cell::new(template),
template,
dynamic_nodes,
dynamic_attrs,
}),
@@ -290,7 +289,7 @@ impl VNode {
///
/// Returns [`None`] if the root is actually a static node (Element/Text)
pub fn dynamic_root(&self, idx: usize) -> Option<&DynamicNode> {
self.template.get().roots[idx]
self.template.roots[idx]
.dynamic_id()
.map(|id| &self.dynamic_nodes[id])
}
@@ -411,7 +410,7 @@ where
}
#[cfg(feature = "serialize")]
fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
pub(crate) fn deserialize_leaky<'a, 'de, T, D>(deserializer: D) -> Result<&'a [T], D::Error>
where
T: serde::Deserialize<'de>,
D: serde::Deserializer<'de>,
@@ -423,7 +422,9 @@ where
}
#[cfg(feature = "serialize")]
fn deserialize_option_leaky<'a, 'de, D>(deserializer: D) -> Result<Option<&'static str>, D::Error>
pub(crate) fn deserialize_option_leaky<'a, 'de, D>(
deserializer: D,
) -> Result<Option<&'static str>, D::Error>
where
D: serde::Deserializer<'de>,
{
@@ -448,36 +449,6 @@ impl Template {
let ptr: *const str = self.name;
ptr as *const () as usize
}
/// Iterate over the attribute paths in order along with the original indexes for each path
pub(crate) fn breadth_first_attribute_paths(
&self,
) -> impl Iterator<Item = (usize, &'static [u8])> {
// In release mode, hot reloading is disabled and everything is in breadth first order already
#[cfg(not(debug_assertions))]
{
self.attr_paths.iter().copied().enumerate()
}
// If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
#[cfg(debug_assertions)]
{
sort_bfo(self.attr_paths).into_iter()
}
}
/// Iterate over the node paths in order along with the original indexes for each path
pub(crate) fn breadth_first_node_paths(&self) -> impl Iterator<Item = (usize, &'static [u8])> {
// In release mode, hot reloading is disabled and everything is in breadth first order already
#[cfg(not(debug_assertions))]
{
self.node_paths.iter().copied().enumerate()
}
// If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
#[cfg(debug_assertions)]
{
sort_bfo(self.node_paths).into_iter()
}
}
}
/// A statically known node in a layout.
@@ -523,7 +494,10 @@ pub enum TemplateNode {
/// This template node is just a piece of static text
Text {
/// The actual text
#[serde(deserialize_with = "deserialize_string_leaky")]
#[cfg_attr(
feature = "serialize",
serde(deserialize_with = "deserialize_string_leaky")
)]
text: &'static str,
},
@@ -548,7 +522,7 @@ impl TemplateNode {
/// A node created at runtime
///
/// This node's index in the DynamicNode list on VNode should match its respective `Dynamic` index
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum DynamicNode {
/// A component node
///
@@ -692,8 +666,10 @@ pub struct VText {
impl VText {
/// Create a new VText
pub fn new(value: String) -> Self {
Self { value }
pub fn new(value: impl ToString) -> Self {
Self {
value: value.to_string(),
}
}
}
@@ -783,6 +759,7 @@ impl Attribute {
///
/// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`]
/// variant.
#[derive(Clone)]
pub enum AttributeValue {
/// Text attribute
Text(String),
@@ -800,7 +777,7 @@ pub enum AttributeValue {
Listener(ListenerCb),
/// An arbitrary value that implements PartialEq and is static
Any(Box<dyn AnyValue>),
Any(Rc<dyn AnyValue>),
/// A "none" value, resulting in the removal of an attribute from the dom
None,
@@ -824,7 +801,7 @@ impl AttributeValue {
/// Create a new [`AttributeValue`] with a value that implements [`AnyValue`]
pub fn any_value<T: AnyValue>(value: T) -> AttributeValue {
AttributeValue::Any(Box::new(value))
AttributeValue::Any(Rc::new(value))
}
}
@@ -859,19 +836,6 @@ impl PartialEq for AttributeValue {
}
}
impl Clone for AttributeValue {
fn clone(&self) -> Self {
match self {
Self::Text(arg0) => Self::Text(arg0.clone()),
Self::Float(arg0) => Self::Float(*arg0),
Self::Int(arg0) => Self::Int(*arg0),
Self::Bool(arg0) => Self::Bool(*arg0),
Self::Listener(_) | Self::Any(_) => panic!("Cannot clone listener or any value"),
Self::None => Self::None,
}
}
}
#[doc(hidden)]
pub trait AnyValue: 'static {
fn any_cmp(&self, other: &dyn AnyValue) -> bool;
@@ -1117,7 +1081,7 @@ impl IntoAttributeValue for Arguments<'_> {
}
}
impl IntoAttributeValue for Box<dyn AnyValue> {
impl IntoAttributeValue for Rc<dyn AnyValue> {
fn into_value(self) -> AttributeValue {
AttributeValue::Any(self)
}
@@ -1143,54 +1107,3 @@ pub trait HasAttributes {
volatile: bool,
) -> Self;
}
#[cfg(debug_assertions)]
pub(crate) fn sort_bfo(paths: &[&'static [u8]]) -> Vec<(usize, &'static [u8])> {
let mut with_indices = paths.iter().copied().enumerate().collect::<Vec<_>>();
with_indices.sort_by(|(_, a), (_, b)| {
let mut a = a.iter();
let mut b = b.iter();
loop {
match (a.next(), b.next()) {
(Some(a), Some(b)) => {
if a != b {
return a.cmp(b);
}
}
// The shorter path goes first
(None, Some(_)) => return std::cmp::Ordering::Less,
(Some(_), None) => return std::cmp::Ordering::Greater,
(None, None) => return std::cmp::Ordering::Equal,
}
}
});
with_indices
}
#[test]
#[cfg(debug_assertions)]
fn sorting() {
let r: [(usize, &[u8]); 5] = [
(0, &[0, 1]),
(1, &[0, 2]),
(2, &[1, 0]),
(3, &[1, 0, 1]),
(4, &[1, 2]),
];
assert_eq!(
sort_bfo(&[&[0, 1,], &[0, 2,], &[1, 0,], &[1, 0, 1,], &[1, 2,],]),
r
);
let r: [(usize, &[u8]); 6] = [
(0, &[0]),
(1, &[0, 1]),
(2, &[0, 1, 2]),
(3, &[1]),
(4, &[1, 2]),
(5, &[2]),
];
assert_eq!(
sort_bfo(&[&[0], &[0, 1], &[0, 1, 2], &[1], &[1, 2], &[2],]),
r
);
}
+7 -61
View File
@@ -18,7 +18,7 @@ use crate::{
};
use crate::{Task, VComponent};
use futures_util::StreamExt;
use rustc_hash::FxHashMap;
use rustc_hash::FxHashSet;
use slab::Slab;
use std::collections::BTreeSet;
use std::{any::Any, rc::Rc};
@@ -208,8 +208,8 @@ pub struct VirtualDom {
pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
// A map of overridden templates?
pub(crate) templates: FxHashMap<TemplateId, Template>,
// A map of templates we have sent to the renderer
pub(crate) templates: FxHashSet<TemplateId>,
// Templates changes that are queued for the next render
pub(crate) queued_templates: Vec<Template>,
@@ -572,60 +572,6 @@ impl VirtualDom {
}
}
/// Replace a template at runtime. This will re-render all components that use this template.
/// This is the primitive that enables hot-reloading.
///
/// The caller must ensure that the template references the same dynamic attributes and nodes as the original template.
///
/// This will only replace the parent template, not any nested templates.
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
pub fn replace_template(&mut self, template: Template) {
// we only replace templates if hot reloading is enabled
#[cfg(debug_assertions)]
{
// Save the template ID
self.templates.insert(template.name, template);
// Only queue the template to be written if its not completely dynamic
if !template.is_completely_dynamic() {
self.queued_templates.push(template);
}
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
let mut dirty = Vec::new();
for (id, scope) in self.scopes.iter() {
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
fn check_node_for_templates(node: &crate::VNode, template: Template) -> bool {
if node.template.get().name == template.name {
return true;
}
for dynamic in node.dynamic_nodes.iter() {
if let crate::DynamicNode::Fragment(nodes) = dynamic {
for node in nodes {
if check_node_for_templates(node, template) {
return true;
}
}
}
}
false
}
if let Some(sync) = scope.try_root_node() {
if check_node_for_templates(sync, template) {
dirty.push(ScopeId(id));
}
}
}
for dirty in dirty {
self.mark_dirty(dirty);
}
}
}
/// Rebuild the virtualdom without handling any of the mutations
///
/// This is useful for testing purposes and in cases where you render the output of the virtualdom without
@@ -885,11 +831,11 @@ impl VirtualDom {
return;
};
let el_ref = &mount.node;
let node_template = el_ref.template.get();
let node_template = el_ref.template;
let target_path = path.path;
// Accumulate listeners into the listener list bottom to top
for (idx, this_path) in node_template.breadth_first_attribute_paths() {
for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
let attrs = &*el_ref.dynamic_attrs[idx];
for attr in attrs.iter() {
@@ -943,10 +889,10 @@ impl VirtualDom {
return;
};
let el_ref = &mount.node;
let node_template = el_ref.template.get();
let node_template = el_ref.template;
let target_path = node.path;
for (idx, this_path) in node_template.breadth_first_attribute_paths() {
for (idx, this_path) in node_template.attr_paths.iter().enumerate() {
let attrs = &*el_ref.dynamic_attrs[idx];
for attr in attrs.iter() {