dioxus_fullstack_core/streaming.rs
1use crate::ServerFnError;
2use axum_core::{extract::FromRequest, response::IntoResponse};
3use dioxus_core::try_consume_context;
4use dioxus_signals::{ReadableExt, Signal, WritableExt};
5use http::request::Parts;
6use std::{cell::RefCell, rc::Rc};
7
8/// The status of the streaming response
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum StreamingStatus {
11 /// The initial chunk is still being rendered. The http response parts can still be modified at this point.
12 RenderingInitialChunk,
13
14 /// The initial chunk has been committed and the response is now streaming. The http response parts
15 /// have already been sent to the client and can no longer be modified.
16 InitialChunkCommitted,
17}
18
19/// The context dioxus fullstack provides for the status of streaming responses on the server
20#[derive(Clone, Debug)]
21pub struct StreamingContext {
22 current_status: Signal<StreamingStatus>,
23 request_headers: Rc<RefCell<http::request::Parts>>,
24}
25
26impl PartialEq for StreamingContext {
27 fn eq(&self, other: &Self) -> bool {
28 self.current_status == other.current_status
29 && Rc::ptr_eq(&self.request_headers, &other.request_headers)
30 }
31}
32
33impl StreamingContext {
34 /// Create a new streaming context. You should not need to call this directly. Dioxus fullstack will
35 /// provide this context for you.
36 pub fn new(parts: Parts) -> Self {
37 Self {
38 current_status: Signal::new(StreamingStatus::RenderingInitialChunk),
39 request_headers: Rc::new(RefCell::new(parts)),
40 }
41 }
42
43 /// Commit the initial chunk of the response. This will be called automatically if you are using the
44 /// dioxus router when the suspense boundary above the router is resolved. Otherwise, you will need
45 /// to call this manually to start the streaming part of the response.
46 ///
47 /// Once this method has been called, the http response parts can no longer be modified.
48 pub fn commit_initial_chunk(&mut self) {
49 self.current_status
50 .set(StreamingStatus::InitialChunkCommitted);
51 }
52
53 /// Get the current status of the streaming response. This method is reactive and will cause
54 /// the current reactive context to rerun when the status changes.
55 pub fn current_status(&self) -> StreamingStatus {
56 *self.current_status.read()
57 }
58
59 /// Access the http request parts mutably. This will allow you to modify headers and other parts of the request.
60 pub fn parts_mut(&self) -> std::cell::RefMut<'_, http::request::Parts> {
61 self.request_headers.borrow_mut()
62 }
63
64 /// Extract an axum extractor from the current request. This will always use an empty body for the request,
65 /// since it's assumed that rendering the app is done under a `GET` request.
66 pub async fn extract<T: FromRequest<(), M>, M>() -> Result<T, ServerFnError> {
67 let this = Self::current()
68 .ok_or_else(|| ServerFnError::new("No StreamingContext found".to_string()))?;
69
70 let parts = this.request_headers.borrow_mut().clone();
71 let request =
72 axum_core::extract::Request::from_parts(parts, axum_core::body::Body::empty());
73 match T::from_request(request, &()).await {
74 Ok(res) => Ok(res),
75 Err(err) => {
76 let resp = err.into_response();
77 Err(ServerFnError::from_axum_response(resp).await)
78 }
79 }
80 }
81
82 /// Get the current `StreamingContext` if it exists. This will return `None` if called on the client
83 /// or outside of a streaming response on the server.
84 pub fn current() -> Option<Self> {
85 if let Some(rt) = dioxus_core::Runtime::try_current() {
86 let id = rt.try_current_scope_id()?;
87 if let Some(ctx) = rt.consume_context::<StreamingContext>(id) {
88 return Some(ctx);
89 }
90 }
91
92 None
93 }
94}
95
96/// Commit the initial chunk of the response. This will be called automatically if you are using the
97/// dioxus router when the suspense boundary above the router is resolved. Otherwise, you will need
98/// to call this manually to start the streaming part of the response.
99///
100/// On the client, this will do nothing.
101///
102/// # Example
103/// ```rust, no_run
104/// # use dioxus::prelude::*;
105/// # use dioxus_fullstack_core::*;
106/// # fn Children() -> Element { unimplemented!() }
107/// fn App() -> Element {
108/// // This will start streaming immediately after the current render is complete.
109/// use_hook(commit_initial_chunk);
110///
111/// rsx! { Children {} }
112/// }
113/// ```
114pub fn commit_initial_chunk() {
115 crate::history::finalize_route();
116 if let Some(mut streaming) = try_consume_context::<StreamingContext>() {
117 streaming.commit_initial_chunk();
118 }
119}
120
121/// Get the current status of the streaming response. This method is reactive and will cause
122/// the current reactive context to rerun when the status changes.
123///
124/// On the client, this will always return `StreamingStatus::InitialChunkCommitted`.
125///
126/// # Example
127/// ```rust, no_run
128/// # use dioxus::prelude::*;
129/// # use dioxus_fullstack_core::*;
130/// #[component]
131/// fn MetaTitle(title: String) -> Element {
132/// // If streaming has already started, warn the user that the meta tag will not show
133/// // up in the initial chunk.
134/// use_hook(|| {
135/// if current_status() == StreamingStatus::InitialChunkCommitted {
136/// dioxus::logger::tracing::warn!("Since `MetaTitle` was rendered after the initial chunk was committed, the meta tag will not show up in the head without javascript enabled.");
137/// }
138/// });
139///
140/// rsx! { meta { property: "og:title", content: title } }
141/// }
142/// ```
143pub fn current_status() -> StreamingStatus {
144 if let Some(streaming) = try_consume_context::<StreamingContext>() {
145 streaming.current_status()
146 } else {
147 StreamingStatus::InitialChunkCommitted
148 }
149}