1use std::collections::VecDeque;
9
10#[derive(Debug, Clone)]
18pub struct BlockChunk {
19 pub cids: Vec<String>,
21 pub data: Vec<Vec<u8>>,
23 pub chunk_index: u64,
25 pub is_final: bool,
27}
28
29impl BlockChunk {
30 #[inline]
32 pub fn total_bytes(&self) -> usize {
33 self.data.iter().map(|d| d.len()).sum()
34 }
35
36 #[inline]
38 pub fn len(&self) -> usize {
39 self.cids.len()
40 }
41
42 #[inline]
44 pub fn is_empty(&self) -> bool {
45 self.cids.is_empty()
46 }
47}
48
49#[derive(Debug, Clone)]
53pub struct StreamConfig {
54 pub chunk_size: usize,
56 pub max_buffer_chunks: usize,
59 pub include_data: bool,
62}
63
64impl Default for StreamConfig {
65 fn default() -> Self {
66 Self {
67 chunk_size: 64,
68 max_buffer_chunks: 4,
69 include_data: true,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum BlockStreamState {
79 Ready,
81 Draining,
83 Paused,
85 Exhausted,
87 Error(String),
89}
90
91#[derive(Debug, Clone, Default)]
95pub struct StreamStats {
96 pub chunks_produced: u64,
98 pub chunks_drained: u64,
100 pub bytes_streamed: u64,
103 pub backpressure_events: u64,
105}
106
107#[derive(Debug)]
140pub struct BlockStreamIterator {
141 pub all_blocks: Vec<(String, Vec<u8>)>,
143 pub position: usize,
145 pub config: StreamConfig,
147 pub buffer: VecDeque<BlockChunk>,
149 pub state: BlockStreamState,
151 pub stats: StreamStats,
153}
154
155impl BlockStreamIterator {
156 pub fn new(blocks: Vec<(String, Vec<u8>)>, config: StreamConfig) -> Self {
158 let state = if blocks.is_empty() {
159 BlockStreamState::Exhausted
160 } else {
161 BlockStreamState::Ready
162 };
163 Self {
164 all_blocks: blocks,
165 position: 0,
166 config,
167 buffer: VecDeque::new(),
168 state,
169 stats: StreamStats::default(),
170 }
171 }
172
173 pub fn next_chunk(&mut self) -> Option<BlockChunk> {
184 if matches!(self.state, BlockStreamState::Exhausted) {
186 return None;
187 }
188 if self.buffer.len() >= self.config.max_buffer_chunks {
190 self.stats.backpressure_events += 1;
191 self.state = BlockStreamState::Paused;
192 return None;
193 }
194 if self.position >= self.all_blocks.len() {
196 if self.buffer.is_empty() {
198 self.state = BlockStreamState::Exhausted;
199 } else {
200 self.state = BlockStreamState::Draining;
201 }
202 return None;
203 }
204
205 let start = self.position;
207 let end = (start + self.config.chunk_size).min(self.all_blocks.len());
208 let is_final = end >= self.all_blocks.len();
209
210 let mut cids = Vec::with_capacity(end - start);
211 let mut data = Vec::with_capacity(end - start);
212
213 for (cid, block_data) in &self.all_blocks[start..end] {
214 cids.push(cid.clone());
215 if self.config.include_data {
216 data.push(block_data.clone());
217 } else {
218 data.push(Vec::new());
219 }
220 }
221
222 self.position = end;
223
224 let chunk_index = self.stats.chunks_produced;
225 let chunk = BlockChunk {
226 cids,
227 data,
228 chunk_index,
229 is_final,
230 };
231
232 self.stats.chunks_produced += 1;
233 self.buffer.push_back(chunk.clone());
234
235 if is_final {
237 self.state = BlockStreamState::Draining;
238 } else if self.buffer.len() >= self.config.max_buffer_chunks {
239 self.state = BlockStreamState::Paused;
240 } else {
241 self.state = BlockStreamState::Ready;
242 }
243
244 Some(chunk)
245 }
246
247 pub fn drain_one(&mut self) -> Option<BlockChunk> {
253 let chunk = self.buffer.pop_front()?;
254 self.stats.chunks_drained += 1;
255 self.stats.bytes_streamed += chunk.total_bytes() as u64;
256
257 let buffer_was_full = matches!(self.state, BlockStreamState::Paused);
259 if buffer_was_full && self.buffer.len() < self.config.max_buffer_chunks {
260 if self.position < self.all_blocks.len() {
261 self.state = BlockStreamState::Ready;
262 } else if self.buffer.is_empty() {
263 self.state = BlockStreamState::Exhausted;
264 } else {
265 self.state = BlockStreamState::Draining;
266 }
267 } else if self.buffer.is_empty() && self.position >= self.all_blocks.len() {
268 self.state = BlockStreamState::Exhausted;
269 }
270
271 Some(chunk)
272 }
273
274 pub fn fill_buffer(&mut self) {
279 while self.buffer.len() < self.config.max_buffer_chunks
280 && self.position < self.all_blocks.len()
281 && !matches!(
282 self.state,
283 BlockStreamState::Exhausted | BlockStreamState::Error(_)
284 )
285 {
286 if self.next_chunk().is_none() {
287 break;
288 }
289 }
290 }
291
292 #[inline]
295 pub fn is_exhausted(&self) -> bool {
296 matches!(self.state, BlockStreamState::Exhausted)
297 }
298
299 #[inline]
301 pub fn remaining_blocks(&self) -> usize {
302 self.all_blocks.len().saturating_sub(self.position)
303 }
304
305 #[inline]
307 pub fn buffered_chunks(&self) -> usize {
308 self.buffer.len()
309 }
310}
311
312#[cfg(test)]
315mod tests {
316 use super::*;
317
318 fn make_blocks(n: usize) -> Vec<(String, Vec<u8>)> {
320 (0..n)
321 .map(|i| (format!("cid-{i:04}"), vec![i as u8; 32]))
322 .collect()
323 }
324
325 #[test]
328 fn test_chunk_size_exact_multiple() {
329 let blocks = make_blocks(64);
330 let config = StreamConfig {
331 chunk_size: 16,
332 max_buffer_chunks: 10,
333 include_data: true,
334 };
335 let mut iter = BlockStreamIterator::new(blocks, config);
336
337 let chunk = iter.next_chunk().expect("should produce chunk");
338 assert_eq!(chunk.len(), 16);
339 assert_eq!(chunk.cids.len(), chunk.data.len());
340 }
341
342 #[test]
343 fn test_chunk_size_remainder() {
344 let blocks = make_blocks(70);
346 let config = StreamConfig {
347 chunk_size: 32,
348 max_buffer_chunks: 10,
349 include_data: true,
350 };
351 let mut iter = BlockStreamIterator::new(blocks, config);
352
353 let c0 = iter.next_chunk().expect("chunk 0");
354 assert_eq!(c0.len(), 32);
355 assert!(!c0.is_final);
356
357 let c1 = iter.next_chunk().expect("chunk 1");
358 assert_eq!(c1.len(), 32);
359 assert!(!c1.is_final);
360
361 let c2 = iter.next_chunk().expect("chunk 2");
362 assert_eq!(c2.len(), 6);
363 assert!(c2.is_final);
364 }
365
366 #[test]
369 fn test_is_final_flag() {
370 let blocks = make_blocks(10);
371 let config = StreamConfig {
372 chunk_size: 5,
373 max_buffer_chunks: 10,
374 include_data: true,
375 };
376 let mut iter = BlockStreamIterator::new(blocks, config);
377
378 let c0 = iter.next_chunk().expect("chunk 0");
379 assert!(!c0.is_final);
380
381 let c1 = iter.next_chunk().expect("chunk 1");
382 assert!(c1.is_final);
383 }
384
385 #[test]
388 fn test_is_exhausted_after_drain() {
389 let blocks = make_blocks(5);
390 let config = StreamConfig {
391 chunk_size: 5,
392 max_buffer_chunks: 4,
393 include_data: true,
394 };
395 let mut iter = BlockStreamIterator::new(blocks, config);
396
397 iter.next_chunk().expect("chunk");
398 assert!(!iter.is_exhausted());
399
400 iter.drain_one().expect("drain");
401 assert!(iter.is_exhausted());
402 }
403
404 #[test]
407 fn test_backpressure_returns_none() {
408 let blocks = make_blocks(10);
410 let config = StreamConfig {
411 chunk_size: 1,
412 max_buffer_chunks: 2,
413 include_data: true,
414 };
415 let mut iter = BlockStreamIterator::new(blocks, config);
416
417 iter.next_chunk().expect("chunk 0");
419 iter.next_chunk().expect("chunk 1");
420 assert_eq!(iter.buffered_chunks(), 2);
421
422 let result = iter.next_chunk();
424 assert!(result.is_none());
425 assert_eq!(iter.stats.backpressure_events, 1);
426 assert!(matches!(iter.state, BlockStreamState::Paused));
427 }
428
429 #[test]
432 fn test_drain_releases_backpressure() {
433 let blocks = make_blocks(10);
434 let config = StreamConfig {
435 chunk_size: 1,
436 max_buffer_chunks: 2,
437 include_data: true,
438 };
439 let mut iter = BlockStreamIterator::new(blocks, config);
440
441 iter.next_chunk();
442 iter.next_chunk();
443 assert!(iter.next_chunk().is_none());
445
446 iter.drain_one().expect("should drain");
448 assert!(matches!(iter.state, BlockStreamState::Ready));
449
450 let chunk = iter.next_chunk().expect("should produce after drain");
452 assert_eq!(chunk.chunk_index, 2);
453 }
454
455 #[test]
458 fn test_include_data_false() {
459 let blocks = make_blocks(8);
460 let config = StreamConfig {
461 chunk_size: 8,
462 max_buffer_chunks: 4,
463 include_data: false,
464 };
465 let mut iter = BlockStreamIterator::new(blocks, config);
466 let chunk = iter.next_chunk().expect("chunk");
467
468 assert_eq!(chunk.cids.len(), 8);
469 for d in &chunk.data {
470 assert!(d.is_empty());
471 }
472 assert_eq!(chunk.total_bytes(), 0);
473 }
474
475 #[test]
478 fn test_remaining_blocks_decreases() {
479 let blocks = make_blocks(20);
480 let config = StreamConfig {
481 chunk_size: 10,
482 max_buffer_chunks: 4,
483 include_data: true,
484 };
485 let mut iter = BlockStreamIterator::new(blocks, config);
486
487 assert_eq!(iter.remaining_blocks(), 20);
488 iter.next_chunk();
489 assert_eq!(iter.remaining_blocks(), 10);
490 iter.next_chunk();
491 assert_eq!(iter.remaining_blocks(), 0);
492 }
493
494 #[test]
497 fn test_empty_input_exhausted() {
498 let mut iter = BlockStreamIterator::new(vec![], StreamConfig::default());
499 assert!(iter.is_exhausted());
500 assert!(iter.next_chunk().is_none());
501 assert!(iter.drain_one().is_none());
502 assert_eq!(iter.remaining_blocks(), 0);
503 assert_eq!(iter.buffered_chunks(), 0);
504 }
505
506 #[test]
509 fn test_stats_accumulate() {
510 let blocks = make_blocks(6);
511 let config = StreamConfig {
512 chunk_size: 3,
513 max_buffer_chunks: 4,
514 include_data: true,
515 };
516 let mut iter = BlockStreamIterator::new(blocks, config);
517
518 iter.next_chunk().expect("c0");
519 iter.next_chunk().expect("c1");
520
521 assert_eq!(iter.stats.chunks_produced, 2);
522 assert_eq!(iter.stats.chunks_drained, 0);
523
524 iter.drain_one().expect("drain c0");
525 assert_eq!(iter.stats.bytes_streamed, 96);
527 assert_eq!(iter.stats.chunks_drained, 1);
528
529 iter.drain_one().expect("drain c1");
530 assert_eq!(iter.stats.bytes_streamed, 192);
531 assert_eq!(iter.stats.chunks_drained, 2);
532 }
533
534 #[test]
537 fn test_chunk_index_monotonic() {
538 let blocks = make_blocks(30);
539 let config = StreamConfig {
540 chunk_size: 10,
541 max_buffer_chunks: 4,
542 include_data: true,
543 };
544 let mut iter = BlockStreamIterator::new(blocks, config);
545
546 for expected in 0u64..3 {
547 let chunk = iter.next_chunk().expect("chunk");
548 assert_eq!(chunk.chunk_index, expected);
549 }
550 }
551
552 #[test]
555 fn test_fill_buffer() {
556 let blocks = make_blocks(100);
557 let config = StreamConfig {
558 chunk_size: 10,
559 max_buffer_chunks: 4,
560 include_data: true,
561 };
562 let mut iter = BlockStreamIterator::new(blocks, config);
563
564 iter.fill_buffer();
565 assert_eq!(iter.buffered_chunks(), 4);
566 assert_eq!(iter.remaining_blocks(), 60);
567 }
568
569 #[test]
572 fn test_cid_order_preserved() {
573 let blocks = make_blocks(5);
574 let config = StreamConfig {
575 chunk_size: 5,
576 max_buffer_chunks: 4,
577 include_data: true,
578 };
579 let mut iter = BlockStreamIterator::new(blocks.clone(), config);
580 let chunk = iter.next_chunk().expect("chunk");
581
582 for (i, cid) in chunk.cids.iter().enumerate() {
583 assert_eq!(*cid, blocks[i].0);
584 }
585 }
586
587 #[test]
590 fn test_single_block() {
591 let blocks = vec![("cid-single".to_string(), vec![0xAB; 64])];
592 let config = StreamConfig::default();
593 let mut iter = BlockStreamIterator::new(blocks, config);
594
595 let chunk = iter.next_chunk().expect("chunk");
596 assert!(chunk.is_final);
597 assert_eq!(chunk.len(), 1);
598 assert_eq!(chunk.total_bytes(), 64);
599
600 assert!(iter.next_chunk().is_none());
602
603 assert!(!iter.is_exhausted());
605 iter.drain_one().expect("drain");
606 assert!(iter.is_exhausted());
607 }
608
609 #[test]
612 fn test_full_drain_loop() {
613 let n = 50usize;
614 let blocks = make_blocks(n);
615 let config = StreamConfig {
616 chunk_size: 7,
617 max_buffer_chunks: 3,
618 include_data: true,
619 };
620 let mut iter = BlockStreamIterator::new(blocks, config);
621
622 let mut total_blocks_seen = 0usize;
623 while !iter.is_exhausted() {
624 if let Some(chunk) = iter.next_chunk() {
625 total_blocks_seen += chunk.len();
626 } else {
627 if let Some(chunk) = iter.drain_one() {
629 let _ = chunk;
630 } else {
631 break;
633 }
634 }
635 }
636 while iter.drain_one().is_some() {}
638
639 assert!(iter.is_exhausted());
640 assert_eq!(iter.stats.chunks_produced, iter.stats.chunks_drained);
641 assert_eq!(total_blocks_seen, n);
643 }
644
645 #[test]
648 fn test_backpressure_events_counter() {
649 let blocks = make_blocks(20);
650 let config = StreamConfig {
651 chunk_size: 1,
652 max_buffer_chunks: 2,
653 include_data: false,
654 };
655 let mut iter = BlockStreamIterator::new(blocks, config);
656
657 iter.next_chunk();
658 iter.next_chunk();
659 iter.next_chunk(); iter.next_chunk(); iter.next_chunk(); assert_eq!(iter.stats.backpressure_events, 3);
665 }
666}