#![allow(clippy::type_complexity)] use crate::{ CborEncoding, ClientRequest, ClientResponse, Encoding, FromResponse, IntoRequest, JsonEncoding, ServerFnError, }; use axum::extract::{FromRequest, Request}; use axum_core::response::IntoResponse; use bytes::Bytes; use dioxus_fullstack_core::{HttpError, RequestError}; use futures::{Stream, StreamExt}; use headers::{ContentType, Header}; use send_wrapper::SendWrapper; use serde::{de::DeserializeOwned, Serialize}; use std::{future::Future, marker::PhantomData, pin::Pin}; pub type TextStream = Streaming; pub type ByteStream = Streaming; pub type JsonStream = Streaming; pub type CborStream = Streaming; /// A streaming payload. /// /// ## Browser Support for Streaming Input /// /// Browser fetch requests do not currently support full request duplexing, which /// means that that they do not begin handling responses until the full request has been sent. /// /// This means that if you use a streaming input encoding, the input stream needs to /// end before the output will begin. /// /// Streaming requests are only allowed over HTTP2 or HTTP3. /// /// Also note that not all browsers support streaming bodies to servers. pub struct Streaming { output_stream: Pin> + Send>>, input_stream: Pin> + Send>>, encoding: PhantomData, } #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq, Hash)] pub enum StreamingError { /// The streaming request was interrupted and could not be completed. #[error("The streaming request was interrupted")] Interrupted, /// The stream failed to decode a chunk - possibly due to invalid data or version mismatch. #[error("The stream failed to decode a chunk")] Decoding, /// The stream failed to connect or encountered an error. #[error("The streaming request failed")] Failed, } impl Streaming { /// Creates a new stream from the given stream. pub fn new(value: impl Stream + Send + 'static) -> Self { // Box and pin the incoming stream and store as a trait object Self { output_stream: Box::pin(futures::stream::empty()) as _, input_stream: Box::pin(value.map(|item| Ok(item))) as Pin> + Send>>, encoding: PhantomData, } } /// Spawns a new task that produces items for the stream. /// /// The callback is provided an `UnboundedSender` that can be used to send items to the stream. #[cfg(feature = "server")] pub fn spawn( callback: impl FnOnce(futures_channel::mpsc::UnboundedSender) -> F + Send + 'static, ) -> Self where F: Future + 'static, T: Send, { let (tx, rx) = futures_channel::mpsc::unbounded(); crate::spawn_platform(move || callback(tx)); Self::new(rx) } /// Returns the next item in the stream, or `None` if the stream has ended. pub async fn next(&mut self) -> Option> { self.output_stream.as_mut().next().await } /// Consumes the wrapper, returning the inner stream. pub fn into_inner(self) -> impl Stream> + Send { self.input_stream } } impl From for TextStream where S: Stream + Send + 'static, U: Into, { fn from(value: S) -> Self { Self { input_stream: Box::pin(value.map(|data| Ok(data.into()))), output_stream: Box::pin(futures::stream::empty()) as _, encoding: PhantomData, } } } impl From for ByteStream where S: Stream> + Send + 'static, { fn from(value: S) -> Self { Self { input_stream: Box::pin(value.map(|data| data.map_err(|_| StreamingError::Failed))), output_stream: Box::pin(futures::stream::empty()) as _, encoding: PhantomData, } } } impl From for Streaming where S: Stream + Send + 'static, U: Into, T: 'static + Send, E: Encoding, { fn from(value: S) -> Self { Self { input_stream: Box::pin(value.map(|data| Ok(data.into()))), output_stream: Box::pin(futures::stream::empty()) as _, encoding: PhantomData, } } } impl IntoResponse for Streaming { fn into_response(self) -> axum_core::response::Response { axum::response::Response::builder() .header("Content-Type", "text/plain; charset=utf-8") .body(axum::body::Body::from_stream(self.input_stream)) .unwrap() } } impl IntoResponse for Streaming { fn into_response(self) -> axum_core::response::Response { axum::response::Response::builder() .header("Content-Type", "application/octet-stream") .body(axum::body::Body::from_stream(self.input_stream)) .unwrap() } } impl IntoResponse for Streaming { fn into_response(self) -> axum_core::response::Response { let res = self.input_stream.map(|r| match r { Ok(res) => match E::to_bytes(&res) { Some(bytes) => Ok(bytes), None => Err(StreamingError::Failed), }, Err(_err) => Err(StreamingError::Failed), }); axum::response::Response::builder() .header("Content-Type", E::stream_content_type()) .body(axum::body::Body::from_stream(res)) .unwrap() } } impl FromResponse for Streaming { fn from_response(res: ClientResponse) -> impl Future> { SendWrapper::new(async move { let client_stream = Box::pin(res.bytes_stream().map(|byte| match byte { Ok(bytes) => match String::from_utf8(bytes.to_vec()) { Ok(string) => Ok(string), Err(_) => Err(StreamingError::Decoding), }, Err(_) => Err(StreamingError::Failed), })); Ok(Self { output_stream: client_stream, input_stream: Box::pin(futures::stream::empty()), encoding: PhantomData, }) }) } } impl FromResponse for Streaming { fn from_response(res: ClientResponse) -> impl Future> { async move { let client_stream = Box::pin(SendWrapper::new(res.bytes_stream().map( |byte| match byte { Ok(bytes) => Ok(bytes), Err(_) => Err(StreamingError::Failed), }, ))); Ok(Self { output_stream: client_stream, input_stream: Box::pin(futures::stream::empty()), encoding: PhantomData, }) } } } impl FromResponse for Streaming { fn from_response(res: ClientResponse) -> impl Future> { SendWrapper::new(async move { let client_stream = Box::pin(SendWrapper::new(res.bytes_stream().map( |byte| match byte { Ok(bytes) => match E::from_bytes(bytes) { Some(res) => Ok(res), None => Err(StreamingError::Decoding), }, Err(_) => Err(StreamingError::Failed), }, ))); Ok(Self { output_stream: client_stream, input_stream: Box::pin(futures::stream::empty()), encoding: PhantomData, }) }) } } impl FromRequest for Streaming { type Rejection = ServerFnError; fn from_request( req: Request, _state: &S, ) -> impl Future> + Send { async move { let (parts, body) = req.into_parts(); let content_type = parts .headers .get("content-type") .and_then(|v| v.to_str().ok()) .unwrap_or(""); if !content_type.starts_with("text/plain") { HttpError::bad_request("Invalid content type")?; } let stream = body.into_data_stream(); Ok(Self { input_stream: Box::pin(futures::stream::empty()), output_stream: Box::pin(stream.map(|byte| match byte { Ok(bytes) => match String::from_utf8(bytes.to_vec()) { Ok(string) => Ok(string), Err(_) => Err(StreamingError::Decoding), }, Err(_) => Err(StreamingError::Failed), })), encoding: PhantomData, }) } } } impl FromRequest for ByteStream { type Rejection = ServerFnError; fn from_request( req: Request, _state: &S, ) -> impl Future> + Send { async move { let (parts, body) = req.into_parts(); let content_type = parts .headers .get("content-type") .and_then(|v| v.to_str().ok()) .unwrap_or(""); if !content_type.starts_with("application/octet-stream") { HttpError::bad_request("Invalid content type")?; } let stream = body.into_data_stream(); Ok(Self { input_stream: Box::pin(futures::stream::empty()), output_stream: Box::pin(stream.map(|byte| match byte { Ok(bytes) => Ok(bytes), Err(_) => Err(StreamingError::Failed), })), encoding: PhantomData, }) } } } impl FromRequest for Streaming { type Rejection = ServerFnError; fn from_request( req: Request, _state: &S, ) -> impl Future> + Send { async move { let (parts, body) = req.into_parts(); let content_type = parts .headers .get("content-type") .and_then(|v| v.to_str().ok()) .unwrap_or(""); if !content_type.starts_with(E::stream_content_type()) { HttpError::bad_request("Invalid content type")?; } let stream = body.into_data_stream(); Ok(Self { input_stream: Box::pin(futures::stream::empty()), output_stream: Box::pin(stream.map(|byte| match byte { Ok(bytes) => match E::from_bytes(bytes) { Some(res) => Ok(res), None => Err(StreamingError::Decoding), }, Err(_) => Err(StreamingError::Failed), })), encoding: PhantomData, }) } } } impl IntoRequest for Streaming { fn into_request( self, builder: ClientRequest, ) -> impl Future> + 'static { async move { builder .header("Content-Type", "text/plain; charset=utf-8")? .send_body_stream(self.input_stream.map(|e| e.map(Bytes::from))) .await } } } impl IntoRequest for ByteStream { fn into_request( self, builder: ClientRequest, ) -> impl Future> + 'static { async move { builder .header(ContentType::name(), "application/octet-stream")? .send_body_stream(self.input_stream) .await } } } impl IntoRequest for Streaming { fn into_request( self, builder: ClientRequest, ) -> impl Future> + 'static { async move { builder .header("Content-Type", E::stream_content_type())? .send_body_stream( self.input_stream.map(|r| { r.and_then(|item| E::to_bytes(&item).ok_or(StreamingError::Failed)) }), ) .await } } } impl std::fmt::Debug for Streaming { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("Streaming").finish() } } impl std::fmt::Debug for Streaming { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Streaming") .field("encoding", &std::any::type_name::()) .finish() } }