1use crate::{Bytes, JsonlLine, JsonlLineError, Stream, StreamExt};
13use alloc::{boxed::Box, vec::Vec};
14use core::{
15 iter::FusedIterator,
16 num::{NonZeroUsize, TryFromIntError},
17 ops::Range,
18 pin::Pin,
19 time::Duration,
20};
21
22#[derive(Clone, Debug)]
47pub struct JsonlBatch {
48 storage: BatchStorage,
49 byte_len: usize,
50}
51
52#[derive(Clone, Debug)]
53enum BatchStorage {
54 Contiguous { bytes: Bytes, line_ends: Vec<usize> },
58 Lines(Vec<JsonlLine>),
59}
60
61impl Default for JsonlBatch {
62 fn default() -> Self {
63 Self {
64 storage: BatchStorage::Lines(Vec::new()),
65 byte_len: 0,
66 }
67 }
68}
69
70impl PartialEq for JsonlBatch {
71 fn eq(&self, other: &Self) -> bool {
72 self.byte_len == other.byte_len
73 && self.len() == other.len()
74 && self.lines().eq(other.lines())
75 }
76}
77impl Eq for JsonlBatch {}
78
79impl JsonlBatch {
80 pub fn new(lines: Vec<JsonlLine>) -> Self {
83 let byte_len = lines
84 .iter()
85 .try_fold(0usize, |total, line| total.checked_add(line.len()))
86 .expect("JSONL batch byte length exceeds usize");
87 Self {
88 storage: BatchStorage::Lines(lines),
89 byte_len,
90 }
91 }
92
93 pub fn from_bytes(bytes: Bytes) -> Self {
110 let mut line_ends: Vec<_> = memchr::memchr_iter(b'\n', &bytes)
111 .map(|index| index + 1)
112 .collect();
113 if !bytes.is_empty() && line_ends.last().copied() != Some(bytes.len()) {
114 line_ends.push(bytes.len());
115 }
116 Self::contiguous(bytes, line_ends)
117 }
118
119 fn contiguous(bytes: Bytes, line_ends: Vec<usize>) -> Self {
120 debug_assert_eq!(line_ends.last().copied().unwrap_or(0), bytes.len());
121 debug_assert!({
122 let mut start = 0;
123 line_ends.iter().all(|&end| {
124 let valid = JsonlLine::shared_slice(bytes.clone(), start..end).is_ok();
125 start = end;
126 valid
127 })
128 });
129 Self {
130 byte_len: bytes.len(),
131 storage: BatchStorage::Contiguous { bytes, line_ends },
132 }
133 }
134
135 pub fn len(&self) -> usize {
137 match &self.storage {
138 BatchStorage::Contiguous { line_ends, .. } => line_ends.len(),
139 BatchStorage::Lines(lines) => lines.len(),
140 }
141 }
142
143 pub fn is_empty(&self) -> bool {
145 self.len() == 0
146 }
147
148 pub fn byte_len(&self) -> usize {
151 self.byte_len
152 }
153
154 pub fn lines(&self) -> impl ExactSizeIterator<Item = &[u8]> + DoubleEndedIterator {
157 BatchLines {
158 batch: self,
159 front: 0,
160 back: self.len(),
161 }
162 }
163
164 pub fn into_lines(self) -> Vec<JsonlLine> {
168 match self.storage {
169 BatchStorage::Lines(lines) => lines,
170 BatchStorage::Contiguous { bytes, line_ends } => {
171 let mut start = 0;
172 line_ends
173 .into_iter()
174 .map(|end| {
175 let line = JsonlLine::framed(bytes.slice(start..end));
176 start = end;
177 line
178 })
179 .collect()
180 },
181 }
182 }
183
184 fn line_bytes(&self, index: usize) -> &[u8] {
185 match &self.storage {
186 BatchStorage::Lines(lines) => lines[index].as_bytes(),
187 BatchStorage::Contiguous { bytes, line_ends } => {
188 let start = if index == 0 { 0 } else { line_ends[index - 1] };
189 &bytes[start..line_ends[index]]
190 },
191 }
192 }
193
194 pub fn as_contiguous_bytes(&self) -> Option<&[u8]> {
200 if self.is_empty() {
201 return Some(&[]);
202 }
203 match &self.storage {
204 BatchStorage::Contiguous { bytes, .. }
205 if self.lines().all(|line| line.ends_with(b"\n")) =>
206 {
207 Some(bytes)
208 },
209 BatchStorage::Lines(lines) if lines.len() == 1 && lines[0].is_terminated() => {
210 Some(lines[0].as_bytes())
211 },
212 _ => None,
213 }
214 }
215
216 pub fn into_compact(self) -> Self {
221 let mut bytes = Vec::with_capacity(self.byte_len);
222 let mut line_ends = Vec::with_capacity(self.len());
223 for line in self.lines() {
224 bytes.extend_from_slice(line);
225 line_ends.push(bytes.len());
226 }
227 Self::contiguous(Bytes::from(bytes), line_ends)
228 }
229
230 #[cfg(feature = "std")]
233 pub fn wire_slices(&self, maximum: usize) -> Option<Vec<std::io::IoSlice<'_>>> {
234 use std::io::IoSlice;
235 let mut slices = Vec::new();
236 match &self.storage {
237 BatchStorage::Lines(lines) => {
238 for line in lines {
239 if !line.is_empty() {
240 slices.push(IoSlice::new(line.as_bytes()));
241 }
242 if !line.is_terminated() {
243 slices.push(IoSlice::new(b"\n"));
244 }
245 if slices.len() > maximum {
246 return None;
247 }
248 }
249 },
250 BatchStorage::Contiguous { bytes, line_ends } => {
251 let mut start = 0;
252 let mut run_start = 0;
253 for &end in line_ends {
254 if !bytes[start..end].ends_with(b"\n") {
255 if end != run_start {
256 slices.push(IoSlice::new(&bytes[run_start..end]));
257 }
258 slices.push(IoSlice::new(b"\n"));
259 run_start = end;
260 }
261 if slices.len() > maximum {
262 return None;
263 }
264 start = end;
265 }
266 if run_start < bytes.len() {
267 slices.push(IoSlice::new(&bytes[run_start..]));
268 }
269 if slices.len() > maximum {
270 return None;
271 }
272 },
273 }
274 Some(slices)
275 }
276}
277
278struct BatchLines<'a> {
279 batch: &'a JsonlBatch,
280 front: usize,
281 back: usize,
282}
283
284impl<'a> Iterator for BatchLines<'a> {
285 type Item = &'a [u8];
286 fn next(&mut self) -> Option<Self::Item> {
287 if self.front == self.back {
288 return None;
289 }
290 let index = self.front;
291 self.front += 1;
292 Some(self.batch.line_bytes(index))
293 }
294 fn size_hint(&self) -> (usize, Option<usize>) {
295 let remaining = self.back - self.front;
296 (remaining, Some(remaining))
297 }
298}
299impl DoubleEndedIterator for BatchLines<'_> {
300 fn next_back(&mut self) -> Option<Self::Item> {
301 if self.front == self.back {
302 return None;
303 }
304 self.back -= 1;
305 Some(self.batch.line_bytes(self.back))
306 }
307}
308impl ExactSizeIterator for BatchLines<'_> {}
309impl FusedIterator for BatchLines<'_> {}
310
311#[doc(hidden)]
314pub struct FramedLine(FrameStorage);
315
316enum FrameStorage {
317 Line(JsonlLine),
318 Buffer { bytes: Bytes, range: Range<usize> },
319}
320
321impl FramedLine {
322 pub(crate) fn new(bytes: Bytes, range: Range<usize>) -> Self {
323 debug_assert!(JsonlLine::shared_slice(bytes.clone(), range.clone()).is_ok());
324 Self(FrameStorage::Buffer { bytes, range })
325 }
326 pub fn as_bytes(&self) -> &[u8] {
327 match &self.0 {
328 FrameStorage::Line(line) => line.as_bytes(),
329 FrameStorage::Buffer { bytes, range } => &bytes[range.clone()],
330 }
331 }
332 #[cfg(feature = "tokio")]
333 fn len(&self) -> usize {
334 self.as_bytes().len()
335 }
336 pub fn into_line(self) -> JsonlLine {
337 match self.0 {
338 FrameStorage::Line(line) => line,
339 FrameStorage::Buffer { bytes, range } => JsonlLine::framed(bytes.slice(range)),
340 }
341 }
342}
343
344impl From<JsonlLine> for FramedLine {
345 fn from(line: JsonlLine) -> Self {
346 Self(FrameStorage::Line(line))
347 }
348}
349
350#[doc(hidden)]
351pub type FrameStream<E> = Pin<Box<dyn Stream<Item = Result<FramedLine, E>> + Send>>;
352
353#[cfg(feature = "tokio")]
354#[derive(Default)]
355enum BuilderStorage {
356 #[default]
357 Empty,
358 Buffer {
359 bytes: Bytes,
360 start: usize,
361 line_ends: Vec<usize>,
362 },
363 Lines(Vec<JsonlLine>),
364}
365
366#[cfg(feature = "tokio")]
367impl BuilderStorage {
368 fn finish(self, byte_len: usize) -> JsonlBatch {
369 match self {
370 Self::Empty => JsonlBatch::default(),
371 Self::Lines(lines) => JsonlBatch {
372 storage: BatchStorage::Lines(lines),
373 byte_len,
374 },
375 Self::Buffer {
376 bytes,
377 start,
378 mut line_ends,
379 } => {
380 let end = *line_ends.last().expect("buffered batch contains a line");
381 for offset in &mut line_ends {
382 *offset -= start;
383 }
384 JsonlBatch::contiguous(bytes.slice(start..end), line_ends)
385 },
386 }
387 }
388}
389
390#[cfg(feature = "tokio")]
391#[derive(Default)]
392struct BatchBuilder {
393 storage: BuilderStorage,
394 byte_len: usize,
395 len: usize,
396}
397
398#[cfg(feature = "tokio")]
399impl BatchBuilder {
400 fn len(&self) -> usize {
401 self.len
402 }
403 fn byte_len(&self) -> usize {
404 self.byte_len
405 }
406 fn push(&mut self, line: FramedLine) {
407 let previous_bytes = self.byte_len;
408 self.byte_len = self
409 .byte_len
410 .checked_add(line.len())
411 .expect("JSONL batch byte length exceeds usize");
412 self.len += 1;
413 match (&mut self.storage, line) {
414 (BuilderStorage::Empty, FramedLine(FrameStorage::Buffer { bytes, range })) => {
415 self.storage = BuilderStorage::Buffer {
416 bytes,
417 start: range.start,
418 line_ends: alloc::vec![range.end],
419 };
420 },
421 (
422 BuilderStorage::Buffer {
423 bytes, line_ends, ..
424 },
425 FramedLine(FrameStorage::Buffer { bytes: next, range }),
426 ) if bytes.as_ptr() == next.as_ptr()
427 && bytes.len() == next.len()
428 && line_ends.last().copied() == Some(range.start) =>
429 {
430 line_ends.push(range.end);
431 },
432 (BuilderStorage::Lines(lines), line) => lines.push(line.into_line()),
433 (_, line) => {
434 let storage = core::mem::take(&mut self.storage);
435 let mut lines = storage.finish(previous_bytes).into_lines();
436 lines.push(line.into_line());
437 self.storage = BuilderStorage::Lines(lines);
438 },
439 }
440 }
441 fn finish(self) -> JsonlBatch {
442 self.storage.finish(self.byte_len)
443 }
444}
445
446impl From<Vec<JsonlLine>> for JsonlBatch {
447 fn from(lines: Vec<JsonlLine>) -> Self {
448 Self::new(lines)
449 }
450}
451
452impl TryFrom<Vec<Vec<u8>>> for JsonlBatch {
453 type Error = JsonlLineError;
454 fn try_from(lines: Vec<Vec<u8>>) -> Result<Self, Self::Error> {
457 lines.into_iter().map(JsonlLine::owned).collect()
458 }
459}
460
461impl FromIterator<JsonlLine> for JsonlBatch {
462 fn from_iter<T: IntoIterator<Item = JsonlLine>>(iter: T) -> Self {
463 Self::new(iter.into_iter().collect())
464 }
465}
466
467#[derive(Clone, Copy, Debug, Eq, PartialEq)]
474pub struct BatchOptions {
475 pub max_lines: NonZeroUsize,
477 pub target_bytes: NonZeroUsize,
479 pub max_delay: Duration,
483}
484
485impl BatchOptions {
486 pub fn new(
496 max_lines: usize,
497 target_bytes: usize,
498 max_delay: Duration,
499 ) -> Result<Self, TryFromIntError> {
500 Ok(Self {
501 max_lines: NonZeroUsize::try_from(max_lines)?,
502 target_bytes: NonZeroUsize::try_from(target_bytes)?,
503 max_delay,
504 })
505 }
506}
507
508impl Default for BatchOptions {
509 fn default() -> Self {
510 Self::new(256, 256 * 1024, Duration::from_millis(10)).expect("nonzero batch thresholds")
511 }
512}
513
514pub type BatchStream<E> = Pin<Box<dyn Stream<Item = Result<JsonlBatch, E>> + Send>>;
516
517pub type LineStream<E> = Pin<Box<dyn Stream<Item = Result<JsonlLine, E>> + Send>>;
519
520#[cfg(feature = "tokio")]
536pub fn batch_lines<E: Send + 'static>(
537 source: impl Stream<Item = Result<JsonlLine, E>> + Send + 'static,
538 options: BatchOptions,
539) -> BatchStream<E> {
540 batch_frames(source.map(|line| line.map(FramedLine::from)), options)
541}
542
543#[doc(hidden)]
546#[cfg(feature = "tokio")]
547pub fn batch_frames<E: Send + 'static>(
548 source: impl Stream<Item = Result<FramedLine, E>> + Send + 'static,
549 options: BatchOptions,
550) -> BatchStream<E> {
551 Box::pin(async_stream::stream! {
552 let mut source = Box::pin(source);
553 let mut lookahead = None;
554 loop {
555 let first = match lookahead.take() {
556 Some(line) => Some(Ok(line)),
557 None => source.next().await,
558 };
559 let first = match first {
560 Some(Ok(line)) => line,
561 Some(Err(error)) => {
562 drop(source);
563 yield Err(error);
564 return;
565 },
566 None => return,
567 };
568 let mut batch = BatchBuilder::default();
569 batch.push(first);
570 let deadline = tokio::time::Instant::now().checked_add(options.max_delay);
571 let timer = async {
572 match deadline {
573 Some(deadline) => tokio::time::sleep_until(deadline).await,
574 None => core::future::pending::<()>().await,
575 }
576 };
577 tokio::pin!(timer);
578 let mut terminal = None;
579 while batch.len() < options.max_lines.get()
580 && batch.byte_len() < options.target_bytes.get()
581 && !deadline.is_some_and(|deadline| tokio::time::Instant::now() >= deadline)
582 {
583 let next = tokio::select! {
584 biased;
585 _ = &mut timer => break,
586 next = source.next() => next,
587 };
588 match next {
589 Some(Ok(line)) => {
590 if line.len() > options.target_bytes.get() - batch.byte_len() {
591 lookahead = Some(line);
592 break;
593 }
594 batch.push(line);
595 },
596 Some(Err(error)) => {
597 terminal = Some(Err(error));
598 break;
599 },
600 None => {
601 terminal = Some(Ok(()));
602 break;
603 },
604 }
605 }
606 if let Some(terminal) = terminal {
607 drop(source);
610 yield Ok(batch.finish());
611 if let Err(error) = terminal {
612 yield Err(error);
613 }
614 return;
615 }
616 yield Ok(batch.finish());
617 }
618 })
619}
620
621pub fn flatten_batches<E: Send + 'static>(
624 source: impl Stream<Item = Result<JsonlBatch, E>> + Send + 'static,
625) -> LineStream<E> {
626 Box::pin(async_stream::stream! {
627 let mut source = Box::pin(source);
628 while let Some(batch) = source.next().await {
629 match batch {
630 Ok(batch) => {
631 if batch.is_empty() {
632 futures_lite::future::yield_now().await;
633 }
634 for line in batch.into_lines() {
635 yield Ok(line);
636 }
637 },
638 Err(error) => {
639 drop(source);
640 yield Err(error);
641 return;
642 },
643 }
644 }
645 })
646}
647
648#[cfg(all(test, feature = "std"))]
649mod tests {
650 use super::*;
651 use alloc::vec;
652 use core::sync::atomic::{AtomicBool, Ordering};
653 use std::sync::Arc;
654
655 #[test]
656 fn contiguous_and_detached_batches_have_the_same_line_semantics() {
657 for (bytes, expected) in [
658 (b"".as_slice(), vec![]),
659 (b"\n", vec![b"\n".as_slice()]),
660 (b"\r\n{}", vec![b"\r\n".as_slice(), b"{}"]),
661 (b"a\nb\nc\n", vec![b"a\n".as_slice(), b"b\n", b"c\n"]),
662 (b"first\nlast", vec![b"first\n".as_slice(), b"last"]),
663 ] {
664 let batch = JsonlBatch::from_bytes(Bytes::copy_from_slice(bytes));
665 assert_eq!(batch.len(), expected.len());
666 assert_eq!(batch.byte_len(), bytes.len());
667 assert_eq!(batch.lines().collect::<Vec<_>>(), expected);
668 let detached = JsonlBatch::new(batch.clone().into_lines());
669 assert_eq!(batch, detached);
670 let mut actual = batch.lines();
671 let mut expected_iter = expected.iter().copied();
672 assert_eq!(actual.next(), expected_iter.next());
673 assert_eq!(actual.len(), expected_iter.len());
674 assert_eq!(actual.next_back(), expected_iter.next_back());
675 assert_eq!(actual.len(), expected_iter.len());
676 for expected_line in expected_iter {
677 assert_eq!(actual.next(), Some(expected_line));
678 }
679 assert_eq!(actual.size_hint(), (0, Some(0)));
680 assert_eq!(actual.next(), None);
681 assert_eq!(actual.next_back(), None);
682 }
683 }
684
685 #[test]
686 fn detaching_lines_does_not_infer_a_contiguous_allocation() {
687 let original = JsonlBatch::from_bytes(Bytes::from_static(b"a\nb\n"));
688 assert!(original.as_contiguous_bytes().is_some());
689 let detached = JsonlBatch::new(original.into_lines());
690 assert!(detached.as_contiguous_bytes().is_none());
691 let wire: Vec<_> = detached
692 .wire_slices(16)
693 .unwrap()
694 .iter()
695 .flat_map(|slice| slice.iter().copied())
696 .collect();
697 assert_eq!(wire, b"a\nb\n");
698 }
699
700 #[test]
701 fn raw_batch_construction_rejects_embedded_records() {
702 assert_eq!(
703 JsonlBatch::try_from(vec![b"{}\n[]".to_vec()]).unwrap_err(),
704 JsonlLineError::EmbeddedLf { offset: 2 }
705 );
706 }
707
708 #[test]
709 fn contiguous_views_never_include_filtered_out_lines_or_reorder_records() {
710 let backing = Bytes::from_static(b"a\nsecret\nb\n");
711 let a = JsonlLine::shared_slice(backing.clone(), 0..2).unwrap();
712 let b = JsonlLine::shared_slice(backing, 9..11).unwrap();
713 for (lines, expected) in [
714 (vec![a.clone(), b.clone()], b"a\nb\n"),
715 (vec![b, a], b"b\na\n"),
716 ] {
717 let batch = JsonlBatch::new(lines);
718 assert_eq!(batch.byte_len(), 4);
719 assert!(batch.as_contiguous_bytes().is_none());
720 let bytes: Vec<_> = batch
721 .wire_slices(16)
722 .unwrap()
723 .iter()
724 .flat_map(|slice| slice.iter().copied())
725 .collect();
726 assert_eq!(bytes, expected);
727 let compact = batch.into_compact();
728 assert_eq!(compact.as_contiguous_bytes().unwrap(), expected);
729 }
730 }
731
732 #[test]
733 fn compacting_preserves_empty_and_unterminated_line_boundaries() {
734 let batch = JsonlBatch::try_from(vec![
735 Vec::new(),
736 b"{}".to_vec(),
737 b"\r\n".to_vec(),
738 b"x\n".to_vec(),
739 ])
740 .unwrap();
741 let compact = batch.clone().into_compact();
742 assert_eq!(compact, batch);
743 assert!(compact.as_contiguous_bytes().is_none());
744 let bytes: Vec<_> = compact
745 .wire_slices(16)
746 .unwrap()
747 .iter()
748 .flat_map(|slice| slice.iter().copied())
749 .collect();
750 assert_eq!(bytes, b"\n{}\n\r\nx\n");
751 }
752
753 #[test]
754 fn compacting_a_sparse_batch_releases_its_backing_owner() {
755 struct Owner {
756 bytes: Vec<u8>,
757 dropped: Arc<AtomicBool>,
758 }
759 impl AsRef<[u8]> for Owner {
760 fn as_ref(&self) -> &[u8] {
761 &self.bytes
762 }
763 }
764 impl Drop for Owner {
765 fn drop(&mut self) {
766 self.dropped.store(true, Ordering::SeqCst);
767 }
768 }
769 let dropped = Arc::new(AtomicBool::new(false));
770 let mut bytes = vec![b'x'; 256 * 1024];
771 bytes[..11].copy_from_slice(b"a\nsecret\nb\n");
772 let backing = Bytes::from_owner(Owner {
773 bytes,
774 dropped: dropped.clone(),
775 });
776 let batch = JsonlBatch::new(vec![
777 JsonlLine::shared_slice(backing.clone(), 0..2).unwrap(),
778 JsonlLine::shared_slice(backing, 9..11).unwrap(),
779 ]);
780 assert!(!dropped.load(Ordering::SeqCst));
781 let compact = batch.into_compact();
782 assert!(dropped.load(Ordering::SeqCst));
783 assert_eq!(compact.as_contiguous_bytes().unwrap(), b"a\nb\n");
784 }
785
786 #[test]
787 fn validates_thresholds_and_reports_batch_dimensions() {
788 assert!(BatchOptions::new(0, 1, Duration::ZERO).is_err());
789 assert!(BatchOptions::new(1, 0, Duration::ZERO).is_err());
790 assert_eq!(BatchOptions::default().max_lines.get(), 256);
791 assert_eq!(BatchOptions::default().target_bytes.get(), 256 * 1024);
792 assert_eq!(BatchOptions::default().max_delay, Duration::from_millis(10));
793 let batch =
794 JsonlBatch::try_from(vec![b"{}\r\n".to_vec(), Vec::new(), b"tail".to_vec()]).unwrap();
795 assert_eq!(batch.len(), 3);
796 assert_eq!(batch.byte_len(), 8);
797 assert!(!batch.is_empty());
798 }
799
800 #[test]
801 fn flattening_is_runtime_independent_and_preserves_errors() {
802 futures_lite::future::block_on(async {
803 let bytes = Bytes::from_static(b"{}\r\nlast");
804 let pointer = bytes.as_ptr();
805 let batches: BatchStream<&'static str> = Box::pin(crate::stream::iter([
806 Ok(JsonlBatch::default()),
807 Ok(JsonlBatch::from_bytes(bytes)),
808 Err("source failed"),
809 Ok(JsonlBatch::from_bytes(Bytes::from_static(b"unreachable\n"))),
810 ]));
811 let mut lines: LineStream<&'static str> = flatten_batches(batches);
812 let first = lines.next().await.unwrap().unwrap();
813 assert_eq!(first.as_bytes(), b"{}\r\n");
814 assert_eq!(first.as_bytes().as_ptr(), pointer);
815 assert_eq!(lines.next().await.unwrap().unwrap().as_bytes(), b"last");
816 assert_eq!(lines.next().await.unwrap().unwrap_err(), "source failed");
817 assert!(lines.next().await.is_none());
818 });
819 }
820
821 #[cfg(feature = "tokio")]
822 mod runtime {
823 use super::*;
824 use core::{
825 convert::Infallible,
826 sync::atomic::AtomicUsize,
827 task::{Context, Poll},
828 };
829 use std::io;
830 use tokio::{
831 io::{AsyncRead, AsyncWriteExt, ReadBuf},
832 time::{Instant, timeout},
833 };
834
835 fn options(lines: usize, bytes: usize, delay: Duration) -> BatchOptions {
836 BatchOptions::new(lines, bytes, delay).unwrap()
837 }
838
839 fn lines(values: &[&[u8]]) -> LineStream<Infallible> {
840 Box::pin(crate::stream::iter(
841 values
842 .iter()
843 .map(|line| Ok(JsonlLine::copy_from_slice(line).unwrap()))
844 .collect::<Vec<_>>(),
845 ))
846 }
847
848 #[tokio::test(start_paused = true)]
849 async fn line_threshold_and_backpressure_do_not_prefetch_more_batches() {
850 let read = Arc::new(AtomicUsize::new(0));
851 let counter = read.clone();
852 let source = lines(&[b"a\n", b"b\n", b"c\n", b"d\n", b"e"]).map(move |line| {
853 counter.fetch_add(1, Ordering::SeqCst);
854 line
855 });
856 let mut batches = batch_lines(source, options(2, 1024, Duration::from_secs(1)));
857 assert_eq!(batches.next().await.unwrap().unwrap().len(), 2);
858 assert_eq!(read.load(Ordering::SeqCst), 2);
859 tokio::time::advance(Duration::from_secs(5)).await;
860 assert_eq!(
861 read.load(Ordering::SeqCst),
862 2,
863 "holding a batch must backpressure the source"
864 );
865 assert_eq!(batches.next().await.unwrap().unwrap().len(), 2);
866 assert_eq!(
867 batches
868 .next()
869 .await
870 .unwrap()
871 .unwrap()
872 .lines()
873 .collect::<Vec<_>>(),
874 vec![b"e".to_vec()]
875 );
876 assert!(batches.next().await.is_none());
877 assert!(batches.next().await.is_none());
878 }
879
880 #[tokio::test]
881 async fn byte_target_uses_complete_lines_and_allows_oversized_singletons() {
882 let mut batches = batch_lines(
883 lines(&[b"a\n", b"b\n", b"ccc\n", b"oversized", b"z"]),
884 options(100, 5, Duration::from_secs(1)),
885 );
886 for expected in [
887 vec![b"a\n".to_vec(), b"b\n".to_vec()],
888 vec![b"ccc\n".to_vec()],
889 vec![b"oversized".to_vec()],
890 vec![b"z".to_vec()],
891 ] {
892 assert_eq!(
893 batches
894 .next()
895 .await
896 .unwrap()
897 .unwrap()
898 .lines()
899 .collect::<Vec<_>>(),
900 expected
901 );
902 }
903 assert!(batches.next().await.is_none());
904 }
905
906 #[tokio::test(start_paused = true)]
907 async fn flushes_sparse_stream_on_deadline_without_waiting_for_eof() {
908 let source = lines(&[b"first\n"]).chain(crate::stream::pending());
909 let mut batches = batch_lines(source, options(100, 1024, Duration::from_millis(10)));
910 let start = Instant::now();
911 assert_eq!(
912 batches
913 .next()
914 .await
915 .unwrap()
916 .unwrap()
917 .lines()
918 .collect::<Vec<_>>(),
919 vec![b"first\n".to_vec()]
920 );
921 assert_eq!(start.elapsed(), Duration::from_millis(10));
922 }
923
924 #[tokio::test(start_paused = true)]
925 async fn never_emits_empty_batches_and_zero_delay_emits_immediately() {
926 let mut pending = batch_lines(
927 crate::stream::pending::<Result<JsonlLine, Infallible>>(),
928 BatchOptions::default(),
929 );
930 assert!(
931 timeout(Duration::from_secs(1), pending.next())
932 .await
933 .is_err()
934 );
935 let mut empty = batch_lines(lines(&[]), BatchOptions::default());
936 assert!(empty.next().await.is_none());
937 let mut ready = batch_lines(lines(&[b"a", b"b"]), options(100, 1024, Duration::ZERO));
938 let start = Instant::now();
939 assert_eq!(ready.next().await.unwrap().unwrap().len(), 1);
940 assert_eq!(ready.next().await.unwrap().unwrap().len(), 1);
941 assert_eq!(start.elapsed(), Duration::ZERO);
942 }
943
944 #[tokio::test(start_paused = true)]
945 async fn deadline_does_not_discard_a_partially_read_next_line() {
946 let (reader, mut writer) = tokio::io::duplex(128);
947 let writing = tokio::spawn(async move {
948 writer.write_all(b"{}\n{\"pa").await.unwrap();
949 tokio::time::sleep(Duration::from_millis(20)).await;
950 writer.write_all(b"rt\":true}\n").await.unwrap();
951 });
952 let mut batches =
953 crate::jsonl_batches(reader, options(100, 1024, Duration::from_millis(5)));
954 let start = Instant::now();
955 assert_eq!(
956 batches
957 .next()
958 .await
959 .unwrap()
960 .unwrap()
961 .lines()
962 .collect::<Vec<_>>(),
963 vec![b"{}\n".to_vec()]
964 );
965 assert_eq!(start.elapsed(), Duration::from_millis(5));
966 assert_eq!(
967 batches
968 .next()
969 .await
970 .unwrap()
971 .unwrap()
972 .lines()
973 .collect::<Vec<_>>(),
974 vec![b"{\"part\":true}\n".to_vec()]
975 );
976 assert!(batches.next().await.is_none());
977 writing.await.unwrap();
978 }
979
980 struct FailingSource {
981 step: usize,
982 dropped: Arc<AtomicBool>,
983 }
984 impl Drop for FailingSource {
985 fn drop(&mut self) {
986 self.dropped.store(true, Ordering::SeqCst);
987 }
988 }
989 impl Stream for FailingSource {
990 type Item = Result<JsonlLine, &'static str>;
991 fn poll_next(
992 mut self: Pin<&mut Self>,
993 _: &mut Context<'_>,
994 ) -> Poll<Option<Self::Item>> {
995 self.step += 1;
996 Poll::Ready(Some(match self.step {
997 1 => Ok(JsonlLine::owned(b"first\n".to_vec()).unwrap()),
998 2 => Err("source failed"),
999 _ => panic!("source must not be polled after failure"),
1000 }))
1001 }
1002 }
1003
1004 #[tokio::test]
1005 async fn flushes_partial_batch_before_error_but_drops_source_before_yielding() {
1006 let dropped = Arc::new(AtomicBool::new(false));
1007 let mut batches = batch_lines(
1008 FailingSource {
1009 step: 0,
1010 dropped: dropped.clone(),
1011 },
1012 BatchOptions::default(),
1013 );
1014 assert_eq!(
1015 batches
1016 .next()
1017 .await
1018 .unwrap()
1019 .unwrap()
1020 .lines()
1021 .collect::<Vec<_>>(),
1022 vec![b"first\n".to_vec()]
1023 );
1024 assert!(
1025 dropped.load(Ordering::SeqCst),
1026 "error cleanup must not wait for the consumer's next poll"
1027 );
1028 assert_eq!(batches.next().await.unwrap().unwrap_err(), "source failed");
1029 assert!(batches.next().await.is_none());
1030 }
1031
1032 struct FailingReader(bool);
1033 impl AsyncRead for FailingReader {
1034 fn poll_read(
1035 mut self: Pin<&mut Self>,
1036 _: &mut Context<'_>,
1037 buffer: &mut ReadBuf<'_>,
1038 ) -> Poll<io::Result<()>> {
1039 if self.0 {
1040 return Poll::Ready(Err(io::Error::other("read failed")));
1041 }
1042 self.0 = true;
1043 buffer.put_slice(b"{}\npartial");
1044 Poll::Ready(Ok(()))
1045 }
1046 }
1047
1048 #[tokio::test]
1049 async fn read_error_never_emits_an_incomplete_line() {
1050 let mut batches = crate::jsonl_batches(FailingReader(false), BatchOptions::default());
1051 assert_eq!(
1052 batches
1053 .next()
1054 .await
1055 .unwrap()
1056 .unwrap()
1057 .lines()
1058 .collect::<Vec<_>>(),
1059 vec![b"{}\n".to_vec()]
1060 );
1061 assert!(batches.next().await.unwrap().is_err());
1062 assert!(batches.next().await.is_none());
1063 }
1064
1065 #[tokio::test]
1066 async fn flattening_round_trips_raw_lines_and_ignores_empty_batches() {
1067 let expected = [b"{}\n".as_slice(), b"\r\n", b"\xff\n", b"tail"];
1068 let source = batch_lines(lines(&expected), options(2, 1024, Duration::from_secs(1)));
1069 let empty = crate::stream::iter([Ok(JsonlBatch::default())]);
1070 let mut flattened = flatten_batches(empty.chain(source));
1071 for line in expected {
1072 assert_eq!(flattened.next().await.unwrap().unwrap().as_bytes(), line);
1073 }
1074 assert!(flattened.next().await.is_none());
1075 }
1076 }
1077}