#![cfg(feature = "alloc")]
use core::any::Any;
use core::pin::Pin;
use alloc::boxed::Box;
use alloc::vec::Vec;
use bytes::Bytes;
use futures::stream::Stream;
use futures::stream::StreamExt;
#[cfg(feature = "std")]
use alloc::sync::Arc;
#[cfg(feature = "std")]
use proxima_core::signal::Signal;
#[cfg(feature = "std")]
use crate::sync::blocking::Mutex;
#[cfg(not(feature = "std"))]
use alloc::sync::Arc;
#[cfg(feature = "std")]
use crate::pipe::header_list::HeaderList;
use proxima_core::ProximaError;
pub type ChunkStream = Pin<Box<dyn Stream<Item = Result<Bytes, ProximaError>> + Send>>;
#[cfg(feature = "std")]
pub type TrailersSlot = Arc<Mutex<Option<HeaderList>>>;
#[derive(Clone)]
pub struct Carry(Arc<dyn Any + Send + Sync>);
impl Carry {
#[must_use]
pub fn new<T>(value: T) -> Self
where
T: Any + Send + Sync + 'static,
{
Self(Arc::new(value))
}
#[must_use]
pub fn from_arc<T>(value: Arc<T>) -> Self
where
T: Any + Send + Sync + 'static,
{
Self(value)
}
#[must_use]
pub fn downcast_ref<T>(&self) -> Option<&T>
where
T: Any + Send + Sync + 'static,
{
self.0.downcast_ref::<T>()
}
}
impl core::fmt::Debug for Carry {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.debug_struct("Carry").finish_non_exhaustive()
}
}
pub struct ResponseStream {
stream: ChunkStream,
#[cfg(feature = "std")]
trailers: Option<TrailersSlot>,
}
impl ResponseStream {
#[must_use]
pub fn new<S>(stream: S) -> Self
where
S: Stream<Item = Result<Bytes, ProximaError>> + Send + 'static,
{
Self {
stream: Box::pin(stream),
#[cfg(feature = "std")]
trailers: None,
}
}
#[must_use]
pub fn from_chunk_stream(stream: ChunkStream) -> Self {
Self {
stream,
#[cfg(feature = "std")]
trailers: None,
}
}
#[must_use]
pub fn once(bytes: Bytes) -> Self {
Self::new(futures::stream::once(async move { Ok(bytes) }))
}
#[cfg(feature = "std")]
#[must_use]
pub fn with_trailers_slot(mut self, slot: TrailersSlot) -> Self {
self.trailers = Some(slot);
self
}
#[cfg(feature = "std")]
#[must_use]
pub fn with_trailers(mut self, trailers: HeaderList) -> Self {
self.trailers = Some(Arc::new(Mutex::new(Some(trailers))));
self
}
#[cfg(feature = "std")]
#[must_use]
pub fn trailers(&self) -> Option<HeaderList> {
self.trailers.as_ref()?.lock().clone()
}
#[cfg(feature = "std")]
#[must_use]
pub fn trailers_slot(&self) -> Option<&TrailersSlot> {
self.trailers.as_ref()
}
pub fn into_chunk_stream(self) -> ChunkStream {
self.stream
}
#[must_use]
pub fn chunk_stream_mut(&mut self) -> &mut ChunkStream {
&mut self.stream
}
pub async fn drain(self) -> Result<(), ProximaError> {
let mut stream = self.stream;
while let Some(item) = stream.next().await {
item?;
}
Ok(())
}
pub async fn collect(
self,
#[cfg(feature = "std")] cancel: Option<&Signal>,
) -> Result<Bytes, ProximaError> {
let mut stream = self.stream;
let mut chunks: Vec<Bytes> = Vec::new();
let mut total = 0usize;
loop {
#[cfg(feature = "std")]
let next = match cancel {
Some(token) => match futures::future::select(token.fired(), stream.next()).await {
futures::future::Either::Left(((), _)) => {
return Err(ProximaError::Body("body cancelled".into()));
}
futures::future::Either::Right((item, _)) => item,
},
None => stream.next().await,
};
#[cfg(not(feature = "std"))]
let next = stream.next().await;
match next {
Some(item) => {
let chunk = item?;
total += chunk.len();
chunks.push(chunk);
}
None => break,
}
}
if chunks.len() == 1 {
return Ok(chunks.remove(0));
}
let mut buffer = bytes::BytesMut::with_capacity(total);
for chunk in chunks {
buffer.extend_from_slice(&chunk);
}
Ok(buffer.freeze())
}
}
impl core::fmt::Debug for ResponseStream {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("ResponseStream")
.finish_non_exhaustive()
}
}
pub struct RequestStream {
stream: ChunkStream,
#[cfg(feature = "std")]
trailers: Option<TrailersSlot>,
}
impl RequestStream {
#[must_use]
pub fn new<S>(stream: S) -> Self
where
S: Stream<Item = Result<Bytes, ProximaError>> + Send + 'static,
{
Self {
stream: Box::pin(stream),
#[cfg(feature = "std")]
trailers: None,
}
}
#[must_use]
pub fn from_chunk_stream(stream: ChunkStream) -> Self {
Self {
stream,
#[cfg(feature = "std")]
trailers: None,
}
}
#[cfg(feature = "std")]
#[must_use]
pub fn with_trailers_slot(mut self, slot: TrailersSlot) -> Self {
self.trailers = Some(slot);
self
}
#[cfg(feature = "std")]
#[must_use]
pub fn trailers(&self) -> Option<HeaderList> {
self.trailers.as_ref()?.lock().clone()
}
#[cfg(feature = "std")]
#[must_use]
pub fn trailers_slot(&self) -> Option<&TrailersSlot> {
self.trailers.as_ref()
}
pub fn into_chunk_stream(self) -> ChunkStream {
self.stream
}
#[must_use]
pub fn chunk_stream_mut(&mut self) -> &mut ChunkStream {
&mut self.stream
}
pub async fn collect(self) -> Result<Bytes, ProximaError> {
let mut stream = self.stream;
let mut chunks: Vec<Bytes> = Vec::new();
let mut total = 0usize;
while let Some(item) = stream.next().await {
let chunk = item?;
total += chunk.len();
chunks.push(chunk);
}
if chunks.len() == 1 {
return Ok(chunks.remove(0));
}
let mut buffer = bytes::BytesMut::with_capacity(total);
for chunk in chunks {
buffer.extend_from_slice(&chunk);
}
Ok(buffer.freeze())
}
}
impl core::fmt::Debug for RequestStream {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("RequestStream")
.finish_non_exhaustive()
}
}
#[cfg(all(test, not(loom)))]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[derive(Debug, PartialEq, Eq)]
struct DummyRecord {
name: alloc::string::String,
count: u32,
}
#[test]
fn carry_new_then_downcast_returns_value() {
let carry = Carry::new(DummyRecord {
name: "carried".into(),
count: 3,
});
let view = carry
.downcast_ref::<DummyRecord>()
.expect("downcast succeeds");
assert_eq!(view.name, "carried");
assert_eq!(view.count, 3);
}
#[test]
fn carry_wrong_type_downcast_returns_none() {
let carry = Carry::new(DummyRecord {
name: "x".into(),
count: 1,
});
assert!(carry.downcast_ref::<u64>().is_none());
}
#[test]
fn carry_from_arc_shares_one_allocation() {
let shared = Arc::new(DummyRecord {
name: "shared".into(),
count: 9,
});
let strong_before = Arc::strong_count(&shared);
let carry = Carry::from_arc(Arc::clone(&shared));
assert_eq!(Arc::strong_count(&shared), strong_before + 1);
assert_eq!(
carry
.downcast_ref::<DummyRecord>()
.expect("downcast succeeds")
.count,
9
);
}
#[test]
fn carry_clone_is_one_arc_bump_not_a_record_copy() {
let shared = Arc::new(DummyRecord {
name: "fanout".into(),
count: 5,
});
let strong_before = Arc::strong_count(&shared);
let carry = Carry::from_arc(Arc::clone(&shared));
assert_eq!(Arc::strong_count(&shared), strong_before + 1);
let branch_a = carry.clone();
let branch_b = carry.clone();
assert_eq!(Arc::strong_count(&shared), strong_before + 3);
assert_eq!(
branch_a.downcast_ref::<DummyRecord>().expect("a").count,
branch_b.downcast_ref::<DummyRecord>().expect("b").count
);
}
#[proxima::test]
async fn response_stream_once_collects_to_original_bytes() {
let stream = ResponseStream::once(Bytes::from_static(b"hello"));
let collected = stream
.collect(
#[cfg(feature = "std")]
None,
)
.await
.expect("collect succeeds");
assert_eq!(&collected[..], b"hello");
}
#[proxima::test]
async fn response_stream_multi_chunk_concatenates_in_order() {
let stream = ResponseStream::new(futures::stream::iter([
Ok(Bytes::from_static(b"hel")),
Ok(Bytes::from_static(b"lo")),
]));
let collected = stream
.collect(
#[cfg(feature = "std")]
None,
)
.await
.expect("collect succeeds");
assert_eq!(&collected[..], b"hello");
}
#[proxima::test]
async fn response_stream_propagates_inner_error() {
let stream = ResponseStream::new(futures::stream::iter([Err::<Bytes, _>(
ProximaError::Body("upstream broken".into()),
)]));
let outcome = stream
.collect(
#[cfg(feature = "std")]
None,
)
.await;
assert!(matches!(outcome, Err(ProximaError::Body(_))));
}
#[cfg(feature = "std")]
#[proxima::test]
async fn response_stream_collect_aborts_on_cancel() {
use proxima_core::signal::Signal;
let cancel = Signal::new();
let pending = futures::stream::poll_fn(|_cx| {
core::task::Poll::<Option<Result<Bytes, ProximaError>>>::Pending
});
let stream = ResponseStream::new(pending);
let cancel_for_task = cancel.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
cancel_for_task.fire();
});
let outcome = tokio::time::timeout(
std::time::Duration::from_secs(1),
stream.collect(Some(&cancel)),
)
.await
.expect("collect returns before outer timeout");
assert!(matches!(outcome, Err(ProximaError::Body(ref msg)) if msg.contains("cancelled")));
}
#[cfg(feature = "std")]
#[proxima::test]
async fn response_stream_trailers_publish_after_construction() {
let slot: TrailersSlot = Arc::new(Mutex::new(None));
let stream =
ResponseStream::once(Bytes::from_static(b"x")).with_trailers_slot(slot.clone());
assert!(stream.trailers().is_none());
let mut headers = HeaderList::new();
headers.insert(Bytes::from_static(b"X-Done"), Bytes::from_static(b"true"));
*slot.lock() = Some(headers);
assert_eq!(
stream
.trailers()
.expect("trailers present")
.get_str("x-done"),
Some("true")
);
}
#[proxima::test]
#[case::ascii("hello")]
#[case::unicode("héllo")]
async fn request_stream_roundtrips_via_collect(#[case] input: &'static str) {
let stream = RequestStream::new(futures::stream::once({
let bytes = Bytes::copy_from_slice(input.as_bytes());
async move { Ok(bytes) }
}));
let collected = stream.collect().await.expect("collect succeeds");
assert_eq!(&collected[..], input.as_bytes());
}
}