Skip to main content

oxirs_gql/
response_streaming.rs

1//! GraphQL Response Streaming
2//!
3//! This module provides efficient streaming for large GraphQL responses:
4//! - **Chunked Transfer**: Stream responses in chunks for memory efficiency
5//! - **Incremental Delivery**: Support for @defer and @stream directives
6//! - **Backpressure Handling**: Automatic flow control based on client capacity
7//! - **Progress Tracking**: Monitor streaming progress and performance
8//! - **Compression**: Optional gzip/deflate compression for large responses
9//! - **Resumable Streams**: Support for resuming interrupted streams
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16use tokio::sync::{mpsc, RwLock};
17
18/// Streaming mode for response delivery
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum StreamingMode {
21    /// No streaming - return complete response
22    None,
23    /// Chunked transfer encoding
24    Chunked,
25    /// Incremental delivery with @defer
26    Deferred,
27    /// Array streaming with @stream
28    Streamed,
29    /// Multipart response format (for subscriptions)
30    Multipart,
31}
32
33/// Compression type for streamed responses
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub enum CompressionType {
36    /// No compression
37    None,
38    /// Gzip compression
39    Gzip,
40    /// Deflate compression
41    Deflate,
42    /// Brotli compression
43    Brotli,
44}
45
46/// A chunk of response data
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ResponseChunk {
49    /// Chunk ID for ordering
50    pub id: u64,
51    /// Path in response (for incremental delivery)
52    pub path: Option<Vec<PathSegment>>,
53    /// Data payload
54    pub data: serde_json::Value,
55    /// Whether this chunk has errors
56    pub has_errors: bool,
57    /// Errors in this chunk
58    pub errors: Vec<StreamError>,
59    /// Whether this is the final chunk
60    pub is_final: bool,
61    /// Whether more data is pending for this path
62    pub has_next: bool,
63    /// Label for @defer/@stream directive
64    pub label: Option<String>,
65}
66
67/// Path segment in response
68#[derive(Debug, Clone, Serialize, Deserialize)]
69#[serde(untagged)]
70pub enum PathSegment {
71    /// Field name
72    Field(String),
73    /// Array index
74    Index(usize),
75}
76
77/// Error in stream
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct StreamError {
80    /// Error message
81    pub message: String,
82    /// Path to error location
83    pub path: Option<Vec<PathSegment>>,
84    /// Error extensions
85    pub extensions: Option<HashMap<String, serde_json::Value>>,
86}
87
88/// Configuration for response streaming
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct StreamingConfig {
91    /// Default streaming mode
92    pub default_mode: StreamingMode,
93    /// Maximum chunk size in bytes
94    pub max_chunk_size: usize,
95    /// Minimum chunk size in bytes (for batching small items)
96    pub min_chunk_size: usize,
97    /// Enable compression
98    pub compression: CompressionType,
99    /// Compression threshold in bytes
100    pub compression_threshold: usize,
101    /// Maximum concurrent deferred fields
102    pub max_concurrent_deferred: usize,
103    /// Timeout for individual chunks
104    pub chunk_timeout: Duration,
105    /// Enable progress tracking
106    pub track_progress: bool,
107    /// Buffer size for backpressure
108    pub buffer_size: usize,
109    /// Enable chunk acknowledgment
110    pub require_ack: bool,
111    /// Heartbeat interval for long-running streams
112    pub heartbeat_interval: Option<Duration>,
113}
114
115impl Default for StreamingConfig {
116    fn default() -> Self {
117        Self {
118            default_mode: StreamingMode::Chunked,
119            max_chunk_size: 64 * 1024, // 64KB
120            min_chunk_size: 1024,      // 1KB
121            compression: CompressionType::None,
122            compression_threshold: 1024, // Compress if > 1KB
123            max_concurrent_deferred: 10,
124            chunk_timeout: Duration::from_secs(30),
125            track_progress: true,
126            buffer_size: 100,
127            require_ack: false,
128            heartbeat_interval: Some(Duration::from_secs(15)),
129        }
130    }
131}
132
133/// Streaming progress information
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct StreamProgress {
136    /// Stream ID
137    pub stream_id: String,
138    /// Total chunks sent
139    pub chunks_sent: u64,
140    /// Total bytes sent
141    pub bytes_sent: u64,
142    /// Estimated total bytes (if known)
143    pub total_bytes: Option<u64>,
144    /// Current throughput (bytes/sec)
145    pub throughput_bps: f64,
146    /// Elapsed time
147    pub elapsed_ms: u64,
148    /// Pending chunks in buffer
149    pub pending_chunks: usize,
150    /// Whether stream is complete
151    pub is_complete: bool,
152    /// Whether stream has errors
153    pub has_errors: bool,
154}
155
156/// Response streamer for a single response
157pub struct ResponseStreamer {
158    /// Stream ID
159    id: String,
160    /// Configuration
161    config: StreamingConfig,
162    /// Channel sender for chunks
163    sender: mpsc::Sender<ResponseChunk>,
164    /// Current chunk ID
165    chunk_id: AtomicU64,
166    /// Start time
167    start_time: Instant,
168    /// Bytes sent
169    bytes_sent: AtomicU64,
170    /// State
171    state: Arc<RwLock<StreamerState>>,
172}
173
174/// Internal streamer state
175struct StreamerState {
176    /// Whether stream is active
177    is_active: bool,
178    /// Deferred paths pending
179    pending_paths: Vec<Vec<PathSegment>>,
180    /// Errors encountered
181    errors: Vec<StreamError>,
182    /// Progress callback
183    progress_callback: Option<Box<dyn Fn(StreamProgress) + Send + Sync>>,
184}
185
186impl ResponseStreamer {
187    /// Create a new response streamer
188    pub fn new(config: StreamingConfig) -> (Self, mpsc::Receiver<ResponseChunk>) {
189        let (sender, receiver) = mpsc::channel(config.buffer_size);
190        let id = uuid::Uuid::new_v4().to_string();
191
192        let streamer = Self {
193            id,
194            config,
195            sender,
196            chunk_id: AtomicU64::new(0),
197            start_time: Instant::now(),
198            bytes_sent: AtomicU64::new(0),
199            state: Arc::new(RwLock::new(StreamerState {
200                is_active: true,
201                pending_paths: Vec::new(),
202                errors: Vec::new(),
203                progress_callback: None,
204            })),
205        };
206
207        (streamer, receiver)
208    }
209
210    /// Get stream ID
211    pub fn id(&self) -> &str {
212        &self.id
213    }
214
215    /// Set progress callback
216    pub async fn set_progress_callback<F>(&self, callback: F)
217    where
218        F: Fn(StreamProgress) + Send + Sync + 'static,
219    {
220        let mut state = self.state.write().await;
221        state.progress_callback = Some(Box::new(callback));
222    }
223
224    /// Send a chunk
225    pub async fn send_chunk(
226        &self,
227        data: serde_json::Value,
228        path: Option<Vec<PathSegment>>,
229        label: Option<String>,
230        is_final: bool,
231        has_next: bool,
232    ) -> Result<(), StreamError> {
233        let state = self.state.read().await;
234        if !state.is_active {
235            return Err(StreamError {
236                message: "Stream is not active".to_string(),
237                path: None,
238                extensions: None,
239            });
240        }
241        drop(state);
242
243        let chunk_id = self.chunk_id.fetch_add(1, Ordering::SeqCst);
244
245        // Calculate chunk size
246        let chunk_json = serde_json::to_string(&data).unwrap_or_default();
247        let chunk_size = chunk_json.len() as u64;
248        self.bytes_sent.fetch_add(chunk_size, Ordering::SeqCst);
249
250        let chunk = ResponseChunk {
251            id: chunk_id,
252            path,
253            data,
254            has_errors: false,
255            errors: Vec::new(),
256            is_final,
257            has_next,
258            label,
259        };
260
261        self.sender.send(chunk).await.map_err(|_| StreamError {
262            message: "Failed to send chunk".to_string(),
263            path: None,
264            extensions: None,
265        })?;
266
267        // Report progress
268        if self.config.track_progress {
269            self.report_progress().await;
270        }
271
272        Ok(())
273    }
274
275    /// Send an error chunk
276    pub async fn send_error(&self, error: StreamError, path: Option<Vec<PathSegment>>) {
277        let chunk_id = self.chunk_id.fetch_add(1, Ordering::SeqCst);
278
279        let chunk = ResponseChunk {
280            id: chunk_id,
281            path,
282            data: serde_json::Value::Null,
283            has_errors: true,
284            errors: vec![error.clone()],
285            is_final: false,
286            has_next: true,
287            label: None,
288        };
289
290        let _ = self.sender.send(chunk).await;
291
292        // Track error
293        let mut state = self.state.write().await;
294        state.errors.push(error);
295    }
296
297    /// Send heartbeat to keep connection alive
298    pub async fn send_heartbeat(&self) -> Result<(), StreamError> {
299        let chunk = ResponseChunk {
300            id: self.chunk_id.fetch_add(1, Ordering::SeqCst),
301            path: None,
302            data: serde_json::json!({"__heartbeat": true}),
303            has_errors: false,
304            errors: Vec::new(),
305            is_final: false,
306            has_next: true,
307            label: Some("__heartbeat".to_string()),
308        };
309
310        self.sender.send(chunk).await.map_err(|_| StreamError {
311            message: "Failed to send heartbeat".to_string(),
312            path: None,
313            extensions: None,
314        })
315    }
316
317    /// Complete the stream
318    pub async fn complete(&self) {
319        // Send final chunk
320        let chunk = ResponseChunk {
321            id: self.chunk_id.fetch_add(1, Ordering::SeqCst),
322            path: None,
323            data: serde_json::Value::Null,
324            has_errors: false,
325            errors: Vec::new(),
326            is_final: true,
327            has_next: false,
328            label: None,
329        };
330
331        let _ = self.sender.send(chunk).await;
332
333        // Mark as inactive
334        let mut state = self.state.write().await;
335        state.is_active = false;
336    }
337
338    /// Get current progress
339    pub async fn get_progress(&self) -> StreamProgress {
340        let state = self.state.read().await;
341        let elapsed = self.start_time.elapsed();
342        let bytes = self.bytes_sent.load(Ordering::SeqCst);
343
344        StreamProgress {
345            stream_id: self.id.clone(),
346            chunks_sent: self.chunk_id.load(Ordering::SeqCst),
347            bytes_sent: bytes,
348            total_bytes: None,
349            throughput_bps: if elapsed.as_secs_f64() > 0.0 {
350                bytes as f64 / elapsed.as_secs_f64()
351            } else {
352                0.0
353            },
354            elapsed_ms: elapsed.as_millis() as u64,
355            pending_chunks: state.pending_paths.len(),
356            is_complete: !state.is_active,
357            has_errors: !state.errors.is_empty(),
358        }
359    }
360
361    /// Report progress via callback
362    async fn report_progress(&self) {
363        let progress = self.get_progress().await;
364        let state = self.state.read().await;
365        if let Some(ref callback) = state.progress_callback {
366            callback(progress);
367        }
368    }
369}
370
371/// Incremental delivery manager for @defer and @stream
372pub struct IncrementalDeliveryManager {
373    /// Configuration
374    config: StreamingConfig,
375    /// Active streams
376    streams: Arc<RwLock<HashMap<String, StreamInfo>>>,
377    /// Global statistics
378    stats: Arc<RwLock<StreamingStatistics>>,
379}
380
381/// Information about an active stream
382#[derive(Debug, Clone)]
383struct StreamInfo {
384    /// Stream ID
385    id: String,
386    /// Mode
387    #[allow(dead_code)]
388    mode: StreamingMode,
389    /// Start time
390    started_at: Instant,
391    /// Bytes sent
392    bytes_sent: u64,
393    /// Chunks sent
394    chunks_sent: u64,
395    /// Client ID
396    #[allow(dead_code)]
397    client_id: Option<String>,
398}
399
400/// Global streaming statistics
401#[derive(Debug, Clone, Default, Serialize, Deserialize)]
402pub struct StreamingStatistics {
403    /// Total streams created
404    pub total_streams: u64,
405    /// Currently active streams
406    pub active_streams: u64,
407    /// Total bytes streamed
408    pub total_bytes: u64,
409    /// Total chunks sent
410    pub total_chunks: u64,
411    /// Average stream duration (ms)
412    pub avg_duration_ms: f64,
413    /// Streams by mode
414    pub streams_by_mode: HashMap<String, u64>,
415    /// Error count
416    pub error_count: u64,
417}
418
419impl IncrementalDeliveryManager {
420    /// Create a new incremental delivery manager
421    pub fn new(config: StreamingConfig) -> Self {
422        Self {
423            config,
424            streams: Arc::new(RwLock::new(HashMap::new())),
425            stats: Arc::new(RwLock::new(StreamingStatistics::default())),
426        }
427    }
428
429    /// Create a new stream
430    pub async fn create_stream(
431        &self,
432        mode: StreamingMode,
433        client_id: Option<&str>,
434    ) -> (ResponseStreamer, mpsc::Receiver<ResponseChunk>) {
435        let (streamer, receiver) = ResponseStreamer::new(self.config.clone());
436        let stream_id = streamer.id().to_string();
437
438        // Register stream
439        {
440            let mut streams = self.streams.write().await;
441            streams.insert(
442                stream_id.clone(),
443                StreamInfo {
444                    id: stream_id.clone(),
445                    mode,
446                    started_at: Instant::now(),
447                    bytes_sent: 0,
448                    chunks_sent: 0,
449                    client_id: client_id.map(|s| s.to_string()),
450                },
451            );
452        }
453
454        // Update stats
455        {
456            let mut stats = self.stats.write().await;
457            stats.total_streams += 1;
458            stats.active_streams += 1;
459            *stats
460                .streams_by_mode
461                .entry(format!("{:?}", mode))
462                .or_insert(0) += 1;
463        }
464
465        (streamer, receiver)
466    }
467
468    /// Close a stream
469    pub async fn close_stream(&self, stream_id: &str) {
470        let stream_info = {
471            let mut streams = self.streams.write().await;
472            streams.remove(stream_id)
473        };
474
475        if let Some(info) = stream_info {
476            let mut stats = self.stats.write().await;
477            stats.active_streams = stats.active_streams.saturating_sub(1);
478            stats.total_bytes += info.bytes_sent;
479            stats.total_chunks += info.chunks_sent;
480
481            // Update average duration
482            let duration = info.started_at.elapsed().as_millis() as f64;
483            let n = stats.total_streams as f64;
484            stats.avg_duration_ms = (stats.avg_duration_ms * (n - 1.0) + duration) / n;
485        }
486    }
487
488    /// Get active streams
489    pub async fn get_active_streams(&self) -> Vec<StreamProgress> {
490        let streams = self.streams.read().await;
491        streams
492            .values()
493            .map(|info| StreamProgress {
494                stream_id: info.id.clone(),
495                chunks_sent: info.chunks_sent,
496                bytes_sent: info.bytes_sent,
497                total_bytes: None,
498                throughput_bps: 0.0,
499                elapsed_ms: info.started_at.elapsed().as_millis() as u64,
500                pending_chunks: 0,
501                is_complete: false,
502                has_errors: false,
503            })
504            .collect()
505    }
506
507    /// Get statistics
508    pub async fn get_statistics(&self) -> StreamingStatistics {
509        self.stats.read().await.clone()
510    }
511
512    /// Process incremental response with @defer
513    pub async fn process_deferred(
514        &self,
515        streamer: &ResponseStreamer,
516        initial_data: serde_json::Value,
517        deferred_fields: Vec<DeferredField>,
518    ) -> Result<(), StreamError> {
519        let total_deferred = deferred_fields.len();
520
521        // Send initial response
522        streamer
523            .send_chunk(initial_data, None, None, false, total_deferred > 0)
524            .await?;
525
526        // Process deferred fields
527        for (i, deferred) in deferred_fields.into_iter().enumerate() {
528            let is_last = i == total_deferred - 1;
529
530            // Simulate async resolution
531            let data = (deferred.resolver)().await;
532
533            streamer
534                .send_chunk(data, Some(deferred.path), deferred.label, is_last, !is_last)
535                .await?;
536        }
537
538        Ok(())
539    }
540
541    /// Process streaming response with @stream
542    pub async fn process_stream<I, T>(
543        &self,
544        streamer: &ResponseStreamer,
545        path: Vec<PathSegment>,
546        label: Option<String>,
547        items: I,
548    ) -> Result<(), StreamError>
549    where
550        I: IntoIterator<Item = T>,
551        T: Serialize,
552    {
553        let mut items_iter = items.into_iter().peekable();
554        let mut index = 0;
555
556        while let Some(item) = items_iter.next() {
557            let has_next = items_iter.peek().is_some();
558            let mut item_path = path.clone();
559            item_path.push(PathSegment::Index(index));
560
561            let data = serde_json::to_value(&item).map_err(|e| StreamError {
562                message: format!("Serialization error: {}", e),
563                path: Some(item_path.clone()),
564                extensions: None,
565            })?;
566
567            streamer
568                .send_chunk(data, Some(item_path), label.clone(), !has_next, has_next)
569                .await?;
570
571            index += 1;
572        }
573
574        Ok(())
575    }
576}
577
578/// A deferred field to be resolved later
579pub struct DeferredField {
580    /// Path to the field
581    pub path: Vec<PathSegment>,
582    /// Optional label from @defer directive
583    pub label: Option<String>,
584    /// Resolver function
585    pub resolver: Box<
586        dyn Fn() -> std::pin::Pin<Box<dyn std::future::Future<Output = serde_json::Value> + Send>>
587            + Send
588            + Sync,
589    >,
590}
591
592/// Multipart response formatter
593pub struct MultipartFormatter {
594    /// Boundary string
595    boundary: String,
596}
597
598impl MultipartFormatter {
599    /// Create a new multipart formatter
600    pub fn new() -> Self {
601        Self {
602            boundary: format!("-graphql-{}", uuid::Uuid::new_v4()),
603        }
604    }
605
606    /// Get the content type header value
607    pub fn content_type(&self) -> String {
608        format!("multipart/mixed; boundary=\"{}\"", self.boundary)
609    }
610
611    /// Format a chunk as multipart
612    pub fn format_chunk(&self, chunk: &ResponseChunk) -> Vec<u8> {
613        let mut output = Vec::new();
614
615        // Boundary
616        output.extend_from_slice(format!("--{}\r\n", self.boundary).as_bytes());
617
618        // Headers
619        output.extend_from_slice(b"Content-Type: application/json; charset=utf-8\r\n");
620
621        if chunk.is_final {
622            output.extend_from_slice(b"X-GraphQL-Final: true\r\n");
623        }
624
625        if let Some(ref label) = chunk.label {
626            output.extend_from_slice(format!("X-GraphQL-Label: {}\r\n", label).as_bytes());
627        }
628
629        output.extend_from_slice(b"\r\n");
630
631        // Body
632        let body = serde_json::json!({
633            "data": chunk.data,
634            "path": chunk.path,
635            "hasNext": chunk.has_next,
636            "errors": if chunk.has_errors { Some(&chunk.errors) } else { None },
637        });
638
639        output.extend_from_slice(
640            serde_json::to_string(&body)
641                .expect("serializing JSON body should succeed")
642                .as_bytes(),
643        );
644        output.extend_from_slice(b"\r\n");
645
646        output
647    }
648
649    /// Format the final boundary
650    pub fn format_final(&self) -> Vec<u8> {
651        format!("--{}--\r\n", self.boundary).into_bytes()
652    }
653}
654
655impl Default for MultipartFormatter {
656    fn default() -> Self {
657        Self::new()
658    }
659}
660
661/// Compression utilities for streamed responses
662pub mod compression {
663    use super::*;
664
665    /// Compress data using the specified algorithm
666    pub fn compress(data: &[u8], compression_type: CompressionType) -> Result<Vec<u8>, String> {
667        match compression_type {
668            CompressionType::None => Ok(data.to_vec()),
669            CompressionType::Gzip => compress_gzip(data),
670            CompressionType::Deflate => compress_deflate(data),
671            CompressionType::Brotli => compress_brotli(data),
672        }
673    }
674
675    fn compress_gzip(data: &[u8]) -> Result<Vec<u8>, String> {
676        // Gzip (RFC 1952) compression via Pure-Rust oxiarc-deflate.
677        // Level 6 is the balanced default.
678        oxiarc_deflate::gzip_compress(data, 6).map_err(|e| format!("Gzip compression error: {}", e))
679    }
680
681    fn compress_deflate(data: &[u8]) -> Result<Vec<u8>, String> {
682        // Raw DEFLATE (RFC 1951) compression via Pure-Rust oxiarc-deflate.
683        // Level 6 is the balanced default. This is the bare deflate stream
684        // (no zlib/gzip framing), matching the HTTP "deflate" content-encoding
685        // produced by a raw DeflateEncoder.
686        oxiarc_deflate::deflate(data, 6).map_err(|e| format!("Deflate compression error: {}", e))
687    }
688
689    fn compress_brotli(data: &[u8]) -> Result<Vec<u8>, String> {
690        // Brotli compression via Pure-Rust oxiarc-brotli. Quality 6 mirrors
691        // the "balanced default" used by the gzip/deflate paths above
692        // (oxiarc-brotli quality ranges 0-11, where 11 is maximum
693        // compression / slowest).
694        const BROTLI_QUALITY: u32 = 6;
695        oxiarc_brotli::compress(data, BROTLI_QUALITY)
696            .map_err(|e| format!("Brotli compression error: {}", e))
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    #[tokio::test]
705    async fn test_streamer_creation() {
706        let config = StreamingConfig::default();
707        let (streamer, _receiver) = ResponseStreamer::new(config);
708        assert!(!streamer.id().is_empty());
709    }
710
711    #[tokio::test]
712    async fn test_send_chunk() {
713        let config = StreamingConfig::default();
714        let (streamer, mut receiver) = ResponseStreamer::new(config);
715
716        let data = serde_json::json!({"field": "value"});
717        streamer
718            .send_chunk(data.clone(), None, None, false, true)
719            .await
720            .expect("should succeed");
721
722        let chunk = receiver.recv().await.expect("should succeed");
723        assert_eq!(chunk.data, data);
724        assert!(!chunk.is_final);
725        assert!(chunk.has_next);
726    }
727
728    #[tokio::test]
729    async fn test_send_error() {
730        let config = StreamingConfig::default();
731        let (streamer, mut receiver) = ResponseStreamer::new(config);
732
733        let error = StreamError {
734            message: "Test error".to_string(),
735            path: None,
736            extensions: None,
737        };
738        streamer.send_error(error, None).await;
739
740        let chunk = receiver.recv().await.expect("should succeed");
741        assert!(chunk.has_errors);
742        assert!(!chunk.errors.is_empty());
743    }
744
745    #[tokio::test]
746    async fn test_complete_stream() {
747        let config = StreamingConfig::default();
748        let (streamer, mut receiver) = ResponseStreamer::new(config);
749
750        streamer.complete().await;
751
752        let chunk = receiver.recv().await.expect("should succeed");
753        assert!(chunk.is_final);
754        assert!(!chunk.has_next);
755    }
756
757    #[tokio::test]
758    async fn test_progress_tracking() {
759        let config = StreamingConfig {
760            track_progress: true,
761            ..Default::default()
762        };
763        let (streamer, _receiver) = ResponseStreamer::new(config);
764
765        // Send some data
766        let data = serde_json::json!({"test": "data"});
767        let _ = streamer.send_chunk(data, None, None, false, true).await;
768
769        let progress = streamer.get_progress().await;
770        assert_eq!(progress.chunks_sent, 1);
771        assert!(progress.bytes_sent > 0);
772    }
773
774    #[tokio::test]
775    async fn test_manager_create_stream() {
776        let manager = IncrementalDeliveryManager::new(StreamingConfig::default());
777
778        let (_streamer, _receiver) = manager.create_stream(StreamingMode::Chunked, None).await;
779
780        let stats = manager.get_statistics().await;
781        assert_eq!(stats.total_streams, 1);
782        assert_eq!(stats.active_streams, 1);
783    }
784
785    #[tokio::test]
786    async fn test_manager_close_stream() {
787        let manager = IncrementalDeliveryManager::new(StreamingConfig::default());
788
789        let (streamer, _receiver) = manager.create_stream(StreamingMode::Chunked, None).await;
790        let stream_id = streamer.id().to_string();
791
792        manager.close_stream(&stream_id).await;
793
794        let stats = manager.get_statistics().await;
795        assert_eq!(stats.active_streams, 0);
796    }
797
798    #[tokio::test]
799    async fn test_multipart_formatter() {
800        let formatter = MultipartFormatter::new();
801
802        assert!(formatter.content_type().contains("multipart/mixed"));
803
804        let chunk = ResponseChunk {
805            id: 0,
806            path: None,
807            data: serde_json::json!({"test": true}),
808            has_errors: false,
809            errors: Vec::new(),
810            is_final: false,
811            has_next: true,
812            label: None,
813        };
814
815        let formatted = formatter.format_chunk(&chunk);
816        assert!(!formatted.is_empty());
817    }
818
819    #[tokio::test]
820    async fn test_path_segments() {
821        let path = vec![
822            PathSegment::Field("users".to_string()),
823            PathSegment::Index(0),
824            PathSegment::Field("name".to_string()),
825        ];
826
827        let json = serde_json::to_string(&path).expect("should succeed");
828        assert!(json.contains("users"));
829    }
830
831    #[tokio::test]
832    async fn test_stream_items() {
833        let manager = IncrementalDeliveryManager::new(StreamingConfig::default());
834        let (streamer, mut receiver) = manager.create_stream(StreamingMode::Streamed, None).await;
835
836        let items = vec!["item1", "item2", "item3"];
837        let path = vec![PathSegment::Field("items".to_string())];
838
839        // Spawn stream processing
840        let streamer_clone = Arc::new(streamer);
841        tokio::spawn(async move {
842            let _ = manager
843                .process_stream(&streamer_clone, path, Some("items".to_string()), items)
844                .await;
845        });
846
847        // Receive chunks
848        let mut received = Vec::new();
849        while let Some(chunk) = receiver.recv().await {
850            received.push(chunk);
851            if received.len() >= 3 {
852                break;
853            }
854        }
855
856        assert_eq!(received.len(), 3);
857    }
858
859    #[tokio::test]
860    async fn test_heartbeat() {
861        let config = StreamingConfig::default();
862        let (streamer, mut receiver) = ResponseStreamer::new(config);
863
864        streamer.send_heartbeat().await.expect("should succeed");
865
866        let chunk = receiver.recv().await.expect("should succeed");
867        assert_eq!(chunk.label, Some("__heartbeat".to_string()));
868    }
869
870    #[tokio::test]
871    async fn test_compression() {
872        let data = b"Hello, World!";
873
874        let compressed =
875            compression::compress(data, CompressionType::Gzip).expect("should succeed");
876        assert!(!compressed.is_empty());
877
878        let deflated =
879            compression::compress(data, CompressionType::Deflate).expect("should succeed");
880        assert!(!deflated.is_empty());
881
882        let none = compression::compress(data, CompressionType::None).expect("should succeed");
883        assert_eq!(none, data);
884    }
885
886    #[test]
887    fn test_gzip_compression_roundtrip() {
888        // Verify the gzip (RFC 1952) Content-Encoding path round-trips: the
889        // bytes produced by compress(Gzip) must decode with a gzip decoder.
890        let data: Vec<u8> = b"oxirs-gql streamed response gzip body "
891            .iter()
892            .cycle()
893            .take(4096)
894            .copied()
895            .collect();
896
897        let compressed =
898            compression::compress(&data, CompressionType::Gzip).expect("gzip compression failed");
899        // Gzip magic header (RFC 1952): 0x1f 0x8b.
900        assert_eq!(&compressed[..2], &[0x1f, 0x8b]);
901        assert!(compressed.len() < data.len());
902
903        let decompressed =
904            oxiarc_deflate::gzip_decompress(&compressed).expect("gzip decompression failed");
905        assert_eq!(decompressed, data);
906    }
907
908    #[test]
909    fn test_deflate_compression_roundtrip() {
910        // Verify the raw DEFLATE (RFC 1951) Content-Encoding path round-trips:
911        // compress(Deflate) emits a bare deflate stream (no zlib/gzip framing),
912        // so it must decode with the raw inflate function.
913        let data: Vec<u8> = b"oxirs-gql streamed response deflate body "
914            .iter()
915            .cycle()
916            .take(4096)
917            .copied()
918            .collect();
919
920        let compressed = compression::compress(&data, CompressionType::Deflate)
921            .expect("deflate compression failed");
922        assert!(compressed.len() < data.len());
923
924        let decompressed = oxiarc_deflate::inflate(&compressed).expect("inflate failed");
925        assert_eq!(decompressed, data);
926    }
927
928    #[test]
929    fn test_brotli_compression_roundtrip() {
930        // Regression test: CompressionType::Brotli used to unconditionally
931        // return Err("Brotli compression not implemented"). It must now
932        // actually compress via the Pure-Rust oxiarc-brotli backend and
933        // round-trip through oxiarc_brotli::decompress.
934        let data: Vec<u8> = b"oxirs-gql streamed response brotli body "
935            .iter()
936            .cycle()
937            .take(4096)
938            .copied()
939            .collect();
940
941        let compressed = compression::compress(&data, CompressionType::Brotli)
942            .expect("brotli compression should now be implemented");
943        assert!(!compressed.is_empty());
944        assert!(compressed.len() < data.len());
945
946        let decompressed =
947            oxiarc_brotli::decompress(&compressed).expect("brotli decompression failed");
948        assert_eq!(decompressed, data);
949    }
950}