SSR/hydration example!

This commit is contained in:
Greg Johnston 2022-09-03 17:53:18 -04:00
parent 22b4c78804
commit 812c3a2045
20 changed files with 283 additions and 128 deletions

View file

@ -3,6 +3,18 @@ name = "todomvc-ssr-client"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
todomvc = { path = "../../todomvc", features = ["hydrate"] }
console_log = "0.2"
leptos = { path = "../../../leptos", features = ["hydrate"] }
todomvc = { path = "../../todomvc", features = ["hydrate"] }
log = "0.4"
wasm-bindgen = "0.2"
wee_alloc = "0.4"
[profile.release]
codegen-units = 1
lto = true
opt-level = 'z'

View file

@ -0,0 +1,24 @@
use leptos::*;
use todomvc::*;
use wasm_bindgen::prelude::wasm_bindgen;
extern crate wee_alloc;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
pub fn main() {
console_log::init_with_level(log::Level::Debug);
log::debug!("initialized logging");
leptos::hydrate(body().unwrap(), |cx| {
// initial state — identical to server
let todos = Todos(vec![
Todo::new(cx, 0, "Buy milk".to_string()),
Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string()),
]);
view! { <TodoMVC todos=todos/> }
});
}

View file

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View file

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
actix-files = "0.6"
actix-web = "4"
leptos = { path = "../../../leptos", features = ["ssr"] }
todomvc = { path = "../../todomvc", features = ["ssr"] }
todomvc = { path = "../../todomvc", default-features = false, features = ["ssr"] }

View file

@ -1,20 +1,43 @@
use std::{cell::RefCell, rc::Rc};
use actix_files::{Directory, Files, NamedFile};
use actix_web::*;
use leptos::*;
use todomvc::*;
// TODO
// - WASM/JS routes
#[get("/")]
async fn render_todomvc() -> impl Responder {
let mut buffer: String;
_ = create_scope(|cx| {
let todos = Todos(vec![
Todo::new(cx, 0, "Buy milk".to_string()),
Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string())
]);
HttpResponse::Ok().content_type("text/html").body(format!(
"<!DOCTYPE html>{}",
run_scope({
|cx| {
let todos = Todos(vec![
Todo::new(cx, 0, "Buy milk".to_string()),
Todo::new(cx, 1, "???".to_string()),
Todo::new(cx, 2, "Profit!".to_string()),
]);
buffer = view! { <div id="root"><TodoMVC todos=todos/></div> };
});
buffer
view! {
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/todomvc-common/base.css"/>
<link rel="stylesheet" href="/static/todomvc-app-css/index.css"/>
<title>"Leptos • TodoMVC"</title>
</head>
<body>
<TodoMVC todos=todos/>
</body>
<script type="module">r#"import init, {{ main }} from './pkg/todomvc_ssr_client.js'; init().then(main);"#</script>
</html>
}
}
})
))
}
#[actix_web::main]
@ -22,8 +45,10 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(render_todomvc)
.service(Files::new("/static", "../../todomvc/node_modules"))
.service(Files::new("/pkg", "../todomvc-ssr-client/pkg"))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
}

View file

@ -8,7 +8,7 @@ leptos = { path = "../../leptos" }
wee_alloc = "0.4"
miniserde = "0.1"
log = "0.4"
console_log = "0.1"
console_log = "0.2"
[dev-dependencies]
wasm-bindgen-test = "0.3.0"
@ -19,6 +19,7 @@ lto = true
opt-level = 'z'
[features]
default = ["csr"]
csr = ["leptos/csr"]
hydrate = ["leptos/hydrate"]
ssr = ["leptos/ssr"]

View file

@ -13,23 +13,21 @@ impl Todos {
pub fn new(cx: Scope) -> Self {
let starting_todos = if is_server!() {
Vec::new()
} else if let Ok(Some(storage)) = window().local_storage() {
storage
.get_item(STORAGE_KEY)
.ok()
.flatten()
.and_then(|value| json::from_str::<Vec<TodoSerialized>>(&value).ok())
.map(|values| {
values
.into_iter()
.map(|stored| stored.into_todo(cx))
.collect()
})
.unwrap_or_default()
} else {
if let Ok(Some(storage)) = window().local_storage() {
storage
.get_item(STORAGE_KEY)
.ok()
.flatten()
.and_then(|value| json::from_str::<Vec<TodoSerialized>>(&value).ok())
.map(|values| {
values
.into_iter()
.map(|stored| stored.into_todo(cx))
.collect()
})
.unwrap_or_default()
} else {
Vec::new()
}
Vec::new()
};
Self(starting_todos)
}
@ -39,7 +37,7 @@ impl Todos {
}
pub fn add(&mut self, todo: Todo) {
self.0.push(todo);
self.0.push(todo);
}
pub fn remove(&mut self, id: usize) {
@ -47,17 +45,11 @@ impl Todos {
}
pub fn remaining(&self) -> usize {
self.0
.iter()
.filter(|todo| !(todo.completed)())
.count()
self.0.iter().filter(|todo| !(todo.completed)()).count()
}
pub fn completed(&self) -> usize {
self.0
.iter()
.filter(|todo| (todo.completed)())
.count()
self.0.iter().filter(|todo| (todo.completed)()).count()
}
pub fn toggle_all(&self) {
@ -67,13 +59,13 @@ impl Todos {
if todo.completed.get() {
(todo.set_completed)(|completed| *completed = false);
}
};
}
}
// otherwise, mark them all complete
else {
for todo in &self.0 {
for todo in &self.0 {
(todo.set_completed)(|completed| *completed = true);
};
}
}
}
@ -118,6 +110,14 @@ const ENTER_KEY: u32 = 13;
#[component]
pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
let mut next_id = todos
.0
.iter()
.map(|todo| todo.id)
.max()
.map(|last| last + 1)
.unwrap_or(0);
let (todos, set_todos) = create_signal(cx, todos);
provide_context(cx, set_todos);
@ -127,7 +127,6 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
set_mode(|mode| *mode = new_mode);
});
let mut next_id = 0;
let add_todo = move |ev: web_sys::Event| {
let target = event_target::<HtmlInputElement>(&ev);
ev.stop_propagation();
@ -144,20 +143,20 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
};
let filtered_todos = create_memo::<Vec<Todo>>(cx, move |_| {
todos.with(|todos| {
match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos.0
.iter()
.filter(|todo| !todo.completed.get())
.cloned()
.collect(),
Mode::Completed => todos.0
.iter()
.filter(|todo| todo.completed.get())
.cloned()
.collect(),
}
todos.with(|todos| match mode.get() {
Mode::All => todos.0.to_vec(),
Mode::Active => todos
.0
.iter()
.filter(|todo| !todo.completed.get())
.cloned()
.collect(),
Mode::Completed => todos
.0
.iter()
.filter(|todo| todo.completed.get())
.cloned()
.collect(),
})
});
@ -165,7 +164,9 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
// this does reactive reads, so it will automatically serialize on any relevant change
create_effect(cx, move |_| {
if let Ok(Some(storage)) = window().local_storage() {
let objs = todos.get().0
let objs = todos
.get()
.0
.iter()
.map(TodoSerialized::from)
.collect::<Vec<_>>();
@ -230,11 +231,9 @@ pub fn TodoMVC(cx: Scope, todos: Todos) -> Vec<Element> {
#[component]
pub fn Todo(cx: Scope, todo: Todo) -> Element {
// creates a scope-bound reference to the Todo
// this allows us to move the reference into closures below without cloning it
let (editing, set_editing) = create_signal(cx, false);
let set_todos = use_context::<WriteSignal<Todos>>(cx).unwrap();
let input: web_sys::Element;
let input: Element;
let save = move |value: &str| {
let value = value.trim();
@ -288,6 +287,7 @@ pub fn Todo(cx: Scope, todo: Todo) -> Element {
</li>
};
#[cfg(not(feature = "ssr"))]
create_effect(cx, move |_| {
if editing() {
_ = input.unchecked_ref::<HtmlInputElement>().focus();

View file

@ -1,8 +1,12 @@
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
mod for_component;
mod map;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
mod suspense;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
pub use for_component::*;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
pub use suspense::*;
pub trait Prop {

View file

@ -1,6 +1,8 @@
use std::rc::Rc;
use std::{cell::RefCell, rc::Rc};
use leptos_reactive::Scope;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use wasm_bindgen::JsCast;
use crate::Node;
@ -9,7 +11,7 @@ use crate::Node;
pub enum Child {
Null,
Text(String),
Fn(Rc<dyn Fn() -> Child>),
Fn(Rc<RefCell<dyn FnMut() -> Child>>),
Node(Node),
Nodes(Vec<Node>),
}
@ -21,9 +23,9 @@ impl Child {
Child::Null => String::new(),
Child::Text(text) => text.to_string(),
Child::Fn(f) => {
let mut value = f();
let mut value = (f.borrow_mut())();
while let Child::Fn(f) = value {
value = f();
value = (f.borrow_mut())();
}
value.as_child_string()
}
@ -106,8 +108,7 @@ where
}
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
impl IntoChild for Vec<web_sys::Node> {
impl IntoChild for Vec<Node> {
fn into_child(self, _cx: Scope) -> Child {
Child::Nodes(self)
}
@ -126,11 +127,11 @@ impl IntoChild for Vec<web_sys::Element> {
impl<T, U> IntoChild for T
where
T: Fn() -> U + 'static,
T: FnMut() -> U + 'static,
U: IntoChild,
{
fn into_child(self, cx: Scope) -> Child {
let modified_fn = Rc::new(move || (self)().into_child(cx));
fn into_child(mut self, cx: Scope) -> Child {
let modified_fn = Rc::new(RefCell::new(move || (self)().into_child(cx)));
Child::Fn(modified_fn)
}
}

View file

@ -1,10 +1,9 @@
mod attribute;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
mod child;
mod class;
#[cfg(any(feature = "csr", feature = "hydrate"))]
mod event_delegation;
pub mod logging;
#[cfg(any(feature = "csr", feature = "hydrate"))]
mod operations;
mod property;
#[cfg(any(feature = "csr", feature = "hydrate"))]
@ -13,10 +12,10 @@ mod reconcile;
mod render;
pub use attribute::*;
#[cfg(any(feature = "csr", feature = "hydrate", feature = "ssr"))]
pub use child::*;
pub use class::*;
pub use logging::*;
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub use operations::*;
pub use property::*;
#[cfg(any(feature = "csr", feature = "hydrate"))]
@ -82,7 +81,7 @@ where
});
}
#[cfg(any(feature = "csr", feature = "hydrate"))]
#[cfg(feature = "hydrate")]
pub fn hydrate<T, F>(parent: web_sys::HtmlElement, f: F)
where
F: Fn(Scope) -> T + 'static,
@ -99,7 +98,7 @@ where
pub fn create_component<F, T>(cx: Scope, f: F) -> T
where
F: Fn() -> T,
F: FnOnce() -> T,
{
// TODO hydration logic here
cx.untrack(f)
@ -108,7 +107,7 @@ where
#[macro_export]
macro_rules! is_server {
() => {
cfg!(feature = "server")
cfg!(feature = "ssr")
};
}

View file

@ -239,15 +239,15 @@ pub fn add_event_listener(
.unwrap_throw(); */
}
pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event)) {
let boxed: Box<dyn FnMut(web_sys::Event)> = Box::new(cb);
// Safety: see add_event_listener above
let handler: Box<dyn FnMut(web_sys::Event) + 'static> = unsafe { std::mem::transmute(boxed) };
pub fn window_event_listener(event_name: &str, cb: impl Fn(web_sys::Event) + 'static) {
if !is_server!() {
let handler = Box::new(cb) as Box<dyn FnMut(web_sys::Event)>;
let cb = Closure::wrap(handler).into_js_value();
window()
.add_event_listener_with_callback(event_name, cb.unchecked_ref())
.unwrap_throw();
let cb = Closure::wrap(handler).into_js_value();
window()
.add_event_listener_with_callback(event_name, cb.unchecked_ref())
.unwrap_throw();
}
}
// Hydration operations to find text and comment nodes

View file

@ -128,11 +128,11 @@ pub fn insert(
.unwrap_or_else(|| initial.clone())
.unwrap_or(Child::Null);
let mut value = f();
let mut value = (f.borrow_mut())();
if current != value {
while let Child::Fn(f) = value {
value = f();
value = (f.borrow_mut())();
}
Some(insert_expression(
@ -243,9 +243,9 @@ pub fn insert_expression(
// Nested Signals here simply won't do anything; they should be flattened so it's a single Signal
Child::Fn(f) => {
let mut value = f();
let mut value = (f.borrow_mut())();
while let Child::Fn(f) = value {
value = f();
value = (f.borrow_mut())();
}
value
}

View file

@ -83,12 +83,14 @@ fn root_element_to_tokens(template_uid: &Ident, node: &Node, mode: Mode) -> Toke
match mode {
Mode::Ssr => {
quote! {
quote! {{
#(#navigations);*;
format!(
#template,
#(#expressions),*
)
}
}}
}
_ => {
// create the root element from which navigations and expressions will begin
@ -165,7 +167,14 @@ fn element_to_tokens(
// attributes
for attr in &node.attributes {
attr_to_tokens(attr, &this_el_ident, template, expressions, mode);
attr_to_tokens(
attr,
&this_el_ident,
template,
expressions,
navigations,
mode,
);
}
// navigation for this el
@ -187,7 +196,9 @@ fn element_to_tokens(
let #this_el_ident = #parent.first_child().unwrap_throw();
}
};
navigations.push(this_nav);
if mode != Mode::Ssr {
navigations.push(this_nav);
}
// self-closing tags
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
@ -261,6 +272,7 @@ fn attr_to_tokens(
el_id: &Ident,
template: &mut String,
expressions: &mut Vec<TokenStream>,
navigations: &mut Vec<TokenStream>,
mode: Mode,
) {
let name = node
@ -283,8 +295,26 @@ fn attr_to_tokens(
// refs
if name == "ref" {
// refs mean nothing in SSR
if mode != Mode::Ssr {
let ident = match &node.value {
Some(expr) => {
if let Some(ident) = expr_to_ident(expr) {
quote_spanned! { span => #ident }
} else {
quote_spanned! { span => compile_error!("'ref' needs to be passed a variable name") }
}
}
None => {
quote_spanned! { span => compile_error!("'ref' needs to be passed a variable name") }
}
};
if mode == Mode::Ssr {
// fake the initialization; should only be used in effects or event handlers, which will never run on the server
// but if we don't initialize it, the compiler will complain
navigations.push(quote_spanned! {
span => #ident = String::new();
});
} else {
expressions.push(match &node.value {
Some(expr) => {
if let Some(ident) = expr_to_ident(expr) {
@ -329,7 +359,7 @@ fn attr_to_tokens(
// Classes
else if name.starts_with("class:") {
if mode == Mode::Ssr {
todo!()
// TODO class: in SSR
} else {
let name = name.replacen("class:", "", 1);
let value = node.value.as_ref().expect("class: attributes need values");
@ -468,11 +498,15 @@ fn child_to_tokens(
};
if let Some(v) = str_value {
navigations.push(location);
if mode != Mode::Ssr {
navigations.push(location);
}
template.push_str(&v);
PrevSibChange::Sib(name)
} else /* if next_sib.is_some() */ {
} else
/* if next_sib.is_some() */
{
// these markers are one of the primary templating differences across modes
match mode {
// in CSR, simply insert a comment node: it will be picked up and replaced with the value

View file

@ -2,14 +2,14 @@ use crate::{Runtime, Scope, ScopeId, Source, Subscriber};
use serde::{Deserialize, Serialize};
use std::{any::type_name, cell::RefCell, collections::HashSet, fmt::Debug, marker::PhantomData};
pub fn create_render_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static) -> Effect<T>
pub fn create_render_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
where
T: Debug + 'static,
{
cx.create_eff(true, f)
}
pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static) -> Effect<T>
pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
where
T: Debug + 'static,
{
@ -17,11 +17,8 @@ where
}
impl Scope {
pub(crate) fn create_eff<T>(
self,
render_effect: bool,
f: impl FnMut(Option<T>) -> T + 'static,
) -> Effect<T>
#[cfg(not(feature = "ssr"))]
pub(crate) fn create_eff<T>(self, render_effect: bool, f: impl FnMut(Option<T>) -> T + 'static)
where
T: Debug + 'static,
{
@ -29,16 +26,19 @@ impl Scope {
let id = self.push_effect(state);
let eff = Effect {
scope: self.id,
id,
ty: PhantomData,
};
self.runtime
.any_effect((self.id, id), |effect| effect.run((self.id, id)));
}
eff
// Simply don't run effects on the server at all
#[cfg(feature = "ssr")]
pub(crate) fn create_eff<T>(
self,
_render_effect: bool,
_f: impl FnMut(Option<T>) -> T + 'static,
) where
T: Debug + 'static,
{
}
}

View file

@ -2,13 +2,16 @@ use std::collections::HashMap;
#[derive(Debug, PartialEq, Default)]
pub struct SharedContext {
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub completed: Vec<web_sys::Element>,
pub events: Vec<()>,
pub id: Option<usize>,
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub registry: HashMap<String, web_sys::Element>,
}
impl SharedContext {
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub fn new_with_registry(registry: HashMap<String, web_sys::Element>) -> Self {
Self {
completed: Default::default(),
@ -28,8 +31,4 @@ impl SharedContext {
0
}
}
pub fn gather(&self) {
todo!()
}
}

View file

@ -5,7 +5,6 @@
mod context;
mod effect;
#[cfg(any(feature = "csr", feature = "hydrate"))]
mod hydration;
mod memo;
mod resource;

View file

@ -6,6 +6,7 @@ pub struct Memo<T>(ReadSignal<Option<T>>)
where
T: 'static;
#[cfg(not(feature = "ssr"))]
pub fn create_memo<T>(cx: Scope, mut f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Clone + Debug + 'static,
@ -23,6 +24,18 @@ where
Memo(read)
}
// On the server, Memo just carries its original value
// If we didn't provide this alternate version, it would panic because its inner effect wouldn't run
#[cfg(feature = "ssr")]
pub fn create_memo<T>(cx: Scope, mut f: impl FnMut(Option<T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Clone + Debug + 'static,
{
let (read, _) = create_signal(cx, Some(f(None)));
Memo(read)
}
impl<T> Clone for Memo<T>
where
T: 'static,

View file

@ -65,6 +65,7 @@ where
});
// initial load fires immediately
// TODO SSR — this won't run on server
create_effect(cx, {
let r = Rc::clone(&r);
move |_| r.load(false)
@ -179,6 +180,8 @@ where
let suspense_contexts = self.suspense_contexts.clone();
let has_value = v.is_some();
// TODO SSR check -- this won't run on server
create_effect(self.scope, move |_| {
if let Some(s) = &suspense_cx {
let mut contexts = suspense_contexts.borrow_mut();

View file

@ -3,16 +3,14 @@ use crate::{
ScopeState, SignalId, SignalState, Subscriber, TransitionState,
};
use slotmap::SlotMap;
use std::cell::{Cell, RefCell};
use std::cell::RefCell;
use std::fmt::Debug;
use std::rc::Rc;
#[cfg(any(feature = "csr", feature = "hydrate"))]
use crate::hydration::SharedContext;
#[derive(Default, Debug)]
pub(crate) struct Runtime {
#[cfg(any(feature = "csr", feature = "hydrate"))]
pub(crate) shared_context: RefCell<Option<SharedContext>>,
pub(crate) stack: RefCell<Vec<Subscriber>>,
pub(crate) scopes: RefCell<SlotMap<ScopeId, Rc<ScopeState>>>,
@ -133,6 +131,20 @@ impl Runtime {
ScopeDisposer(Box::new(move || scope.dispose()))
}
pub fn run_scope<T>(&'static self, f: impl FnOnce(Scope) -> T, parent: Option<Scope>) -> T {
let id = {
self.scopes
.borrow_mut()
.insert(Rc::new(ScopeState::new(parent)))
};
let scope = Scope { runtime: self, id };
let ret = f(scope);
scope.dispose();
ret
}
pub fn push_stack(&self, id: Subscriber) {
self.stack.borrow_mut().push(id);
}

View file

@ -1,6 +1,6 @@
use crate::{
AnyEffect, AnySignal, EffectId, EffectState, ReadSignal, ResourceId, ResourceState, Runtime,
SignalId, SignalState, WriteSignal,
hydration::SharedContext, AnyEffect, AnySignal, EffectId, EffectState, ReadSignal, ResourceId,
ResourceState, Runtime, SignalId, SignalState, WriteSignal,
};
use elsa::FrozenVec;
use std::{
@ -12,11 +12,24 @@ use std::{
};
#[must_use = "Scope will leak memory if the disposer function is never called"]
/// Creates a child reactive scope and runs the function within it. This is useful for applications
/// like a list or a router, which may want to create child scopes and dispose of them when
/// they are no longer needed (e.g., a list item has been destroyed or the user has navigated away
/// from the route.)
pub fn create_scope(f: impl FnOnce(Scope) + 'static) -> ScopeDisposer {
let runtime = Box::leak(Box::new(Runtime::new()));
runtime.create_scope(f, None)
}
/// Creates a temporary scope, runs the given function, disposes of the scope,
/// and returns the value returned from the function. This is very useful for short-lived
/// applications like SSR, where actual reactivity is not required beyond the end
/// of the synchronous operation.
pub fn run_scope<T>(f: impl FnOnce(Scope) -> T + 'static) -> T {
let runtime = Box::leak(Box::new(Runtime::new()));
runtime.run_scope(f, None)
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Scope {
pub(crate) runtime: &'static Runtime,
@ -152,13 +165,13 @@ impl Scope {
let v = curr.node_value();
if v == Some("#".to_string()) {
count += 1;
} else if v == Some("/".to_string()) {
count -= 1;
} else if v == Some("/".to_string()) {
count -= 1;
if count == 0 {
current.push(curr.clone());
return (curr, current);
}
log::debug!(">>> count is now {count}");
}
}
@ -168,10 +181,28 @@ impl Scope {
}
log::debug!("end = {end:?}");
log::debug!("current = {:?}", current.iter().map(|n| (n.node_name(), n.node_value())).collect::<Vec<_>>());
log::debug!(
"current = {:?}",
current
.iter()
.map(|n| (n.node_name(), n.node_value()))
.collect::<Vec<_>>()
);
(start, current)
}
pub fn next_hydration_key(&self) -> usize {
let mut sc = self.runtime.shared_context.borrow_mut();
if let Some(ref mut sc) = *sc {
sc.next_hydration_key()
} else {
let mut new_sc = SharedContext::default();
let id = new_sc.next_hydration_key();
*sc = Some(new_sc);
id
}
}
}
pub struct ScopeDisposer(pub(crate) Box<dyn FnOnce()>);