proxima_primitives/pipe/
body.rs1#![cfg(feature = "alloc")]
20
21use core::any::Any;
22use core::pin::Pin;
23
24use alloc::boxed::Box;
25use alloc::vec::Vec;
26
27use bytes::Bytes;
28use futures::stream::Stream;
29use futures::stream::StreamExt;
30
31#[cfg(feature = "std")]
32use alloc::sync::Arc;
33#[cfg(feature = "std")]
34use proxima_core::signal::Signal;
35
36#[cfg(feature = "std")]
37use crate::sync::blocking::Mutex;
38
39#[cfg(not(feature = "std"))]
40use alloc::sync::Arc;
41
42#[cfg(feature = "std")]
43use crate::pipe::header_list::HeaderList;
44use proxima_core::ProximaError;
45
46pub type ChunkStream = Pin<Box<dyn Stream<Item = Result<Bytes, ProximaError>> + Send>>;
51
52#[cfg(feature = "std")]
57pub type TrailersSlot = Arc<Mutex<Option<HeaderList>>>;
58
59#[derive(Clone)]
65pub struct Carry(Arc<dyn Any + Send + Sync>);
66
67impl Carry {
68 #[must_use]
70 pub fn new<T>(value: T) -> Self
71 where
72 T: Any + Send + Sync + 'static,
73 {
74 Self(Arc::new(value))
75 }
76
77 #[must_use]
82 pub fn from_arc<T>(value: Arc<T>) -> Self
83 where
84 T: Any + Send + Sync + 'static,
85 {
86 Self(value)
87 }
88
89 #[must_use]
92 pub fn downcast_ref<T>(&self) -> Option<&T>
93 where
94 T: Any + Send + Sync + 'static,
95 {
96 self.0.downcast_ref::<T>()
97 }
98}
99
100impl core::fmt::Debug for Carry {
101 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102 formatter.debug_struct("Carry").finish_non_exhaustive()
103 }
104}
105
106pub struct ResponseStream {
110 stream: ChunkStream,
111 #[cfg(feature = "std")]
113 trailers: Option<TrailersSlot>,
114}
115
116impl ResponseStream {
117 #[must_use]
119 pub fn new<S>(stream: S) -> Self
120 where
121 S: Stream<Item = Result<Bytes, ProximaError>> + Send + 'static,
122 {
123 Self {
124 stream: Box::pin(stream),
125 #[cfg(feature = "std")]
126 trailers: None,
127 }
128 }
129
130 #[must_use]
132 pub fn from_chunk_stream(stream: ChunkStream) -> Self {
133 Self {
134 stream,
135 #[cfg(feature = "std")]
136 trailers: None,
137 }
138 }
139
140 #[must_use]
143 pub fn once(bytes: Bytes) -> Self {
144 Self::new(futures::stream::once(async move { Ok(bytes) }))
145 }
146
147 #[cfg(feature = "std")]
149 #[must_use]
150 pub fn with_trailers_slot(mut self, slot: TrailersSlot) -> Self {
151 self.trailers = Some(slot);
152 self
153 }
154
155 #[cfg(feature = "std")]
157 #[must_use]
158 pub fn with_trailers(mut self, trailers: HeaderList) -> Self {
159 self.trailers = Some(Arc::new(Mutex::new(Some(trailers))));
160 self
161 }
162
163 #[cfg(feature = "std")]
166 #[must_use]
167 pub fn trailers(&self) -> Option<HeaderList> {
168 self.trailers.as_ref()?.lock().clone()
169 }
170
171 #[cfg(feature = "std")]
174 #[must_use]
175 pub fn trailers_slot(&self) -> Option<&TrailersSlot> {
176 self.trailers.as_ref()
177 }
178
179 pub fn into_chunk_stream(self) -> ChunkStream {
181 self.stream
182 }
183
184 #[must_use]
186 pub fn chunk_stream_mut(&mut self) -> &mut ChunkStream {
187 &mut self.stream
188 }
189
190 pub async fn drain(self) -> Result<(), ProximaError> {
195 let mut stream = self.stream;
196 while let Some(item) = stream.next().await {
197 item?;
198 }
199 Ok(())
200 }
201
202 pub async fn collect(
205 self,
206 #[cfg(feature = "std")] cancel: Option<&Signal>,
207 ) -> Result<Bytes, ProximaError> {
208 let mut stream = self.stream;
209 let mut chunks: Vec<Bytes> = Vec::new();
210 let mut total = 0usize;
211 loop {
212 #[cfg(feature = "std")]
213 let next = match cancel {
214 Some(token) => match futures::future::select(token.fired(), stream.next()).await {
217 futures::future::Either::Left(((), _)) => {
218 return Err(ProximaError::Body("body cancelled".into()));
219 }
220 futures::future::Either::Right((item, _)) => item,
221 },
222 None => stream.next().await,
223 };
224 #[cfg(not(feature = "std"))]
225 let next = stream.next().await;
226 match next {
227 Some(item) => {
228 let chunk = item?;
229 total += chunk.len();
230 chunks.push(chunk);
231 }
232 None => break,
233 }
234 }
235 if chunks.len() == 1 {
236 return Ok(chunks.remove(0));
237 }
238 let mut buffer = bytes::BytesMut::with_capacity(total);
239 for chunk in chunks {
240 buffer.extend_from_slice(&chunk);
241 }
242 Ok(buffer.freeze())
243 }
244}
245
246impl core::fmt::Debug for ResponseStream {
247 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
248 formatter
249 .debug_struct("ResponseStream")
250 .finish_non_exhaustive()
251 }
252}
253
254pub struct RequestStream {
261 stream: ChunkStream,
262 #[cfg(feature = "std")]
264 trailers: Option<TrailersSlot>,
265}
266
267impl RequestStream {
268 #[must_use]
269 pub fn new<S>(stream: S) -> Self
270 where
271 S: Stream<Item = Result<Bytes, ProximaError>> + Send + 'static,
272 {
273 Self {
274 stream: Box::pin(stream),
275 #[cfg(feature = "std")]
276 trailers: None,
277 }
278 }
279
280 #[must_use]
281 pub fn from_chunk_stream(stream: ChunkStream) -> Self {
282 Self {
283 stream,
284 #[cfg(feature = "std")]
285 trailers: None,
286 }
287 }
288
289 #[cfg(feature = "std")]
291 #[must_use]
292 pub fn with_trailers_slot(mut self, slot: TrailersSlot) -> Self {
293 self.trailers = Some(slot);
294 self
295 }
296
297 #[cfg(feature = "std")]
300 #[must_use]
301 pub fn trailers(&self) -> Option<HeaderList> {
302 self.trailers.as_ref()?.lock().clone()
303 }
304
305 #[cfg(feature = "std")]
308 #[must_use]
309 pub fn trailers_slot(&self) -> Option<&TrailersSlot> {
310 self.trailers.as_ref()
311 }
312
313 pub fn into_chunk_stream(self) -> ChunkStream {
314 self.stream
315 }
316
317 #[must_use]
318 pub fn chunk_stream_mut(&mut self) -> &mut ChunkStream {
319 &mut self.stream
320 }
321
322 pub async fn collect(self) -> Result<Bytes, ProximaError> {
324 let mut stream = self.stream;
325 let mut chunks: Vec<Bytes> = Vec::new();
326 let mut total = 0usize;
327 while let Some(item) = stream.next().await {
328 let chunk = item?;
329 total += chunk.len();
330 chunks.push(chunk);
331 }
332 if chunks.len() == 1 {
333 return Ok(chunks.remove(0));
334 }
335 let mut buffer = bytes::BytesMut::with_capacity(total);
336 for chunk in chunks {
337 buffer.extend_from_slice(&chunk);
338 }
339 Ok(buffer.freeze())
340 }
341}
342
343impl core::fmt::Debug for RequestStream {
344 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
345 formatter
346 .debug_struct("RequestStream")
347 .finish_non_exhaustive()
348 }
349}
350
351#[cfg(all(test, not(loom)))]
356#[allow(clippy::unwrap_used, clippy::expect_used)]
357mod tests {
358 use super::*;
359
360 #[derive(Debug, PartialEq, Eq)]
361 struct DummyRecord {
362 name: alloc::string::String,
363 count: u32,
364 }
365
366 #[test]
367 fn carry_new_then_downcast_returns_value() {
368 let carry = Carry::new(DummyRecord {
369 name: "carried".into(),
370 count: 3,
371 });
372 let view = carry
373 .downcast_ref::<DummyRecord>()
374 .expect("downcast succeeds");
375 assert_eq!(view.name, "carried");
376 assert_eq!(view.count, 3);
377 }
378
379 #[test]
380 fn carry_wrong_type_downcast_returns_none() {
381 let carry = Carry::new(DummyRecord {
382 name: "x".into(),
383 count: 1,
384 });
385 assert!(carry.downcast_ref::<u64>().is_none());
386 }
387
388 #[test]
389 fn carry_from_arc_shares_one_allocation() {
390 let shared = Arc::new(DummyRecord {
391 name: "shared".into(),
392 count: 9,
393 });
394 let strong_before = Arc::strong_count(&shared);
395 let carry = Carry::from_arc(Arc::clone(&shared));
396 assert_eq!(Arc::strong_count(&shared), strong_before + 1);
397 assert_eq!(
398 carry
399 .downcast_ref::<DummyRecord>()
400 .expect("downcast succeeds")
401 .count,
402 9
403 );
404 }
405
406 #[test]
407 fn carry_clone_is_one_arc_bump_not_a_record_copy() {
408 let shared = Arc::new(DummyRecord {
412 name: "fanout".into(),
413 count: 5,
414 });
415 let strong_before = Arc::strong_count(&shared);
416 let carry = Carry::from_arc(Arc::clone(&shared));
417 assert_eq!(Arc::strong_count(&shared), strong_before + 1);
418 let branch_a = carry.clone();
419 let branch_b = carry.clone();
420 assert_eq!(Arc::strong_count(&shared), strong_before + 3);
421 assert_eq!(
422 branch_a.downcast_ref::<DummyRecord>().expect("a").count,
423 branch_b.downcast_ref::<DummyRecord>().expect("b").count
424 );
425 }
426
427 #[proxima::test]
428 async fn response_stream_once_collects_to_original_bytes() {
429 let stream = ResponseStream::once(Bytes::from_static(b"hello"));
430 let collected = stream
431 .collect(
432 #[cfg(feature = "std")]
433 None,
434 )
435 .await
436 .expect("collect succeeds");
437 assert_eq!(&collected[..], b"hello");
438 }
439
440 #[proxima::test]
441 async fn response_stream_multi_chunk_concatenates_in_order() {
442 let stream = ResponseStream::new(futures::stream::iter([
443 Ok(Bytes::from_static(b"hel")),
444 Ok(Bytes::from_static(b"lo")),
445 ]));
446 let collected = stream
447 .collect(
448 #[cfg(feature = "std")]
449 None,
450 )
451 .await
452 .expect("collect succeeds");
453 assert_eq!(&collected[..], b"hello");
454 }
455
456 #[proxima::test]
457 async fn response_stream_propagates_inner_error() {
458 let stream = ResponseStream::new(futures::stream::iter([Err::<Bytes, _>(
459 ProximaError::Body("upstream broken".into()),
460 )]));
461 let outcome = stream
462 .collect(
463 #[cfg(feature = "std")]
464 None,
465 )
466 .await;
467 assert!(matches!(outcome, Err(ProximaError::Body(_))));
468 }
469
470 #[cfg(feature = "std")]
471 #[proxima::test]
472 async fn response_stream_collect_aborts_on_cancel() {
473 use proxima_core::signal::Signal;
474 let cancel = Signal::new();
475 let pending = futures::stream::poll_fn(|_cx| {
476 core::task::Poll::<Option<Result<Bytes, ProximaError>>>::Pending
477 });
478 let stream = ResponseStream::new(pending);
479 let cancel_for_task = cancel.clone();
480 tokio::spawn(async move {
481 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
482 cancel_for_task.fire();
483 });
484 let outcome = tokio::time::timeout(
485 std::time::Duration::from_secs(1),
486 stream.collect(Some(&cancel)),
487 )
488 .await
489 .expect("collect returns before outer timeout");
490 assert!(matches!(outcome, Err(ProximaError::Body(ref msg)) if msg.contains("cancelled")));
491 }
492
493 #[cfg(feature = "std")]
494 #[proxima::test]
495 async fn response_stream_trailers_publish_after_construction() {
496 let slot: TrailersSlot = Arc::new(Mutex::new(None));
497 let stream =
498 ResponseStream::once(Bytes::from_static(b"x")).with_trailers_slot(slot.clone());
499 assert!(stream.trailers().is_none());
500 let mut headers = HeaderList::new();
501 headers.insert(Bytes::from_static(b"X-Done"), Bytes::from_static(b"true"));
502 *slot.lock() = Some(headers);
503 assert_eq!(
504 stream
505 .trailers()
506 .expect("trailers present")
507 .get_str("x-done"),
508 Some("true")
509 );
510 }
511
512 #[proxima::test]
513 #[case::ascii("hello")]
514 #[case::unicode("héllo")]
515 async fn request_stream_roundtrips_via_collect(#[case] input: &'static str) {
516 let stream = RequestStream::new(futures::stream::once({
517 let bytes = Bytes::copy_from_slice(input.as_bytes());
518 async move { Ok(bytes) }
519 }));
520 let collected = stream.collect().await.expect("collect succeeds");
521 assert_eq!(&collected[..], input.as_bytes());
522 }
523}