feat: synchronous serialized values with SharedValue

This commit is contained in:
Greg Johnston 2024-06-18 20:47:08 -04:00
parent 9e4c0b86f2
commit 602ac60a85
7 changed files with 139 additions and 1 deletions

View File

@ -68,4 +68,12 @@ impl SharedContext for CsrSharedContext {
#[inline(always)]
fn seal_errors(&self, _boundary_id: &SerializedDataId) {}
#[inline(always)]
fn during_hydration(&self) -> bool {
false
}
#[inline(always)]
fn hydration_complete(&self) {}
}

View File

@ -55,6 +55,7 @@ impl std::error::Error for SerializedError {}
pub struct HydrateSharedContext {
id: AtomicUsize,
is_hydrating: AtomicBool,
during_hydration: AtomicBool,
errors: Lazy<Vec<(SerializedDataId, ErrorId, Error)>>,
}
@ -64,6 +65,7 @@ impl HydrateSharedContext {
Self {
id: AtomicUsize::new(0),
is_hydrating: AtomicBool::new(true),
during_hydration: AtomicBool::new(true),
errors: Lazy::new(serialized_errors),
}
}
@ -76,6 +78,7 @@ impl HydrateSharedContext {
Self {
id: AtomicUsize::new(0),
is_hydrating: AtomicBool::new(false),
during_hydration: AtomicBool::new(true),
errors: Lazy::new(serialized_errors),
}
}
@ -111,6 +114,14 @@ impl SharedContext for HydrateSharedContext {
None
}
fn during_hydration(&self) -> bool {
self.during_hydration.load(Ordering::Relaxed)
}
fn hydration_complete(&self) {
self.during_hydration.store(false, Ordering::Relaxed)
}
fn get_is_hydrating(&self) -> bool {
self.is_hydrating.load(Ordering::Relaxed)
}

View File

@ -85,6 +85,15 @@ pub trait SharedContext: Debug {
/// In browser implementations, this return `None`.
fn pending_data(&self) -> Option<PinnedStream<String>>;
/// Whether the page is currently being hydrated.
///
/// Should always be `false` on the server or when client-rendering, including after the
/// initial hydration in the client.
fn during_hydration(&self) -> bool;
/// Tells the shared context that the hydration process is complete.
fn hydration_complete(&self);
/// Returns `true` if you are currently in a part of the application tree that should be
/// hydrated.
///

View File

@ -180,6 +180,12 @@ impl SharedContext for SsrSharedContext {
stream::once(async move { initial_chunk }).chain(async_data);
Some(Box::pin(stream))
}
fn during_hydration(&self) -> bool {
false
}
fn hydration_complete(&self) {}
}
struct AsyncDataStream {

View File

@ -66,6 +66,10 @@ where
)
});
if let Some(sc) = Owner::current_shared_context() {
sc.hydration_complete();
}
// returns a handle that owns the owner
// when this is dropped, it will clean up the reactive system and unmount the view
UnmountHandle {

View File

@ -8,6 +8,8 @@ pub use multi_action::*;
mod resource;
pub mod serializers;
pub use resource::*;
mod shared;
pub use shared::*;
////! # Leptos Server Functions
////!
@ -223,7 +225,10 @@ mod view_implementations {
fn to_html_async_with_buf<const OUT_OF_ORDER: bool>(
self,
buf: &mut StreamBuilder, position: &mut Position, escape: bool) where
buf: &mut StreamBuilder,
position: &mut Position,
escape: bool,
) where
Self: Sized,
{
(move || Suspend(async move { self.await }))

View File

@ -0,0 +1,95 @@
use crate::serializers::{SerdeJson, SerializableData, Serializer};
use std::{
fmt::Debug,
marker::PhantomData,
ops::{Deref, DerefMut},
};
/// A smart pointer that allows you to share identical, synchronously-loaded data between the
/// server and the client.
///
/// If this constructed on the server, it serializes its value into the shared context. If it is
/// constructed on the client during hydration, it reads its value from the shared context. If
/// it it constructed on the client at any other time, it simply runs on the client.
pub struct SharedValue<T, Ser = SerdeJson> {
value: T,
ser: PhantomData<Ser>,
}
impl<T, Ser> SharedValue<T, Ser> {
pub fn into_inner(self) -> T {
self.value
}
}
impl<T, Ser> SharedValue<T, Ser>
where
T: Debug + SerializableData<Ser>,
T::SerErr: Debug,
Ser: Serializer,
{
pub fn new(initial: impl FnOnce() -> T) -> Self {
let value: T;
#[cfg(feature = "hydration")]
{
use reactive_graph::owner::Owner;
let sc = Owner::current_shared_context();
let id = sc.as_ref().map(|sc| sc.next_id()).unwrap_or_default();
let serialized = sc.as_ref().and_then(|sc| sc.read_data(&id));
let hydrating =
sc.as_ref().map(|sc| sc.during_hydration()).unwrap_or(false);
value = if hydrating {
serialized
.as_ref()
.and_then(|data| T::de(data).ok())
.unwrap_or_else(|| {
#[cfg(feature = "tracing")]
tracing::error!(
"couldn't deserialize from {serialized:?}"
);
initial()
})
} else {
let init = initial();
#[cfg(feature = "ssr")]
if let Some(sc) = sc {
match init.ser() {
Ok(value) => {
sc.write_async(id, Box::pin(async move { value }))
}
Err(e) => {
#[cfg(feature = "tracing")]
tracing::error!(
"couldn't serialize {init:?}: {e:?}"
);
}
}
}
init
}
}
#[cfg(not(feature = "hydration"))]
{
value = initial();
}
Self {
value,
ser: PhantomData,
}
}
}
impl<T, Ser> Deref for SharedValue<T, Ser> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
impl<T, Ser> DerefMut for SharedValue<T, Ser> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.value
}
}