From f70702c6c43a40caf14a2d850e37625980017e59 Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Sat, 6 Jul 2024 08:16:48 -0400 Subject: [PATCH] feat: move to a channel-based implementation for meta --- integrations/actix/src/lib.rs | 7 +-- integrations/axum/src/lib.rs | 12 ++--- integrations/utils/src/lib.rs | 4 +- meta/src/body.rs | 6 ++- meta/src/html.rs | 6 ++- meta/src/lib.rs | 97 +++++++++++++++++++++-------------- 6 files changed, 77 insertions(+), 55 deletions(-) diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs index 37af837bf..15af87f38 100644 --- a/integrations/actix/src/lib.rs +++ b/integrations/actix/src/lib.rs @@ -741,7 +741,7 @@ where async move { let res_options = ResponseOptions::default(); - let meta_context = ServerMetaContext::new(); + let (meta_context, meta_output) = ServerMetaContext::new(); let additional_context = { let meta_context = meta_context.clone(); @@ -755,7 +755,7 @@ where let res = ActixResponse::from_app( app_fn, - meta_context, + meta_output, additional_context, res_options, stream_builder, @@ -945,12 +945,13 @@ where let _ = any_spawner::Executor::init_tokio(); let owner = Owner::new_root(None); + let (mock_meta, _) = ServerMetaContext::new(); let routes = owner .with(|| { // stub out a path for now provide_context(RequestUrl::new("")); provide_context(ResponseOptions::default()); - provide_context(ServerMetaContext::new()); + provide_context(mock_meta); additional_context(); RouteList::generate(&app_fn) }) diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs index c8f23bea5..64d3df73a 100644 --- a/integrations/axum/src/lib.rs +++ b/integrations/axum/src/lib.rs @@ -763,7 +763,7 @@ where Box::pin(async move { let add_context = additional_context.clone(); let res_options = ResponseOptions::default(); - let meta_context = ServerMetaContext::new(); + let (meta_context, meta_output) = ServerMetaContext::new(); let additional_context = { let meta_context = meta_context.clone(); @@ -787,7 +787,7 @@ where let res = AxumResponse::from_app( app_fn, - meta_context, + meta_output, additional_context, res_options, stream_builder, @@ -1155,12 +1155,8 @@ where provide_context(RequestUrl::new("")); let (mock_parts, _) = http::Request::new(Body::from("")).into_parts(); - provide_contexts( - "", - &Default::default(), - mock_parts, - Default::default(), - ); + let (mock_meta, _) = ServerMetaContext::new(); + provide_contexts("", &mock_meta, mock_parts, Default::default()); additional_context(); RouteList::generate(&app_fn) }) diff --git a/integrations/utils/src/lib.rs b/integrations/utils/src/lib.rs index 6791c5479..64e91e4bd 100644 --- a/integrations/utils/src/lib.rs +++ b/integrations/utils/src/lib.rs @@ -5,7 +5,7 @@ use leptos::{ reactive_graph::owner::{Owner, Sandboxed}, IntoView, }; -use leptos_meta::ServerMetaContext; +use leptos_meta::ServerMetaContextOutput; use std::{future::Future, pin::Pin, sync::Arc}; pub type PinnedStream = Pin + Send>>; @@ -24,7 +24,7 @@ pub trait ExtendResponse: Sized { fn from_app( app_fn: impl FnOnce() -> IV + Send + 'static, - meta_context: ServerMetaContext, + meta_context: ServerMetaContextOutput, additional_context: impl FnOnce() + Send + 'static, res_options: Self::ResponseOptions, stream_builder: fn( diff --git a/meta/src/body.rs b/meta/src/body.rs index aab90c23a..b3cf4f73a 100644 --- a/meta/src/body.rs +++ b/meta/src/body.rs @@ -67,11 +67,13 @@ pub fn Body( attributes.push(value.into_any_attr()); } if let Some(meta) = use_context::() { - let mut meta = meta.inner.write().or_poisoned(); // if we are server rendering, we will not actually use these values via RenderHtml // instead, they'll be handled separately by the server integration // so it's safe to take them out of the props here - meta.body = mem::take(&mut attributes); + for attr in attributes.drain(0..) { + // fails only if receiver is already dropped + _ = meta.body.send(attr); + } } BodyView { attributes } diff --git a/meta/src/html.rs b/meta/src/html.rs index e4605059d..855408a5c 100644 --- a/meta/src/html.rs +++ b/meta/src/html.rs @@ -78,11 +78,13 @@ pub fn Html( })), ); if let Some(meta) = use_context::() { - let mut meta = meta.inner.write().or_poisoned(); // if we are server rendering, we will not actually use these values via RenderHtml // instead, they'll be handled separately by the server integration // so it's safe to take them out of the props here - meta.html = mem::take(&mut attributes); + for attr in attributes.drain(0..) { + // fails only if receiver is already dropped + _ = meta.body.send(attr); + } } HtmlView { attributes } diff --git a/meta/src/lib.rs b/meta/src/lib.rs index 36e3ad124..351d8cb0e 100644 --- a/meta/src/lib.rs +++ b/meta/src/lib.rs @@ -73,7 +73,10 @@ use or_poisoned::OrPoisoned; use send_wrapper::SendWrapper; use std::{ fmt::Debug, - sync::{Arc, RwLock}, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, RwLock, + }, }; use wasm_bindgen::JsCast; use web_sys::HtmlHeadElement; @@ -154,17 +157,61 @@ impl Default for MetaContext { } } -/// Contains the state of meta tags for server rendering. +/// Allows you to add `` content from components located in the `` of the application, +/// which can be accessed during server rendering via [`ServerMetaContextOutput`]. /// /// This should be provided as context during server rendering. -#[derive(Clone, Default)] +/// +/// No content added after the first chunk of the stream has been sent will be included in the +/// initial ``. Data that needs to be included in the `` during SSR should be +/// synchronous or loaded as a blocking resource. +#[derive(Clone, Debug)] pub struct ServerMetaContext { - inner: Arc>, /// Metadata associated with the `` element. pub(crate) title: TitleContext, + /// Attributes for the `<html>` element. + pub(crate) html: Sender<AnyAttribute<Dom>>, + /// Attributes for the `<body>` element. + pub(crate) body: Sender<AnyAttribute<Dom>>, + /// Arbitrary elements to be added to the `<head>` as HTML. + pub(crate) elements: Sender<String>, +} + +/// Allows you to access `<head>` content that was inserted via [`ServerMetaContext`]. +#[must_use = "If you do not use the output, adding meta tags will have no \ + effect."] +#[derive(Debug)] +pub struct ServerMetaContextOutput { + pub(crate) title: TitleContext, + html: Receiver<AnyAttribute<Dom>>, + body: Receiver<AnyAttribute<Dom>>, + elements: Receiver<String>, } impl ServerMetaContext { + /// Creates an empty [`ServerMetaContext`]. + pub fn new() -> (ServerMetaContext, ServerMetaContextOutput) { + let title = TitleContext::default(); + let (html_tx, html_rx) = channel(); + let (body_tx, body_rx) = channel(); + let (elements_tx, elements_rx) = channel(); + let tx = ServerMetaContext { + title: title.clone(), + html: html_tx, + body: body_tx, + elements: elements_tx, + }; + let rx = ServerMetaContextOutput { + title, + html: html_rx, + body: body_rx, + elements: elements_rx, + }; + (tx, rx) + } +} + +impl ServerMetaContextOutput { /// Consumes the metadata, injecting it into the the first chunk of an HTML stream in the /// appropriate place. /// @@ -174,17 +221,19 @@ impl ServerMetaContext { self, mut stream: impl Stream<Item = String> + Send + Unpin, ) -> impl Stream<Item = String> + Send { + // wait for the first chunk of the stream, to ensure our components hve run let mut first_chunk = stream.next().await.unwrap_or_default(); - let meta_buf = - std::mem::take(&mut self.inner.write().or_poisoned().head_html); - + // create <title> tag let title = self.title.as_string(); let title_len = title .as_ref() .map(|n| "<title>".len() + n.len() + "".len()) .unwrap_or(0); + // collect all registered meta tags + let meta_buf = self.elements.into_iter().collect::(); + let modified_chunk = if title_len == 0 && meta_buf.is_empty() { first_chunk } else { @@ -218,35 +267,6 @@ impl ServerMetaContext { } } -#[derive(Default, Debug)] -struct ServerMetaContextInner { - /*/// Metadata associated with the `` element - pub html: HtmlContext, - /// Metadata associated with the `` element. - pub title: TitleContext,*/ - /// Metadata associated with the `<html>` element - pub(crate) html: Vec<AnyAttribute<Dom>>, - /// Metadata associated with the `<body>` element - pub(crate) body: Vec<AnyAttribute<Dom>>, - /// HTML for arbitrary tags that will be included in the `<head>` element - pub(crate) head_html: String, -} - -impl Debug for ServerMetaContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ServerMetaContext") - .field("inner", &self.inner) - .finish_non_exhaustive() - } -} - -impl ServerMetaContext { - /// Creates an empty [`ServerMetaContext`]. - pub fn new() -> Self { - Default::default() - } -} - /// Provides a [`MetaContext`], if there is not already one provided. This ensures that you can provide it /// at the highest possible level, without overwriting a [`MetaContext`] that has already been provided /// (for example, by a server-rendering integration.) @@ -293,12 +313,13 @@ where #[cfg(feature = "ssr")] if let Some(cx) = use_context::<ServerMetaContext>() { - let mut inner = cx.inner.write().or_poisoned(); + let mut buf = String::new(); el.take().unwrap().to_html_with_buf( - &mut inner.head_html, + &mut buf, &mut Position::NextChild, false, ); + _ = cx.elements.send(buf); // fails only if the receiver is already dropped } else { tracing::warn!( "tried to use a leptos_meta component without `ServerMetaContext` \