From 487dba90d83b6d8a35faac5ec6632b59dbda654b Mon Sep 17 00:00:00 2001
From: jquesada2016 <54370171+jquesada2016@users.noreply.github.com>
Date: Sat, 18 Feb 2023 13:57:14 -0600
Subject: [PATCH] fix: off-by-one error in `` (closes #533) (#538)
---
leptos_dom/examples/hydration-test/src/lib.rs | 90 +++----
leptos_dom/examples/test-bench/Cargo.toml | 1 +
leptos_dom/examples/test-bench/src/main.rs | 116 +++------
leptos_dom/examples/test-bench/src/utils.rs | 47 ----
leptos_dom/src/components/each.rs | 230 +++++++++---------
5 files changed, 195 insertions(+), 289 deletions(-)
delete mode 100644 leptos_dom/examples/test-bench/src/utils.rs
diff --git a/leptos_dom/examples/hydration-test/src/lib.rs b/leptos_dom/examples/hydration-test/src/lib.rs
index 40c2fc1f5..bfc37c5cb 100644
--- a/leptos_dom/examples/hydration-test/src/lib.rs
+++ b/leptos_dom/examples/hydration-test/src/lib.rs
@@ -4,55 +4,55 @@ use leptos::*;
#[component]
pub fn App(cx: Scope) -> impl IntoView {
- let pending_thing = create_resource(
- cx,
- || false,
- |_| async {
- if cfg!(feature = "ssr") {
- let (tx, rx) = futures::channel::oneshot::channel();
- spawn_local(async {
- std::thread::sleep(std::time::Duration::from_millis(10));
- tx.send(());
- });
- rx.await;
- } else {
- }
- true
- },
- );
+ let pending_thing = create_resource(
+ cx,
+ || false,
+ |_| async {
+ if cfg!(feature = "ssr") {
+ let (tx, rx) = futures::channel::oneshot::channel();
+ spawn_local(async {
+ std::thread::sleep(std::time::Duration::from_millis(10));
+ tx.send(());
+ });
+ rx.await;
+ } else {
+ }
+ true
+ },
+ );
- view! { cx,
-
+ view! { cx,
- "This is some text"
+
+ "This is some text"
+
+ //
"Loading..." }>
+ {move || pending_thing.read().map(|n| view! { cx, })}
+ //
- //
"Loading..." }>
- {move || pending_thing.read().map(|n| view! { cx, })}
- //
-
- }
+ }
}
#[component]
pub fn ComponentA(cx: Scope) -> impl IntoView {
- let (value, set_value) = create_signal(cx, "Hello?".to_string());
- let (counter, set_counter) = create_signal(cx, 0);
+ let (value, set_value) = create_signal(cx, "Hello?".to_string());
+ let (counter, set_counter) = create_signal(cx, 0);
- // Test to make sure hydration isn't broken by
- // something like this
- //let _ = [div(cx)].into_view(cx);
+ // Test to make sure hydration isn't broken by
+ // something like this
+ //let _ = [div(cx)].into_view(cx);
- div(cx)
- .id("the-div")
- .child(
- input(cx)
- .attr("type", "text")
- .prop("value", (cx, value))
- .on(ev::input, move |e| set_value(event_target_value(&e))),
- )
- .child(input(cx).attr("type", "text").prop("value", value))
- .child(p(cx).child("Value: ").child(value))
- .into_view(cx)
+ div(cx)
+ .id("the-div")
+ .child(
+ input(cx)
+ .attr("type", "text")
+ .prop("value", (cx, value))
+ .on(ev::input, move |e| set_value(event_target_value(&e))),
+ )
+ .child(input(cx).attr("type", "text").prop("value", value))
+ .child(p(cx).child("Value: ").child(value))
+ .into_view(cx)
}
#[cfg(feature = "hydrate")]
@@ -61,11 +61,11 @@ use wasm_bindgen::prelude::wasm_bindgen;
#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
- console_error_panic_hook::set_once();
+ console_error_panic_hook::set_once();
- gloo::console::debug!("starting WASM");
+ gloo::console::debug!("starting WASM");
- leptos::mount_to_body(move |cx| {
- view! { cx, }
- });
+ leptos::mount_to_body(move |cx| {
+ view! { cx, }
+ });
}
diff --git a/leptos_dom/examples/test-bench/Cargo.toml b/leptos_dom/examples/test-bench/Cargo.toml
index 67e132e6a..b3b7da3f9 100644
--- a/leptos_dom/examples/test-bench/Cargo.toml
+++ b/leptos_dom/examples/test-bench/Cargo.toml
@@ -9,6 +9,7 @@ gloo = { version = "0.8", features = ["futures"] }
leptos = { path = "../../../leptos", features = ["tracing"] }
tracing = "0.1"
tracing-subscriber = "0.3"
+tracing-subscriber-wasm = "0.1"
wasm-bindgen-futures = "0.4"
web-sys = "0.3"
diff --git a/leptos_dom/examples/test-bench/src/main.rs b/leptos_dom/examples/test-bench/src/main.rs
index c06183070..b33f06d6d 100644
--- a/leptos_dom/examples/test-bench/src/main.rs
+++ b/leptos_dom/examples/test-bench/src/main.rs
@@ -1,102 +1,42 @@
-#![allow(warnings)]
-
#[macro_use]
extern crate tracing;
-mod utils;
-
use leptos::*;
-use tracing::field::debug;
-use tracing_subscriber::util::SubscriberInitExt;
+use tracing_subscriber::prelude::*;
fn main() {
- console_error_panic_hook::set_once();
+ tracing_subscriber::fmt()
+ .with_writer(tracing_subscriber_wasm::MakeConsoleWriter::default())
+ .without_time()
+ .with_max_level(tracing::Level::TRACE)
+ .pretty()
+ .with_target(false)
+ .init();
- tracing_subscriber::fmt()
- .with_max_level(tracing::Level::TRACE)
- .without_time()
- .with_file(true)
- .with_line_number(true)
- .with_target(false)
- .with_writer(utils::MakeConsoleWriter)
- .with_ansi(false)
- .pretty()
- .finish()
- .init();
-
- mount_to_body(view_fn);
+ mount_to_body(app);
}
-fn view_fn(cx: Scope) -> impl IntoView {
- let view = view! { cx,
-
- }
- .into_view(cx);
+#[instrument]
+fn app(cx: Scope) -> impl IntoView {
+ let (data, set_data) = create_signal(cx, vec![1, 3, 5]);
- let (a, set_a) = create_signal(cx, view.clone());
- let (b, set_b) = create_signal(cx, view);
+ let handle_change = move |_| {
+ set_data.update(|data| {
+ if [1, 3, 5] == data[..] {
+ *data = vec![0, 1, 2, 3, 4, 5, 6];
+ } else {
+ *data = vec![1, 3, 5];
+ }
+ })
+ };
- let (is_a, set_is_a) = create_signal(cx, true);
+ view! { cx,
+
- let handle_toggle = move |_| {
- trace!("toggling");
- if is_a() {
- set_b(a());
-
- set_is_a(false);
- } else {
- set_a(a());
-
- set_is_a(true);
+ {i} }
+ />
}
- };
-
- let a_tag = view! { cx, };
-
- view! { cx,
- <>
-
-
-
-
-
-
-
- >
- }
-}
-
-#[component]
-fn A(cx: Scope, child: Signal) -> impl IntoView {
- move || child()
-}
-
-#[component]
-fn Example(cx: Scope) -> impl IntoView {
- trace!("rendering ");
-
- let (value, set_value) = create_signal(cx, 10);
-
- let memo = create_memo(cx, move |_| value() * 2);
- let derived = Signal::derive(cx, move || value() * 3);
-
- create_effect(cx, move |_| {
- trace!("logging value of derived..., {}", derived.get());
- });
-
- set_timeout(
- move || set_value.update(|v| *v += 1),
- std::time::Duration::from_millis(50),
- );
-
- view! { cx,
- "Example"
-
- }
}
diff --git a/leptos_dom/examples/test-bench/src/utils.rs b/leptos_dom/examples/test-bench/src/utils.rs
deleted file mode 100644
index 2ed7c3c13..000000000
--- a/leptos_dom/examples/test-bench/src/utils.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-pub struct MakeConsoleWriter;
-use std::io::{self, Write};
-use tracing_subscriber::fmt::MakeWriter;
-
-impl<'a> MakeWriter<'a> for MakeConsoleWriter {
- type Writer = ConsoleWriter;
-
- fn make_writer(&'a self) -> Self::Writer {
- unimplemented!("use make_writer_for instead");
- }
-
- fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer {
- ConsoleWriter(*meta.level(), Vec::with_capacity(256))
- }
-}
-
-pub struct ConsoleWriter(tracing::Level, Vec);
-
-impl io::Write for ConsoleWriter {
- fn write(&mut self, buf: &[u8]) -> io::Result {
- self.1.write(buf)
- }
-
- fn flush(&mut self) -> io::Result<()> {
- use gloo::console;
- use tracing::Level;
-
- let data = String::from_utf8(self.1.to_owned())
- .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "data not UTF-8"))?;
-
- match self.0 {
- Level::TRACE => console::debug!(&data),
- Level::DEBUG => console::debug!(&data),
- Level::INFO => console::log!(&data),
- Level::WARN => console::warn!(&data),
- Level::ERROR => console::error!(&data),
- }
-
- Ok(())
- }
-}
-
-impl Drop for ConsoleWriter {
- fn drop(&mut self) {
- let _ = self.flush();
- }
-}
diff --git a/leptos_dom/src/components/each.rs b/leptos_dom/src/components/each.rs
index d4d15027b..1ad09e5bb 100644
--- a/leptos_dom/src/components/each.rs
+++ b/leptos_dom/src/components/each.rs
@@ -347,60 +347,72 @@ where
let (children, closing) =
(component.children.clone(), component.closing.node.clone());
- cfg_if::cfg_if! {
- if #[cfg(all(target_arch = "wasm32", feature = "web"))] {
+ #[cfg(all(target_arch = "wasm32", feature = "web"))]
+ {
create_effect(cx, move |prev_hash_run| {
- let mut children_borrow = children.borrow_mut();
+ let mut children_borrow = children.borrow_mut();
- #[cfg(all(target_arch = "wasm32", feature = "web"))]
- let opening = if let Some(Some(child)) = children_borrow.get(0) {
- child.get_opening_node()
- } else {
- closing.clone()
- };
+ let opening = if let Some(Some(child)) = children_borrow.get(0)
+ {
+ child.get_opening_node()
+ } else {
+ closing.clone()
+ };
- let items = items_fn();
+ let items = items_fn();
- let items = items.into_iter().collect::>();
+ let items = items.into_iter().collect::>();
- let hashed_items =
- items.iter().map(&key_fn).collect::>();
+ let hashed_items =
+ items.iter().map(&key_fn).collect::>();
- if let Some(HashRun(prev_hash_run)) = prev_hash_run {
- let cmds = diff(&prev_hash_run, &hashed_items);
+ if let Some(HashRun(prev_hash_run)) = prev_hash_run {
+ let cmds = diff(&prev_hash_run, &hashed_items);
- apply_cmds(
- cx,
- #[cfg(all(target_arch = "wasm32", feature = "web"))]
- &opening,
- #[cfg(all(target_arch = "wasm32", feature = "web"))]
- &closing,
- cmds,
- &mut children_borrow,
- items.into_iter().map(|t| Some(t)).collect(),
- &each_fn
- );
- } else {
- *children_borrow = Vec::with_capacity(items.len());
+ tracing::debug!("cmds:\n{cmds:#?}");
- for item in items {
- let (each_item, _) = cx.run_child_scope(|cx| EachItem::new(cx, each_fn(cx, item).into_view(cx)));
+ apply_cmds(
+ cx,
+ &opening,
+ &closing,
+ cmds,
+ &mut children_borrow,
+ items.into_iter().map(|t| Some(t)).collect(),
+ &each_fn,
+ );
+ } else {
+ children_borrow.clear();
+ children_borrow.reserve(items.len());
- #[cfg(all(target_arch = "wasm32", feature = "web"))]
- mount_child(MountKind::Before(&closing), &each_item);
+ for item in items {
+ let (each_item, disposer) = cx.run_child_scope(|cx| {
+ EachItem::new(cx, each_fn(cx, item).into_view(cx))
+ });
- children_borrow.push(Some(each_item));
+ mount_child(MountKind::Before(&closing), &each_item);
+
+ children_borrow.push(Some(each_item));
+ }
}
- }
- HashRun(hashed_items)
+ HashRun(hashed_items)
});
- } else {
+ }
+
+ #[cfg(not(all(target_arch = "wasm32", feature = "web")))]
+ {
*component.children.borrow_mut() = (items_fn)()
- .into_iter()
- .map(|child| cx.run_child_scope(|cx| Some(EachItem::new(cx, (each_fn)(cx, child).into_view(cx)))).0)
- .collect();
- }
+ .into_iter()
+ .map(|child| {
+ cx.run_child_scope(|cx| {
+ Some(EachItem::new(
+ cx,
+ (each_fn)(cx, child).into_view(cx),
+ ))
+ })
+ .0
+ })
+ .collect();
}
View::CoreComponent(CoreComponent::Each(component))
@@ -411,7 +423,7 @@ where
#[educe(Debug)]
struct HashRun(#[educe(Debug(ignore))] T);
-/// Calculates the operations need to get from `a` to `b`.
+/// Calculates the operations needed to get from `a` to `b`.
#[cfg(all(target_arch = "wasm32", feature = "web"))]
fn diff(from: &FxIndexSet, to: &FxIndexSet) -> Diff {
if from.is_empty() && to.is_empty() {
@@ -424,72 +436,73 @@ fn diff(from: &FxIndexSet, to: &FxIndexSet) -> Diff {
}
// Get removed items
- let mut removed = from.difference(to);
+ let mut removed =
+ from.difference(to).map(|k| from.get_index_of(k).unwrap());
- let removed_cmds = removed
- .clone()
- .map(|k| from.get_full(k).unwrap().0)
- .map(|idx| DiffOpRemove { at: idx });
+ let removed_cmds = removed.clone().map(|idx| DiffOpRemove { at: idx });
// Get added items
- let mut added = to.difference(from);
+ let mut added = to.difference(from).map(|k| to.get_index_of(k).unwrap());
- let added_cmds =
- added
- .clone()
- .map(|k| to.get_full(k).unwrap().0)
- .map(|idx| DiffOpAdd {
- at: idx,
- mode: Default::default(),
- });
+ let added_cmds = added.clone().map(|idx| DiffOpAdd {
+ at: idx,
+ mode: Default::default(),
+ });
- // Get moved items
- let mut normalized_idx = 0;
- let mut move_cmds = SmallVec::<[_; 8]>::with_capacity(to.len());
- let mut added_idx = added.next().map(|k| to.get_full(k).unwrap().0);
- let mut removed_idx = removed.next().map(|k| from.get_full(k).unwrap().0);
+ let mut normalized_idx = 0i64;
+ let mut next_added_idx = added.next();
+ let mut next_removed_idx = removed.next();
- for (idx, k) in to.iter().enumerate() {
- if let Some(added_idx) = added_idx.as_mut().filter(|r_i| **r_i == idx) {
- if let Some(next_added) =
- added.next().map(|k| to.get_full(k).unwrap().0)
- {
- *added_idx = next_added;
+ let move_cmds = to
+ .iter()
+ .enumerate()
+ .filter_map(|(i, k)| {
+ let is_added = if let Some(idx) = next_added_idx {
+ if i == idx {
+ next_added_idx = added.next();
+ normalized_idx -= 1;
- normalized_idx = usize::wrapping_sub(normalized_idx, 1);
+ true
+ } else {
+ false
+ }
+ } else {
+ false
+ };
+
+ let is_removed = if let Some(idx) = next_removed_idx {
+ if i == idx {
+ next_removed_idx = removed.next();
+ normalized_idx += 1;
+
+ true
+ } else {
+ false
+ }
+ } else {
+ false
+ };
+
+ normalized_idx += 1;
+
+ if !is_added && !is_removed {
+ Some((
+ from.get_index_of(k).unwrap(),
+ i,
+ // We need to `-1` because otherwise, we'd be accounting for
+ // the NEXT iteration, not this current one
+ normalized_idx - 1,
+ ))
+ } else {
+ None
}
- }
-
- if let Some(removed_idx) =
- removed_idx.as_mut().filter(|r_i| **r_i == idx)
- {
- normalized_idx = normalized_idx.wrapping_add(1);
-
- if let Some(next_removed) =
- removed.next().map(|k| from.get_full(k).unwrap().0)
- {
- *removed_idx = next_removed;
- }
- }
-
- if let Some((from_idx, _)) = from.get_full(k) {
- if from_idx != normalized_idx {
- move_cmds.push(DiffOpMove {
- from: from_idx,
- to: idx,
- move_in_dom: true,
- });
- } else if from_idx != idx {
- move_cmds.push(DiffOpMove {
- from: from_idx,
- to: idx,
- move_in_dom: false,
- });
- }
- }
-
- normalized_idx = normalized_idx.wrapping_add(1);
- }
+ })
+ .map(|(from, to, normalized_idx)| DiffOpMove {
+ from,
+ to,
+ move_in_dom: to != normalized_idx as usize,
+ })
+ .collect();
let mut diffs = Diff {
removed: removed_cmds.collect(),
@@ -621,17 +634,16 @@ fn apply_cmds(
if opening.previous_sibling().is_none()
&& closing.next_sibling().is_none()
{
- let parent = closing
+ if let Some(parent) = closing
.parent_node()
- .expect("could not get closing node")
- .unchecked_into::();
- parent.set_text_content(Some(""));
+ .map(JsCast::unchecked_into::)
+ {
+ #[cfg(debug_assertions)]
+ parent.append_with_node_2(opening, closing).unwrap();
- #[cfg(debug_assertions)]
- parent.append_with_node_2(opening, closing).unwrap();
-
- #[cfg(not(debug_assertions))]
- parent.append_with_node_1(closing).unwrap();
+ #[cfg(not(debug_assertions))]
+ parent.append_with_node_1(closing).unwrap();
+ }
} else {
range.set_start_before(opening).unwrap();
range.set_end_before(closing).unwrap();
@@ -664,7 +676,7 @@ fn apply_cmds(
for DiffOpAdd { at, mode } in cmds.added {
let item = items[at].take().unwrap();
- let (each_item, _) = cx.run_child_scope(|cx| {
+ let (each_item, disposer) = cx.run_child_scope(|cx| {
let child = each_fn(cx, item).into_view(cx);
EachItem::new(cx, child)
});