Skip to main content

cratestack_client_rust/
streaming_callback.rs

1// -----------------------------------------------------------------------------
2// FFI / callback-shaped streaming consumer
3//
4// Synchronous from the caller's perspective: pass a callback, return
5// when the stream is done. The callback receives raw item bytes
6// (one CBOR-encoded value per call) so the FFI side can decode using
7// whatever native CBOR library it prefers.
8// -----------------------------------------------------------------------------
9
10use serde::{Deserialize, Serialize};
11
12use crate::codec::{CBOR_SEQUENCE_CONTENT_TYPE, media_type_matches};
13use crate::error::ClientError;
14use crate::runtime::wire::{RuntimeErrorCode, RuntimeErrorWire};
15use crate::streaming::CborSeqChunkDecoder;
16
17/// FFI-shaped chunk delivered to the `execute_streamed` callback.
18/// `Item` carries one CBOR-encoded item's raw bytes; `Error` is
19/// terminal; `End` is terminal and indicates a clean stream close.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub enum RuntimeChunkWire {
22    /// One complete cbor-seq item. The bytes are CBOR-encoded — decode
23    /// on the FFI side with whatever native library the host has.
24    Item(Vec<u8>),
25    /// Terminal: the stream ended cleanly. No further chunks follow.
26    End,
27    /// Terminal: the stream failed mid-flight. No further chunks follow.
28    Error(RuntimeErrorWire),
29}
30
31/// Drive a streaming response through a callback. Used by
32/// `RuntimeHandle::execute_streamed` — the callback returns `false` to
33/// cancel the stream early. The function returns once the stream is
34/// done (by completion, error, or cancellation).
35///
36/// This path forwards raw item bytes to an FFI-side decoder rather than
37/// deserializing to a concrete `T` itself, so unlike
38/// `pump_streamed_response_typed` it has no buffered-cbor fallback to
39/// degrade to. It still checks `Content-Type` up front and fails with a
40/// clear message naming what was received instead of letting a
41/// non-cbor-seq body (e.g. negotiation picked buffered `application/cbor`)
42/// crash the frame decoder on the first chunk.
43///
44/// Its only caller, `RuntimeHandle`, is native-only (cratestack#1104).
45#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
46pub(crate) async fn pump_streamed_response_callback<F>(
47    response: reqwest::Response,
48    mut on_chunk: F,
49) -> Result<(), RuntimeErrorWire>
50where
51    F: FnMut(RuntimeChunkWire) -> bool,
52{
53    use futures_util::StreamExt;
54
55    let content_type = response
56        .headers()
57        .get(reqwest::header::CONTENT_TYPE)
58        .and_then(|value| value.to_str().ok())
59        .unwrap_or_default()
60        .to_owned();
61    if !media_type_matches(&content_type, CBOR_SEQUENCE_CONTENT_TYPE) {
62        let err = RuntimeErrorWire {
63            code: RuntimeErrorCode::InvalidResponse,
64            http_status: None,
65            message: format!(
66                "streaming response had Content-Type {content_type:?}, needed \
67                 {CBOR_SEQUENCE_CONTENT_TYPE}",
68            ),
69            remote_code: None,
70            remote_body: None,
71        };
72        on_chunk(RuntimeChunkWire::Error(err.clone()));
73        return Err(err);
74    }
75
76    let mut byte_stream = response.bytes_stream();
77    let mut decoder = CborSeqChunkDecoder::new();
78    loop {
79        let chunk_result = byte_stream.next().await;
80        let chunk = match chunk_result {
81            Some(Ok(c)) => c,
82            Some(Err(error)) => {
83                let err = RuntimeErrorWire::from(ClientError::Transport(error.into()));
84                on_chunk(RuntimeChunkWire::Error(err.clone()));
85                return Err(err);
86            }
87            None => {
88                if decoder.pending_len() > 0 {
89                    let err = RuntimeErrorWire {
90                        code: RuntimeErrorCode::InvalidResponse,
91                        http_status: None,
92                        message: format!(
93                            "stream ended with {} bytes buffered (incomplete final item)",
94                            decoder.pending_len(),
95                        ),
96                        remote_code: None,
97                        remote_body: None,
98                    };
99                    on_chunk(RuntimeChunkWire::Error(err.clone()));
100                    return Err(err);
101                }
102                on_chunk(RuntimeChunkWire::End);
103                return Ok(());
104            }
105        };
106        let items = match decoder.feed_chunk(&chunk) {
107            Ok(items) => items,
108            Err(error) => {
109                let err = RuntimeErrorWire::from(ClientError::Codec(error));
110                on_chunk(RuntimeChunkWire::Error(err.clone()));
111                return Err(err);
112            }
113        };
114        for item_bytes in items {
115            if !on_chunk(RuntimeChunkWire::Item(item_bytes)) {
116                // Caller cancelled.
117                return Ok(());
118            }
119        }
120    }
121}