#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
use crate::Owner;
use crate::{
runtime::PinnedFuture, suspense::StreamChunk, with_runtime, ResourceId,
SignalGet, SuspenseContext,
};
use futures::stream::FuturesUnordered;
#[cfg(feature = "experimental-islands")]
use std::cell::Cell;
use std::collections::{HashMap, HashSet, VecDeque};
#[doc(hidden)]
pub struct SharedContext {
pub server_resources: HashSet<ResourceId>,
pub pending_resources: HashSet<ResourceId>,
pub resolved_resources: HashMap<ResourceId, String>,
pub pending_fragments: HashMap<String, FragmentData>,
pub fragments_with_local_resources: HashSet<String>,
#[cfg(feature = "experimental-islands")]
pub no_hydrate: bool,
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
pub islands: HashMap<Owner, web_sys::HtmlElement>,
}
impl SharedContext {
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn all_resources() -> Vec<ResourceId> {
with_runtime(|runtime| runtime.all_resources()).unwrap_or_default()
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn pending_resources() -> Vec<ResourceId> {
with_runtime(|runtime| runtime.pending_resources()).unwrap_or_default()
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn serialization_resolvers(
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
with_runtime(|runtime| runtime.serialization_resolvers())
.unwrap_or_default()
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn register_suspense(
context: SuspenseContext,
key: &str,
out_of_order_resolver: impl FnOnce() -> String + 'static,
in_order_resolver: impl FnOnce() -> VecDeque<StreamChunk> + 'static,
) {
use crate::create_isomorphic_effect;
use futures::StreamExt;
_ = with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let (tx1, mut rx1) = futures::channel::mpsc::unbounded();
let (tx2, mut rx2) = futures::channel::mpsc::unbounded();
let (tx3, mut rx3) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(move |_| {
let pending = context
.pending_serializable_resources
.read_only()
.try_get()
.unwrap_or_default();
if pending.is_empty() {
_ = tx1.unbounded_send(());
_ = tx2.unbounded_send(());
_ = tx3.unbounded_send(());
}
});
shared_context.pending_fragments.insert(
key.to_string(),
FragmentData {
out_of_order: Box::pin(async move {
rx1.next().await;
out_of_order_resolver()
}),
in_order: Box::pin(async move {
rx2.next().await;
in_order_resolver()
}),
should_block: context.should_block(),
is_ready: Some(Box::pin(async move {
rx3.next().await;
})),
local_only: context.has_local_only(),
},
);
})
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn take_pending_fragment(id: &str) -> Option<FragmentData> {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.pending_fragments.remove(id)
})
.ok()
.flatten()
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn blocking_fragments_ready() -> PinnedFuture<()> {
use futures::StreamExt;
let mut ready = with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
let ready = FuturesUnordered::new();
for (_, data) in shared_context.pending_fragments.iter_mut() {
if data.should_block {
if let Some(is_ready) = data.is_ready.take() {
ready.push(is_ready);
}
}
}
ready
})
.unwrap_or_default();
Box::pin(async move { while ready.next().await.is_some() {} })
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn pending_fragments() -> HashMap<String, FragmentData> {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.pending_fragments)
})
.unwrap_or_default()
}
#[cfg(all(feature = "hydrate", feature = "experimental-islands"))]
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn register_island(el: &web_sys::HtmlElement) {
if let Some(owner) = Owner::current() {
let el = el.clone();
_ = with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.islands.insert(owner, el);
});
}
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn fragment_has_local_resources(fragment: &str) -> bool {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context
.fragments_with_local_resources
.remove(fragment)
})
.unwrap_or_default()
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn fragments_with_local_resources() -> HashSet<String> {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
std::mem::take(&mut shared_context.fragments_with_local_resources)
})
.unwrap_or_default()
}
#[cfg_attr(
any(debug_assertions, feature = "ssr"),
instrument(level = "trace", skip_all,)
)]
pub fn register_local_fragment(key: String) {
with_runtime(|runtime| {
let mut shared_context = runtime.shared_context.borrow_mut();
shared_context.fragments_with_local_resources.insert(key);
})
.unwrap_or_default()
}
}
pub struct FragmentData {
pub out_of_order: PinnedFuture<String>,
pub in_order: PinnedFuture<VecDeque<StreamChunk>>,
pub should_block: bool,
pub is_ready: Option<PinnedFuture<()>>,
pub local_only: bool,
}
impl core::fmt::Debug for SharedContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SharedContext").finish()
}
}
impl PartialEq for SharedContext {
fn eq(&self, other: &Self) -> bool {
self.pending_resources == other.pending_resources
&& self.resolved_resources == other.resolved_resources
}
}
impl Eq for SharedContext {}
#[allow(clippy::derivable_impls)]
impl Default for SharedContext {
fn default() -> Self {
#[cfg(all(feature = "hydrate", target_arch = "wasm32"))]
{
let pending_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_PENDING_RESOURCES"),
);
let pending_resources: HashSet<ResourceId> = pending_resources
.map_err(|_| ())
.and_then(|pr| {
serde_wasm_bindgen::from_value(pr).map_err(|_| ())
})
.unwrap();
let fragments_with_local_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_LOCAL_ONLY"),
);
let fragments_with_local_resources: HashSet<String> =
fragments_with_local_resources
.map_err(|_| ())
.and_then(|pr| {
serde_wasm_bindgen::from_value(pr).map_err(|_| ())
})
.unwrap_or_default();
let resolved_resources = js_sys::Reflect::get(
&web_sys::window().unwrap(),
&wasm_bindgen::JsValue::from_str("__LEPTOS_RESOLVED_RESOURCES"),
)
.unwrap();
let resolved_resources =
serde_wasm_bindgen::from_value(resolved_resources).unwrap();
Self {
server_resources: pending_resources.clone(),
pending_resources,
resolved_resources,
fragments_with_local_resources,
pending_fragments: Default::default(),
#[cfg(feature = "experimental-islands")]
no_hydrate: true,
#[cfg(all(
feature = "hydrate",
feature = "experimental-islands"
))]
islands: Default::default(),
}
}
#[cfg(not(all(feature = "hydrate", target_arch = "wasm32")))]
{
Self {
server_resources: Default::default(),
pending_resources: Default::default(),
resolved_resources: Default::default(),
pending_fragments: Default::default(),
fragments_with_local_resources: Default::default(),
#[cfg(feature = "experimental-islands")]
no_hydrate: true,
#[cfg(all(
feature = "hydrate",
feature = "experimental-islands"
))]
islands: Default::default(),
}
}
}
}
#[cfg(feature = "experimental-islands")]
thread_local! {
pub static NO_HYDRATE: Cell<bool> = const { Cell::new(true) };
}
#[cfg(feature = "experimental-islands")]
impl SharedContext {
pub fn no_hydrate() -> bool {
NO_HYDRATE.with(Cell::get)
}
pub fn set_no_hydrate(hydrate: bool) {
NO_HYDRATE.with(|cell| cell.set(hydrate));
}
#[inline(always)]
pub fn with_hydration<T>(f: impl FnOnce() -> T) -> T {
let prev = SharedContext::no_hydrate();
SharedContext::set_no_hydrate(false);
let v = f();
SharedContext::set_no_hydrate(prev);
v
}
#[inline(always)]
pub fn no_hydration<T>(f: impl FnOnce() -> T) -> T {
let prev = SharedContext::no_hydrate();
SharedContext::set_no_hydrate(true);
let v = f();
SharedContext::set_no_hydrate(prev);
v
}
}