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}