1mod diff;
17pub mod stream;
18
19use std::marker::PhantomData;
20use std::ops::{Deref, DerefMut};
21use std::sync::{Arc, Mutex, MutexGuard};
22use std::task::Poll;
23
24use bytes::Bytes;
25use moq_flate::{Decoder, Encoder};
26use serde::Serialize;
27use serde::de::DeserializeOwned;
28use serde_json::Value;
29
30pub use crate::diff::{Diff, diff};
31
32const MAX_DELTA_FRAMES: usize = 256;
37
38#[derive(thiserror::Error, Debug, Clone)]
40#[non_exhaustive]
41pub enum Error {
42 #[error(transparent)]
44 Net(#[from] moq_net::Error),
45
46 #[error("json: {0}")]
50 Json(String),
51
52 #[error(transparent)]
54 Flate(#[from] moq_flate::Error),
55}
56
57impl From<serde_json::Error> for Error {
58 fn from(err: serde_json::Error) -> Self {
59 Error::Json(err.to_string())
60 }
61}
62
63pub type Result<T> = std::result::Result<T, Error>;
65
66#[derive(Debug, Clone)]
71#[non_exhaustive]
72pub struct ProducerConfig {
73 pub delta_ratio: u32,
88
89 pub compression: bool,
95}
96
97impl Default for ProducerConfig {
98 fn default() -> Self {
99 Self {
100 delta_ratio: 8,
101 compression: false,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default)]
111#[non_exhaustive]
112pub struct ConsumerConfig {
113 pub compression: bool,
116}
117
118pub struct Producer<T> {
123 inner: Arc<Mutex<Inner>>,
124 _marker: PhantomData<fn(T)>,
125}
126
127impl<T> Clone for Producer<T> {
128 fn clone(&self) -> Self {
129 Self {
130 inner: self.inner.clone(),
131 _marker: PhantomData,
132 }
133 }
134}
135
136impl<T> Producer<T> {
137 pub fn consume(&self) -> moq_net::TrackConsumer {
139 self.inner.lock().unwrap().track.consume()
140 }
141}
142
143impl<T: Serialize> Producer<T> {
144 pub fn new(track: moq_net::TrackProducer, config: ProducerConfig) -> Self {
146 Self {
147 inner: Arc::new(Mutex::new(Inner {
148 track,
149 group: None,
150 encoder: None,
151 last: None,
152 delta_bytes: 0,
153 snapshot_len: 0,
154 group_frames: 0,
155 config,
156 })),
157 _marker: PhantomData,
158 }
159 }
160
161 pub fn update(&mut self, value: &T) -> Result<()> {
165 self.inner.lock().unwrap().update(value)
166 }
167
168 pub fn lock(&mut self) -> Guard<'_, T>
179 where
180 T: Default + DeserializeOwned,
181 {
182 let inner = self.inner.lock().unwrap();
183 let value = inner
184 .last
185 .as_ref()
186 .and_then(|last| serde_json::from_value(last.clone()).ok())
187 .unwrap_or_default();
188
189 Guard {
190 inner,
191 value,
192 dirty: false,
193 }
194 }
195
196 pub fn finish(&mut self) -> Result<()> {
198 self.inner.lock().unwrap().finish()
199 }
200}
201
202pub struct Guard<'a, T: Serialize> {
207 inner: MutexGuard<'a, Inner>,
208 value: T,
209 dirty: bool,
210}
211
212impl<T: Serialize> Deref for Guard<'_, T> {
213 type Target = T;
214
215 fn deref(&self) -> &T {
216 &self.value
217 }
218}
219
220impl<T: Serialize> DerefMut for Guard<'_, T> {
221 fn deref_mut(&mut self) -> &mut T {
222 self.dirty = true;
223 &mut self.value
224 }
225}
226
227impl<T: Serialize> Drop for Guard<'_, T> {
228 fn drop(&mut self) {
229 if !self.dirty {
230 return;
231 }
232
233 let _ = self.inner.update(&self.value);
235 }
236}
237
238struct Inner {
240 track: moq_net::TrackProducer,
241 group: Option<moq_net::GroupProducer>,
242 encoder: Option<Encoder>,
244 last: Option<Value>,
245 delta_bytes: u64,
248 snapshot_len: u64,
251 group_frames: usize,
252 config: ProducerConfig,
253}
254
255impl Inner {
256 fn update<T: Serialize>(&mut self, value: &T) -> Result<()> {
257 let Some(last) = self.last.as_ref() else {
260 return self.snapshot(value);
261 };
262
263 let Diff { patch, forced_snapshot } = diff(last, value);
265
266 if !forced_snapshot && patch.as_object().is_some_and(serde_json::Map::is_empty) {
268 return Ok(());
269 }
270
271 if forced_snapshot || !self.delta_allowed() {
274 return self.snapshot(value);
275 }
276
277 let bytes = serde_json::to_vec(&patch)?;
279 let slice = match self.encoder.as_mut() {
280 Some(encoder) => encoder.frame(&bytes),
281 None => Bytes::from(bytes),
282 };
283 let len = slice.len() as u64;
284 self.group
285 .as_mut()
286 .expect("delta_allowed guarantees an open group")
287 .write_frame(slice)?;
288 self.delta_bytes += len;
289 self.group_frames += 1;
290
291 json_patch::merge(self.last.as_mut().expect("a snapshot precedes any delta"), &patch);
293 Ok(())
294 }
295
296 fn delta_allowed(&self) -> bool {
304 let ratio = self.config.delta_ratio as u64;
305 ratio != 0
306 && self.group.is_some()
307 && self.group_frames < MAX_DELTA_FRAMES
308 && self.delta_bytes <= ratio * self.snapshot_len
309 }
310
311 fn snapshot<T: Serialize>(&mut self, value: &T) -> Result<()> {
313 let snapshot = serde_json::to_vec(value)?;
316
317 if let Some(mut group) = self.group.take() {
319 group.finish()?;
320 }
321
322 let mut group = self.track.append_group()?;
323
324 let (slice, encoder) = if self.config.compression {
327 let mut encoder = Encoder::new();
328 let slice = encoder.frame(&snapshot);
329 (slice, Some(encoder))
330 } else {
331 (Bytes::from(snapshot), None)
332 };
333 self.snapshot_len = slice.len() as u64;
334 group.write_frame(slice)?;
335 self.delta_bytes = 0;
336 self.group_frames = 1;
337 self.encoder = encoder;
338
339 if self.config.delta_ratio != 0 {
340 self.group = Some(group);
342 } else {
343 self.encoder = None;
345 group.finish()?;
346 }
347
348 self.last = Some(serde_json::to_value(value)?);
350 Ok(())
351 }
352
353 fn finish(&mut self) -> Result<()> {
354 if let Some(mut group) = self.group.take() {
355 group.finish()?;
356 }
357 self.track.finish()?;
358 Ok(())
359 }
360}
361
362pub struct Consumer<T> {
364 track: moq_net::TrackConsumer,
365 group: Option<moq_net::GroupConsumer>,
366 compressed: bool,
368 decoder: Option<Decoder>,
370 group_slices: Vec<Bytes>,
373 current: Option<Value>,
374 frames_read: usize,
375 _marker: PhantomData<fn() -> T>,
376}
377
378impl<T> Clone for Consumer<T> {
381 fn clone(&self) -> Self {
382 Self {
383 track: self.track.clone(),
384 group: self.group.clone(),
385 compressed: self.compressed,
386 decoder: None,
389 group_slices: self.group_slices.clone(),
390 current: self.current.clone(),
391 frames_read: self.frames_read,
392 _marker: PhantomData,
393 }
394 }
395}
396
397impl<T: DeserializeOwned> Consumer<T> {
398 pub fn new(track: moq_net::TrackConsumer, config: ConsumerConfig) -> Self {
403 Self {
404 track,
405 group: None,
406 compressed: config.compression,
407 decoder: None,
408 group_slices: Vec::new(),
409 current: None,
410 frames_read: 0,
411 _marker: PhantomData,
412 }
413 }
414
415 pub async fn next(&mut self) -> Result<Option<T>>
417 where
418 T: Unpin,
419 {
420 kio::wait(|waiter| self.poll_next(waiter)).await
421 }
422
423 pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<T>>> {
433 let track_finished = loop {
435 match self.track.poll_next_group(waiter)? {
436 Poll::Ready(Some(group)) => {
437 self.group = Some(group);
438 self.current = None;
439 self.frames_read = 0;
440 self.decoder = None;
442 self.group_slices.clear();
443 }
444 Poll::Ready(None) => break true,
445 Poll::Pending => break false,
446 }
447 };
448
449 let mut advanced = false;
454 let mut group_pending = false;
455 while let Some(group) = &mut self.group {
456 match group.poll_read_frame(waiter)? {
457 Poll::Ready(Some(frame)) => {
458 self.apply(frame)?;
459 advanced = true;
460 }
461 Poll::Ready(None) => {
463 self.group = None;
464 break;
465 }
466 Poll::Pending => {
468 group_pending = true;
469 break;
470 }
471 }
472 }
473
474 if advanced {
475 return Poll::Ready(Ok(Some(self.reconstruct()?)));
477 }
478
479 if group_pending {
482 return Poll::Pending;
483 }
484
485 if track_finished {
486 Poll::Ready(Ok(None))
487 } else {
488 Poll::Pending
489 }
490 }
491
492 fn decode(&mut self, slice: Bytes) -> Result<Bytes> {
498 if !self.compressed {
499 return Ok(slice);
500 }
501
502 if self.decoder.is_none() {
503 let mut decoder = Decoder::new();
504 for prev in &self.group_slices {
505 decoder.frame(prev)?;
506 }
507 self.decoder = Some(decoder);
508 }
509
510 let plain = self.decoder.as_mut().unwrap().frame(&slice)?;
511 self.group_slices.push(slice);
512 Ok(plain)
513 }
514
515 fn apply(&mut self, frame: Bytes) -> Result<()> {
518 let frame = self.decode(frame)?;
519 if self.frames_read == 0 {
520 self.current = Some(serde_json::from_slice(&frame)?);
521 } else {
522 let patch: Value = serde_json::from_slice(&frame)?;
523 let current = self.current.as_mut().expect("a snapshot precedes any delta");
524 json_patch::merge(current, &patch);
525 }
526 self.frames_read += 1;
527 Ok(())
528 }
529
530 fn reconstruct(&self) -> Result<T> {
533 let current = self
534 .current
535 .as_ref()
536 .expect("a value is present after applying a frame");
537 Ok(serde_json::from_value(current.clone())?)
538 }
539}
540
541#[cfg(test)]
542mod test {
543 use super::*;
544 use serde_json::json;
545
546 fn cfg(delta_ratio: u32) -> ProducerConfig {
548 ProducerConfig {
549 delta_ratio,
550 ..Default::default()
551 }
552 }
553
554 fn cfg_deflate(delta_ratio: u32) -> ProducerConfig {
556 ProducerConfig {
557 delta_ratio,
558 compression: true,
559 }
560 }
561
562 fn deflate_consumer(track: moq_net::TrackConsumer) -> Consumer<Value> {
564 Consumer::new(track, ConsumerConfig { compression: true })
565 }
566
567 fn producer(config: ProducerConfig) -> (Producer<Value>, moq_net::TrackConsumer) {
568 let track = moq_net::Track::new("test").produce();
569 let consumer = track.consume();
570 (Producer::new(track, config), consumer)
571 }
572
573 fn drain(track: moq_net::TrackConsumer) -> Vec<Value> {
575 drain_with(Consumer::<Value>::new(track, ConsumerConfig::default()))
576 }
577
578 fn drain_with(mut consumer: Consumer<Value>) -> Vec<Value> {
580 let waiter = kio::Waiter::noop();
581 let mut out = Vec::new();
582 while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
583 out.push(value);
584 }
585 out
586 }
587
588 #[test]
589 fn deltas_off_snapshot_per_group() {
590 let (mut producer, track) = producer(cfg(0));
591 producer.update(&json!({ "a": 1 })).unwrap();
592 producer.update(&json!({ "a": 2 })).unwrap();
593 producer.finish().unwrap();
594
595 assert_eq!(track.latest(), Some(1));
598 assert_eq!(drain(track), vec![json!({ "a": 2 })]);
599 }
600
601 #[test]
602 fn live_consumer_sees_each_update() {
603 let (mut producer, track) = producer(ProducerConfig::default());
604 let mut consumer = Consumer::<Value>::new(track, ConsumerConfig::default());
605 let waiter = kio::Waiter::noop();
606
607 for n in 1..=3 {
608 producer.update(&json!({ "a": n })).unwrap();
609 match consumer.poll_next(&waiter) {
610 Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": n })),
611 other => panic!("expected value, got {other:?}"),
612 }
613 }
614 }
615
616 #[test]
617 fn unchanged_value_writes_nothing() {
618 let (mut producer, track) = producer(ProducerConfig::default());
619 producer.update(&json!({ "a": 1 })).unwrap();
620 producer.update(&json!({ "a": 1 })).unwrap();
621 producer.finish().unwrap();
622
623 assert_eq!(track.latest(), Some(0));
624 assert_eq!(drain(track), vec![json!({ "a": 1 })]);
625 }
626
627 #[test]
628 fn deltas_share_one_group() {
629 let config = cfg(100);
630 let (mut producer, track) = producer(config);
631 producer.update(&json!({ "a": 1, "b": 1 })).unwrap();
632 producer.update(&json!({ "a": 1, "b": 2 })).unwrap();
633 producer.update(&json!({ "a": 1, "b": 3 })).unwrap();
634 producer.finish().unwrap();
635
636 assert_eq!(track.latest(), Some(0));
638 let values = drain(track);
639 assert_eq!(values.last().unwrap(), &json!({ "a": 1, "b": 3 }));
640 }
641
642 #[test]
643 fn tight_ratio_rolls_snapshots() {
644 let config = cfg(1);
649 let (mut producer, track) = producer(config);
650 producer.update(&json!({ "a": 1 })).unwrap(); producer.update(&json!({ "a": 2 })).unwrap(); producer.update(&json!({ "a": 3 })).unwrap(); producer.update(&json!({ "a": 4 })).unwrap(); producer.finish().unwrap();
655
656 assert_eq!(track.latest(), Some(1));
657 }
658
659 #[test]
660 fn deltas_stay_within_ratio_times_snapshot() {
661 let config = cfg(8);
667 let (mut producer, track) = producer(config);
668 for n in 0..=10 {
669 producer.update(&json!({ "n": n })).unwrap();
670 }
671 producer.finish().unwrap();
672
673 assert_eq!(track.latest(), Some(1));
675 assert_eq!(drain(track).last().unwrap(), &json!({ "n": 10 }));
676 }
677
678 #[test]
679 fn array_change_is_delta() {
680 let config = cfg(100);
681 let (mut producer, track) = producer(config);
682 producer.update(&json!({ "list": [1, 2] })).unwrap();
683 producer.update(&json!({ "list": [1, 2, 3] })).unwrap();
684 producer.finish().unwrap();
685
686 assert_eq!(track.latest(), Some(0));
688 assert_eq!(drain(track).last().unwrap(), &json!({ "list": [1, 2, 3] }));
689 }
690
691 #[test]
692 fn frame_cap_rolls_snapshot() {
693 let config = cfg(1_000_000);
694 let (mut producer, track) = producer(config);
695 for i in 0..=MAX_DELTA_FRAMES {
697 producer.update(&json!({ "n": i })).unwrap();
698 }
699 producer.finish().unwrap();
700
701 assert_eq!(track.latest(), Some(1));
703 assert_eq!(drain(track).last().unwrap(), &json!({ "n": MAX_DELTA_FRAMES }));
704 }
705
706 #[test]
707 fn late_joiner_reconstructs_from_deltas() {
708 let config = cfg(100);
709 let (mut producer, track) = producer(config);
710 producer.update(&json!({ "a": 1, "b": 1 })).unwrap();
711 producer.update(&json!({ "a": 1, "b": 2 })).unwrap();
712 producer.update(&json!({ "a": 5, "b": 2 })).unwrap();
713 producer.finish().unwrap();
714
715 assert_eq!(drain(track).last().unwrap(), &json!({ "a": 5, "b": 2 }));
717 }
718
719 #[test]
720 fn lock_composes_independent_owners() {
721 #[derive(serde::Serialize, serde::Deserialize, Default, PartialEq, Debug)]
723 struct Doc {
724 #[serde(skip_serializing_if = "Option::is_none")]
725 video: Option<String>,
726 #[serde(skip_serializing_if = "Option::is_none")]
727 scte35: Option<u32>,
728 }
729
730 let track = moq_net::Track::new("test").produce();
731 let consumer = track.consume();
732 let mut producer = Producer::<Doc>::new(track, ProducerConfig::default());
733
734 producer.lock().video = Some("v1".to_string());
736
737 producer.lock().scte35 = Some(42);
739
740 let _ = producer.lock();
742
743 producer.finish().unwrap();
744
745 let mut consumer = Consumer::<Doc>::new(consumer, ConsumerConfig::default());
746 let waiter = kio::Waiter::noop();
747 let mut last = None;
748 while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
749 last = Some(value);
750 }
751 assert_eq!(
752 last.unwrap(),
753 Doc {
754 video: Some("v1".to_string()),
755 scte35: Some(42),
756 }
757 );
758 }
759
760 #[test]
761 fn newer_group_supersedes_in_progress_reconstruction() {
762 let config = cfg(1);
765 let (mut producer, track) = producer(config);
766 let observer = producer.consume();
767 let mut consumer = Consumer::<Value>::new(track, ConsumerConfig::default());
768 let waiter = kio::Waiter::noop();
769
770 producer.update(&json!({ "a": 1 })).unwrap(); match consumer.poll_next(&waiter) {
772 Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1 })),
773 other => panic!("expected first value, got {other:?}"),
774 }
775
776 producer.update(&json!({ "a": 2 })).unwrap(); producer.update(&json!({ "a": 3 })).unwrap(); producer.update(&json!({ "a": 4 })).unwrap(); producer.finish().unwrap();
780 assert_eq!(observer.latest(), Some(1));
781
782 let mut last = None;
784 while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
785 last = Some(value);
786 }
787 assert_eq!(last.unwrap(), json!({ "a": 4 }));
788 }
789
790 #[test]
791 fn cloned_consumer_reconstructs_independently() {
792 let config = cfg(100);
794 let (mut producer, track) = producer(config);
795 let mut consumer = Consumer::<Value>::new(track, ConsumerConfig::default());
796 let waiter = kio::Waiter::noop();
797
798 producer.update(&json!({ "a": 1, "b": 1 })).unwrap(); match consumer.poll_next(&waiter) {
800 Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1, "b": 1 })),
801 other => panic!("expected snapshot, got {other:?}"),
802 }
803
804 let mut clone = consumer.clone();
806
807 producer.update(&json!({ "a": 1, "b": 2 })).unwrap(); producer.finish().unwrap();
809
810 let expected = json!({ "a": 1, "b": 2 });
812 for consumer in [&mut consumer, &mut clone] {
813 match consumer.poll_next(&waiter) {
814 Poll::Ready(Ok(Some(value))) => assert_eq!(value, expected),
815 other => panic!("expected delta, got {other:?}"),
816 }
817 }
818 }
819
820 #[test]
821 fn open_group_pends_after_track_finish() {
822 let mut track = moq_net::Track::new("test").produce();
825 let mut group = track.append_group().unwrap();
826 track.finish().unwrap();
827
828 let mut consumer = Consumer::<Value>::new(track.consume(), ConsumerConfig::default());
829 let waiter = kio::Waiter::noop();
830
831 assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
833
834 group
835 .write_frame(Bytes::from(serde_json::to_vec(&json!({ "a": 1 })).unwrap()))
836 .unwrap();
837 group.finish().unwrap();
838
839 match consumer.poll_next(&waiter) {
840 Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1 })),
841 other => panic!("expected the catalog value, got {other:?}"),
842 }
843 }
844
845 #[test]
846 fn late_joiner_collapses_backlog_to_latest() {
847 let (mut producer, track) = producer(cfg(100));
850 for n in 0..=20 {
851 producer.update(&json!({ "n": n })).unwrap();
852 }
853 producer.finish().unwrap();
854
855 assert_eq!(track.latest(), Some(0));
857 let values = drain(track);
858 assert_eq!(
859 values,
860 vec![json!({ "n": 20 })],
861 "backlog should collapse to the latest value"
862 );
863 }
864
865 #[test]
866 fn compressed_late_joiner_collapses_backlog_to_latest() {
867 let (mut producer, track) = producer(cfg_deflate(100));
869 for n in 0..=20 {
870 producer.update(&json!({ "n": n })).unwrap();
871 }
872 producer.finish().unwrap();
873
874 assert_eq!(track.latest(), Some(0));
875 let values = drain_with(deflate_consumer(track));
876 assert_eq!(
877 values,
878 vec![json!({ "n": 20 })],
879 "compressed backlog should collapse to the latest"
880 );
881 }
882
883 #[test]
884 fn compressed_snapshot_per_group_roundtrips() {
885 let (mut producer, track) = producer(cfg_deflate(0));
886 producer.update(&json!({ "a": 1 })).unwrap();
887 producer.update(&json!({ "a": 2 })).unwrap();
888 producer.finish().unwrap();
889
890 assert_eq!(track.latest(), Some(1));
892 let values = drain_with(deflate_consumer(track));
893 assert_eq!(values, vec![json!({ "a": 2 })]);
894 }
895
896 #[test]
897 fn compressed_deltas_share_one_group() {
898 let (mut producer, track) = producer(cfg_deflate(100));
899 producer.update(&json!({ "a": 1, "b": 1 })).unwrap();
900 producer.update(&json!({ "a": 1, "b": 2 })).unwrap();
901 producer.update(&json!({ "a": 1, "b": 3 })).unwrap();
902 producer.finish().unwrap();
903
904 assert_eq!(track.latest(), Some(0));
906 let values = drain_with(deflate_consumer(track));
907 assert_eq!(values.last().unwrap(), &json!({ "a": 1, "b": 3 }));
908 }
909
910 #[test]
911 fn compressed_late_joiner_reconstructs_from_deltas() {
912 let (mut producer, track) = producer(cfg_deflate(100));
913 producer.update(&json!({ "a": 1, "b": 1 })).unwrap();
914 producer.update(&json!({ "a": 1, "b": 2 })).unwrap();
915 producer.update(&json!({ "a": 5, "b": 2 })).unwrap();
916 producer.finish().unwrap();
917
918 let values = drain_with(deflate_consumer(track));
920 assert_eq!(values.last().unwrap(), &json!({ "a": 5, "b": 2 }));
921 }
922
923 #[test]
924 fn compressed_deltas_roll_on_compressed_budget() {
925 let (mut producer, track) = producer(cfg_deflate(2));
931 for n in 0..=40 {
932 producer.update(&json!({ "n": n })).unwrap();
933 }
934 producer.finish().unwrap();
935
936 assert!(
937 track.latest().unwrap() > 0,
938 "a tight ratio should roll at least one compressed group"
939 );
940 assert_eq!(drain_with(deflate_consumer(track)).last().unwrap(), &json!({ "n": 40 }));
941 }
942
943 #[test]
944 fn compressed_cloned_consumer_reconstructs_mid_group() {
945 let (mut producer, track) = producer(cfg_deflate(100));
947 let mut consumer = deflate_consumer(track);
948 let waiter = kio::Waiter::noop();
949
950 producer.update(&json!({ "a": 1, "b": 1 })).unwrap(); match consumer.poll_next(&waiter) {
952 Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1, "b": 1 })),
953 other => panic!("expected snapshot, got {other:?}"),
954 }
955
956 let mut clone = consumer.clone();
957
958 producer.update(&json!({ "a": 1, "b": 2 })).unwrap(); producer.finish().unwrap();
960
961 let expected = json!({ "a": 1, "b": 2 });
962 for consumer in [&mut consumer, &mut clone] {
963 match consumer.poll_next(&waiter) {
964 Poll::Ready(Ok(Some(value))) => assert_eq!(value, expected),
965 other => panic!("expected delta, got {other:?}"),
966 }
967 }
968 }
969
970 #[test]
971 fn compression_shrinks_wire_frames() {
972 let value = json!({ "renditions": ["video".repeat(50), "video".repeat(50), "video".repeat(50)] });
974
975 let plaintext_bytes = wire_frame_len(cfg(0), &value);
976 let compressed_bytes = wire_frame_len(cfg_deflate(0), &value);
977 assert!(
978 compressed_bytes < plaintext_bytes,
979 "compressed frame {compressed_bytes} should be smaller than plaintext {plaintext_bytes}"
980 );
981 }
982
983 #[test]
984 fn compressed_deltas_reuse_window() {
985 let (mut producer, mut track) = producer(cfg_deflate(100));
988 let phrase = "Media over QUIC delivers real-time latency at massive scale";
989 producer.update(&json!({ "note": phrase })).unwrap();
990 producer.update(&json!({ "note": phrase, "echo": phrase })).unwrap();
991 producer.finish().unwrap();
992
993 let waiter = kio::Waiter::noop();
995 let Poll::Ready(Ok(Some(mut group))) = track.poll_next_group(&waiter) else {
996 panic!("expected a group");
997 };
998 let mut frames = Vec::new();
999 while let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) {
1000 frames.push(frame);
1001 }
1002 assert_eq!(frames.len(), 2, "snapshot + one delta in a single group");
1003
1004 let raw_delta = serde_json::to_vec(&json!({ "echo": phrase })).unwrap();
1006 assert!(
1007 frames[1].len() < raw_delta.len() / 2,
1008 "windowed delta {} should be far below the raw patch {}",
1009 frames[1].len(),
1010 raw_delta.len()
1011 );
1012 }
1013
1014 fn wire_frame_len(config: ProducerConfig, value: &Value) -> usize {
1016 let (mut producer, mut track) = producer(config);
1017 producer.update(value).unwrap();
1018 producer.finish().unwrap();
1019
1020 let waiter = kio::Waiter::noop();
1021 let Poll::Ready(Ok(Some(mut group))) = track.poll_next_group(&waiter) else {
1022 panic!("expected a group");
1023 };
1024 let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) else {
1026 panic!("expected a frame");
1027 };
1028 frame.len()
1029 }
1030}