Skip to main content

scm_sdk/
streaming.rs

1//! Bounded, caller-driven response bodies. Dropping a response releases its socket
2//! and concurrency permit; no reader or background task outlives the caller.
3pub mod sse;
4pub use sse::{SseEvent, SseStream, TerminalPolicy};
5
6/// A native schema-validated event stream. Unknown response fields and values survive.
7#[cfg(feature = "gateway")]
8pub struct ContractStream<T> {
9    stream: SseStream,
10    contract: EventContract,
11    options: RequestOptions,
12    session: Session,
13    failure: Option<Error>,
14    lifecycle: crate::observability::Lifecycle,
15    marker: std::marker::PhantomData<T>,
16}
17#[cfg(feature = "gateway")]
18pub(crate) struct EventContract {
19    pub schema: &'static str,
20    pub operation: &'static str,
21    pub status: u16,
22    pub request_id: Option<String>,
23    pub limits: crate::validation::ValidationLimits,
24}
25#[cfg(feature = "gateway")]
26impl<T: serde::de::DeserializeOwned> ContractStream<T> {
27    pub(crate) fn new(
28        stream: SseStream,
29        contract: EventContract,
30        options: RequestOptions,
31        session: Session,
32    ) -> Self {
33        let lifecycle = crate::observability::Lifecycle::received(
34            crate::observability::Operation::TypedStream,
35            contract.status,
36            contract.request_id.is_some(),
37        );
38        Self {
39            lifecycle,
40            stream,
41            contract,
42            options,
43            session,
44            failure: None,
45            marker: std::marker::PhantomData,
46        }
47    }
48    pub fn close(&mut self) {
49        self.lifecycle.closed();
50        self.stream.close();
51    }
52    /// Consume the caller-owned stream through the standard Rust Stream interface.
53    pub fn into_stream(self) -> impl futures_util::Stream<Item = Result<T>> + Send
54    where
55        T: Send,
56    {
57        futures_util::stream::try_unfold(self, |mut stream| async move {
58            stream
59                .next()
60                .await
61                .map(|value| value.map(|event| (event, stream)))
62        })
63    }
64    pub async fn next(&mut self) -> Result<Option<T>> {
65        if let Some(error) = &self.failure {
66            return Err(error.clone());
67        }
68        let event = loop {
69            match self.stream.next().await {
70                Ok(Some(event)) if event.data().is_empty() => {
71                    // Match the frozen typed Gateway parser's empty-data keepalives.
72                    // Raw SSE consumers still receive these framing events.
73                    let active = self
74                        .options
75                        .run(&self.session, async {
76                            tokio::task::yield_now().await;
77                            Ok(())
78                        })
79                        .await;
80                    if let Err(error) = active {
81                        let error = error
82                            .with_response_metadata(
83                                self.contract.status,
84                                self.contract.request_id.clone(),
85                            )
86                            .with_operation(self.contract.operation)
87                            .with_outcome(RemoteOutcome::Unknown);
88                        self.lifecycle.failed(&error);
89                        self.stream.close();
90                        self.failure = Some(error.clone());
91                        return Err(error);
92                    }
93                }
94                Ok(Some(event)) => break event,
95                Ok(None) => {
96                    self.lifecycle.success();
97                    return Ok(None);
98                }
99                Err(error) => {
100                    self.lifecycle.failed(&error);
101                    self.failure = Some(error.clone());
102                    return Err(error);
103                }
104            }
105        };
106        let result = self
107            .options
108            .run(&self.session, async {
109                let value = event.json::<serde_json::Value>()?;
110                let value = crate::validation::parse_with_limits(
111                    self.contract.schema,
112                    value,
113                    self.options.response_mode(),
114                    &self.contract.limits,
115                )
116                .map_err(|error| {
117                    Error::new(if error.kind() == ErrorKind::Limit {
118                        ErrorKind::Limit
119                    } else {
120                        ErrorKind::Decode
121                    })
122                })?;
123                crate::json_decode::from_value(value).map_err(|_| Error::new(ErrorKind::Decode))
124            })
125            .await;
126        match result {
127            Ok(value) => Ok(Some(value)),
128            Err(error) => {
129                let error = error
130                    .with_response_metadata(self.contract.status, self.contract.request_id.clone())
131                    .with_operation(self.contract.operation)
132                    .with_outcome(RemoteOutcome::Unknown);
133                self.lifecycle.failed(&error);
134                self.stream.close();
135                self.failure = Some(error.clone());
136                Err(error)
137            }
138        }
139    }
140}
141#[cfg(feature = "gateway")]
142impl<T> fmt::Debug for ContractStream<T> {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        f.debug_struct("ContractStream")
145            .field("operation", &self.contract.operation)
146            .field("stream", &self.stream)
147            .finish_non_exhaustive()
148    }
149}
150
151use crate::{
152    Error, ErrorKind, Limits, RemoteOutcome, RequestOptions, Result, Session, transport::BodyStream,
153};
154use bytes::Bytes;
155use futures_util::StreamExt;
156use http::{HeaderMap, StatusCode};
157use std::{fmt, time::Duration};
158use tokio::{sync::OwnedSemaphorePermit, time::Instant};
159use tokio_util::sync::CancellationToken;
160
161pub struct ResponseStream {
162    status: StatusCode,
163    headers: HeaderMap,
164    request_id: Option<String>,
165    body: Option<BodyStream>,
166    permit: Option<OwnedSemaphorePermit>,
167    session: Session,
168    cancellation: CancellationToken,
169    deadline: Option<Instant>,
170    idle: Duration,
171    operation: &'static str,
172    max_bytes: usize,
173    consumed: usize,
174    lifecycle: crate::observability::Lifecycle,
175}
176
177impl ResponseStream {
178    pub(crate) fn new(
179        response: crate::transport::TransportResponse,
180        permit: OwnedSemaphorePermit,
181        session: Session,
182        options: &RequestOptions,
183        explicit_deadline: bool,
184        limits: &Limits,
185        operation: &'static str,
186    ) -> Self {
187        let deadline = match (
188            explicit_deadline.then_some(options.deadline()),
189            limits
190                .stream_session_timeout
191                .and_then(|v| Instant::now().checked_add(v)),
192        ) {
193            (Some(a), Some(b)) => Some(a.min(b)),
194            (a, b) => a.or(b),
195        };
196        let request_id = crate::engine::error_metadata(&response.headers, None).0;
197        let mut headers = response.headers;
198        for value in headers.values_mut() {
199            value.set_sensitive(true);
200        }
201        let lifecycle = crate::observability::Lifecycle::received(
202            crate::observability::Operation::BodyStream,
203            response.status.as_u16(),
204            request_id.is_some(),
205        );
206        Self {
207            lifecycle,
208            status: response.status,
209            headers,
210            request_id,
211            body: Some(response.body),
212            permit: Some(permit),
213            session,
214            cancellation: options.cancellation_token(),
215            deadline,
216            idle: limits.stream_idle_timeout,
217            operation,
218            max_bytes: limits.max_response_bytes,
219            consumed: 0,
220        }
221    }
222    pub fn status(&self) -> StatusCode {
223        self.status
224    }
225    /// Explicit access to response headers; values are marked sensitive for formatting.
226    pub fn headers(&self) -> &HeaderMap {
227        &self.headers
228    }
229    /// Allowlisted correlation metadata retained through all body and parser errors.
230    pub fn request_id(&self) -> Option<&str> {
231        self.request_id.as_deref()
232    }
233    pub fn bytes_consumed(&self) -> usize {
234        self.consumed
235    }
236    pub fn close(&mut self) {
237        self.lifecycle.closed();
238        self.body = None;
239        self.permit = None;
240    }
241    pub(crate) fn check_active(&self) -> Result<()> {
242        let kind = if self.cancellation.is_cancelled() || self.session.is_cancelled() {
243            Some(ErrorKind::Cancelled)
244        } else if self.deadline.is_some_and(|value| Instant::now() >= value) {
245            Some(ErrorKind::Deadline)
246        } else {
247            None
248        };
249        match kind {
250            Some(kind) => Err(Error::new(kind)
251                .with_response_metadata(self.status.as_u16(), self.request_id.clone())
252                .with_operation(self.operation)
253                .with_outcome(RemoteOutcome::Unknown)),
254            None => Ok(()),
255        }
256    }
257    /// Read the next decompressed chunk within idle, caller, and session budgets.
258    /// No stream is automatically replayed, including before its first chunk.
259    pub async fn next(&mut self) -> Result<Option<Bytes>> {
260        if self.body.is_none() {
261            return Ok(None);
262        }
263        let result = async {
264            self.check_active()?;
265            let now = Instant::now();
266            let deadline = now
267                .checked_add(self.idle)
268                .ok_or_else(|| Error::new(ErrorKind::Configuration))?;
269            let deadline = self.deadline.map_or(deadline, |value| value.min(deadline));
270            if now >= deadline {
271                return Err(Error::new(ErrorKind::Deadline));
272            }
273            let body = self
274                .body
275                .as_mut()
276                .ok_or_else(|| Error::new(ErrorKind::Protocol))?;
277            let value = tokio::select! {biased;
278                _=self.cancellation.cancelled()=>Err(Error::new(ErrorKind::Cancelled)),
279                _=self.session.cancelled()=>Err(Error::new(ErrorKind::Cancelled)),
280                _=tokio::time::sleep_until(deadline)=>Err(Error::new(ErrorKind::Deadline)),
281                value=body.next()=>value.transpose(),
282            }?;
283            if self.cancellation.is_cancelled() || self.session.is_cancelled() {
284                return Err(Error::new(ErrorKind::Cancelled));
285            }
286            if Instant::now() >= deadline {
287                return Err(Error::new(ErrorKind::Deadline));
288            }
289            if let Some(bytes) = &value {
290                self.consumed = self
291                    .consumed
292                    .checked_add(bytes.len())
293                    .ok_or_else(|| Error::new(ErrorKind::Limit))?;
294                if self.consumed > self.max_bytes {
295                    return Err(Error::new(ErrorKind::Limit));
296                }
297            }
298            Ok(value)
299        }
300        .await;
301        match result {
302            Ok(None) => {
303                self.lifecycle.success();
304                self.close();
305                Ok(None)
306            }
307            Ok(value) => Ok(value),
308            Err(error) => {
309                let error = crate::engine::body_failure(error)
310                    .with_response_metadata(self.status.as_u16(), self.request_id.clone())
311                    .with_operation(self.operation)
312                    .with_outcome(RemoteOutcome::Unknown);
313                self.lifecycle.failed(&error);
314                self.close();
315                Err(error)
316            }
317        }
318    }
319}
320impl fmt::Debug for ResponseStream {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        f.debug_struct("ResponseStream")
323            .field("operation", &self.operation)
324            .field("status", &self.status)
325            .field("bytes_consumed", &self.consumed)
326            .field("closed", &self.body.is_none())
327            .finish_non_exhaustive()
328    }
329}