mod diff;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::sync::{Arc, Mutex, MutexGuard};
use std::task::Poll;
use bytes::Bytes;
use moq_flate::{Decoder, Encoder};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;
pub use crate::diff::{Diff, diff};
const MAX_DELTA_FRAMES: usize = 256;
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Net(#[from] moq_net::Error),
#[error("json: {0}")]
Json(String),
#[error(transparent)]
Flate(#[from] moq_flate::Error),
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Error::Json(err.to_string())
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ProducerConfig {
pub delta_ratio: u32,
pub compression: bool,
}
impl Default for ProducerConfig {
fn default() -> Self {
Self {
delta_ratio: 8,
compression: false,
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ConsumerConfig {
pub compression: bool,
}
pub struct Producer<T> {
inner: Arc<Mutex<Inner>>,
_marker: PhantomData<fn(T)>,
}
impl<T> Clone for Producer<T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
_marker: PhantomData,
}
}
}
impl<T> Producer<T> {
pub fn consume(&self) -> moq_net::TrackConsumer {
self.inner.lock().unwrap().track.consume()
}
}
impl<T: Serialize> Producer<T> {
pub fn new(track: moq_net::TrackProducer, config: ProducerConfig) -> Self {
Self {
inner: Arc::new(Mutex::new(Inner {
track,
group: None,
encoder: None,
last: None,
delta_bytes: 0,
snapshot_len: 0,
group_frames: 0,
config,
})),
_marker: PhantomData,
}
}
pub fn update(&mut self, value: &T) -> Result<()> {
self.inner.lock().unwrap().update(value)
}
pub fn lock(&mut self) -> Guard<'_, T>
where
T: Default + DeserializeOwned,
{
let inner = self.inner.lock().unwrap();
let value = inner
.last
.as_ref()
.and_then(|last| serde_json::from_value(last.clone()).ok())
.unwrap_or_default();
Guard {
inner,
value,
dirty: false,
}
}
pub fn finish(&mut self) -> Result<()> {
self.inner.lock().unwrap().finish()
}
}
pub struct Guard<'a, T: Serialize> {
inner: MutexGuard<'a, Inner>,
value: T,
dirty: bool,
}
impl<T: Serialize> Deref for Guard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
&self.value
}
}
impl<T: Serialize> DerefMut for Guard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
self.dirty = true;
&mut self.value
}
}
impl<T: Serialize> Drop for Guard<'_, T> {
fn drop(&mut self) {
if !self.dirty {
return;
}
let _ = self.inner.update(&self.value);
}
}
struct Inner {
track: moq_net::TrackProducer,
group: Option<moq_net::GroupProducer>,
encoder: Option<Encoder>,
last: Option<Value>,
delta_bytes: u64,
snapshot_len: u64,
group_frames: usize,
config: ProducerConfig,
}
impl Inner {
fn update<T: Serialize>(&mut self, value: &T) -> Result<()> {
let Some(last) = self.last.as_ref() else {
return self.snapshot(value);
};
let Diff { patch, forced_snapshot } = diff(last, value);
if !forced_snapshot && patch.as_object().is_some_and(serde_json::Map::is_empty) {
return Ok(());
}
if forced_snapshot || !self.delta_allowed() {
return self.snapshot(value);
}
let bytes = serde_json::to_vec(&patch)?;
let slice = match self.encoder.as_mut() {
Some(encoder) => encoder.frame(&bytes),
None => Bytes::from(bytes),
};
let len = slice.len() as u64;
self.group
.as_mut()
.expect("delta_allowed guarantees an open group")
.write_frame(slice)?;
self.delta_bytes += len;
self.group_frames += 1;
json_patch::merge(self.last.as_mut().expect("a snapshot precedes any delta"), &patch);
Ok(())
}
fn delta_allowed(&self) -> bool {
let ratio = self.config.delta_ratio as u64;
ratio != 0
&& self.group.is_some()
&& self.group_frames < MAX_DELTA_FRAMES
&& self.delta_bytes <= ratio * self.snapshot_len
}
fn snapshot<T: Serialize>(&mut self, value: &T) -> Result<()> {
let snapshot = serde_json::to_vec(value)?;
if let Some(mut group) = self.group.take() {
group.finish()?;
}
let mut group = self.track.append_group()?;
let (slice, encoder) = if self.config.compression {
let mut encoder = Encoder::new();
let slice = encoder.frame(&snapshot);
(slice, Some(encoder))
} else {
(Bytes::from(snapshot), None)
};
self.snapshot_len = slice.len() as u64;
group.write_frame(slice)?;
self.delta_bytes = 0;
self.group_frames = 1;
self.encoder = encoder;
if self.config.delta_ratio != 0 {
self.group = Some(group);
} else {
self.encoder = None;
group.finish()?;
}
self.last = Some(serde_json::to_value(value)?);
Ok(())
}
fn finish(&mut self) -> Result<()> {
if let Some(mut group) = self.group.take() {
group.finish()?;
}
self.track.finish()?;
Ok(())
}
}
pub struct Consumer<T> {
track: moq_net::TrackConsumer,
group: Option<moq_net::GroupConsumer>,
compressed: bool,
decoder: Option<Decoder>,
group_slices: Vec<Bytes>,
current: Option<Value>,
frames_read: usize,
_marker: PhantomData<fn() -> T>,
}
impl<T> Clone for Consumer<T> {
fn clone(&self) -> Self {
Self {
track: self.track.clone(),
group: self.group.clone(),
compressed: self.compressed,
decoder: None,
group_slices: self.group_slices.clone(),
current: self.current.clone(),
frames_read: self.frames_read,
_marker: PhantomData,
}
}
}
impl<T: DeserializeOwned> Consumer<T> {
pub fn new(track: moq_net::TrackConsumer, config: ConsumerConfig) -> Self {
Self {
track,
group: None,
compressed: config.compression,
decoder: None,
group_slices: Vec::new(),
current: None,
frames_read: 0,
_marker: PhantomData,
}
}
pub async fn next(&mut self) -> Result<Option<T>>
where
T: Unpin,
{
kio::wait(|waiter| self.poll_next(waiter)).await
}
pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<T>>> {
let track_finished = loop {
match self.track.poll_next_group(waiter)? {
Poll::Ready(Some(group)) => {
self.group = Some(group);
self.current = None;
self.frames_read = 0;
self.decoder = None;
self.group_slices.clear();
}
Poll::Ready(None) => break true,
Poll::Pending => break false,
}
};
let mut advanced = false;
let mut group_pending = false;
while let Some(group) = &mut self.group {
match group.poll_read_frame(waiter)? {
Poll::Ready(Some(frame)) => {
self.apply(frame)?;
advanced = true;
}
Poll::Ready(None) => {
self.group = None;
break;
}
Poll::Pending => {
group_pending = true;
break;
}
}
}
if advanced {
return Poll::Ready(Ok(Some(self.reconstruct()?)));
}
if group_pending {
return Poll::Pending;
}
if track_finished {
Poll::Ready(Ok(None))
} else {
Poll::Pending
}
}
fn decode(&mut self, slice: Bytes) -> Result<Bytes> {
if !self.compressed {
return Ok(slice);
}
if self.decoder.is_none() {
let mut decoder = Decoder::new();
for prev in &self.group_slices {
decoder.frame(prev)?;
}
self.decoder = Some(decoder);
}
let plain = self.decoder.as_mut().unwrap().frame(&slice)?;
self.group_slices.push(slice);
Ok(plain)
}
fn apply(&mut self, frame: Bytes) -> Result<()> {
let frame = self.decode(frame)?;
if self.frames_read == 0 {
self.current = Some(serde_json::from_slice(&frame)?);
} else {
let patch: Value = serde_json::from_slice(&frame)?;
let current = self.current.as_mut().expect("a snapshot precedes any delta");
json_patch::merge(current, &patch);
}
self.frames_read += 1;
Ok(())
}
fn reconstruct(&self) -> Result<T> {
let current = self
.current
.as_ref()
.expect("a value is present after applying a frame");
Ok(serde_json::from_value(current.clone())?)
}
}
#[cfg(test)]
mod test {
use super::*;
use serde_json::json;
fn cfg(delta_ratio: u32) -> ProducerConfig {
ProducerConfig {
delta_ratio,
..Default::default()
}
}
fn cfg_deflate(delta_ratio: u32) -> ProducerConfig {
ProducerConfig {
delta_ratio,
compression: true,
}
}
fn deflate_consumer(track: moq_net::TrackConsumer) -> Consumer<Value> {
Consumer::new(track, ConsumerConfig { compression: true })
}
fn producer(config: ProducerConfig) -> (Producer<Value>, moq_net::TrackConsumer) {
let track = moq_net::Track::new("test").produce();
let consumer = track.consume();
(Producer::new(track, config), consumer)
}
fn drain(track: moq_net::TrackConsumer) -> Vec<Value> {
drain_with(Consumer::<Value>::new(track, ConsumerConfig::default()))
}
fn drain_with(mut consumer: Consumer<Value>) -> Vec<Value> {
let waiter = kio::Waiter::noop();
let mut out = Vec::new();
while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
out.push(value);
}
out
}
#[test]
fn deltas_off_snapshot_per_group() {
let (mut producer, track) = producer(cfg(0));
producer.update(&json!({ "a": 1 })).unwrap();
producer.update(&json!({ "a": 2 })).unwrap();
producer.finish().unwrap();
assert_eq!(track.latest(), Some(1));
assert_eq!(drain(track), vec![json!({ "a": 2 })]);
}
#[test]
fn live_consumer_sees_each_update() {
let (mut producer, track) = producer(ProducerConfig::default());
let mut consumer = Consumer::<Value>::new(track, ConsumerConfig::default());
let waiter = kio::Waiter::noop();
for n in 1..=3 {
producer.update(&json!({ "a": n })).unwrap();
match consumer.poll_next(&waiter) {
Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": n })),
other => panic!("expected value, got {other:?}"),
}
}
}
#[test]
fn unchanged_value_writes_nothing() {
let (mut producer, track) = producer(ProducerConfig::default());
producer.update(&json!({ "a": 1 })).unwrap();
producer.update(&json!({ "a": 1 })).unwrap();
producer.finish().unwrap();
assert_eq!(track.latest(), Some(0));
assert_eq!(drain(track), vec![json!({ "a": 1 })]);
}
#[test]
fn deltas_share_one_group() {
let config = cfg(100);
let (mut producer, track) = producer(config);
producer.update(&json!({ "a": 1, "b": 1 })).unwrap();
producer.update(&json!({ "a": 1, "b": 2 })).unwrap();
producer.update(&json!({ "a": 1, "b": 3 })).unwrap();
producer.finish().unwrap();
assert_eq!(track.latest(), Some(0));
let values = drain(track);
assert_eq!(values.last().unwrap(), &json!({ "a": 1, "b": 3 }));
}
#[test]
fn tight_ratio_rolls_snapshots() {
let config = cfg(1);
let (mut producer, track) = producer(config);
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();
assert_eq!(track.latest(), Some(1));
}
#[test]
fn deltas_stay_within_ratio_times_snapshot() {
let config = cfg(8);
let (mut producer, track) = producer(config);
for n in 0..=10 {
producer.update(&json!({ "n": n })).unwrap();
}
producer.finish().unwrap();
assert_eq!(track.latest(), Some(1));
assert_eq!(drain(track).last().unwrap(), &json!({ "n": 10 }));
}
#[test]
fn array_change_is_delta() {
let config = cfg(100);
let (mut producer, track) = producer(config);
producer.update(&json!({ "list": [1, 2] })).unwrap();
producer.update(&json!({ "list": [1, 2, 3] })).unwrap();
producer.finish().unwrap();
assert_eq!(track.latest(), Some(0));
assert_eq!(drain(track).last().unwrap(), &json!({ "list": [1, 2, 3] }));
}
#[test]
fn frame_cap_rolls_snapshot() {
let config = cfg(1_000_000);
let (mut producer, track) = producer(config);
for i in 0..=MAX_DELTA_FRAMES {
producer.update(&json!({ "n": i })).unwrap();
}
producer.finish().unwrap();
assert_eq!(track.latest(), Some(1));
assert_eq!(drain(track).last().unwrap(), &json!({ "n": MAX_DELTA_FRAMES }));
}
#[test]
fn late_joiner_reconstructs_from_deltas() {
let config = cfg(100);
let (mut producer, track) = producer(config);
producer.update(&json!({ "a": 1, "b": 1 })).unwrap();
producer.update(&json!({ "a": 1, "b": 2 })).unwrap();
producer.update(&json!({ "a": 5, "b": 2 })).unwrap();
producer.finish().unwrap();
assert_eq!(drain(track).last().unwrap(), &json!({ "a": 5, "b": 2 }));
}
#[test]
fn lock_composes_independent_owners() {
#[derive(serde::Serialize, serde::Deserialize, Default, PartialEq, Debug)]
struct Doc {
#[serde(skip_serializing_if = "Option::is_none")]
video: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
scte35: Option<u32>,
}
let track = moq_net::Track::new("test").produce();
let consumer = track.consume();
let mut producer = Producer::<Doc>::new(track, ProducerConfig::default());
producer.lock().video = Some("v1".to_string());
producer.lock().scte35 = Some(42);
let _ = producer.lock();
producer.finish().unwrap();
let mut consumer = Consumer::<Doc>::new(consumer, ConsumerConfig::default());
let waiter = kio::Waiter::noop();
let mut last = None;
while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
last = Some(value);
}
assert_eq!(
last.unwrap(),
Doc {
video: Some("v1".to_string()),
scte35: Some(42),
}
);
}
#[test]
fn newer_group_supersedes_in_progress_reconstruction() {
let config = cfg(1);
let (mut producer, track) = producer(config);
let observer = producer.consume();
let mut consumer = Consumer::<Value>::new(track, ConsumerConfig::default());
let waiter = kio::Waiter::noop();
producer.update(&json!({ "a": 1 })).unwrap(); match consumer.poll_next(&waiter) {
Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1 })),
other => panic!("expected first value, got {other:?}"),
}
producer.update(&json!({ "a": 2 })).unwrap(); producer.update(&json!({ "a": 3 })).unwrap(); producer.update(&json!({ "a": 4 })).unwrap(); producer.finish().unwrap();
assert_eq!(observer.latest(), Some(1));
let mut last = None;
while let Poll::Ready(Ok(Some(value))) = consumer.poll_next(&waiter) {
last = Some(value);
}
assert_eq!(last.unwrap(), json!({ "a": 4 }));
}
#[test]
fn cloned_consumer_reconstructs_independently() {
let config = cfg(100);
let (mut producer, track) = producer(config);
let mut consumer = Consumer::<Value>::new(track, ConsumerConfig::default());
let waiter = kio::Waiter::noop();
producer.update(&json!({ "a": 1, "b": 1 })).unwrap(); match consumer.poll_next(&waiter) {
Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1, "b": 1 })),
other => panic!("expected snapshot, got {other:?}"),
}
let mut clone = consumer.clone();
producer.update(&json!({ "a": 1, "b": 2 })).unwrap(); producer.finish().unwrap();
let expected = json!({ "a": 1, "b": 2 });
for consumer in [&mut consumer, &mut clone] {
match consumer.poll_next(&waiter) {
Poll::Ready(Ok(Some(value))) => assert_eq!(value, expected),
other => panic!("expected delta, got {other:?}"),
}
}
}
#[test]
fn open_group_pends_after_track_finish() {
let mut track = moq_net::Track::new("test").produce();
let mut group = track.append_group().unwrap();
track.finish().unwrap();
let mut consumer = Consumer::<Value>::new(track.consume(), ConsumerConfig::default());
let waiter = kio::Waiter::noop();
assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
group
.write_frame(Bytes::from(serde_json::to_vec(&json!({ "a": 1 })).unwrap()))
.unwrap();
group.finish().unwrap();
match consumer.poll_next(&waiter) {
Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1 })),
other => panic!("expected the catalog value, got {other:?}"),
}
}
#[test]
fn late_joiner_collapses_backlog_to_latest() {
let (mut producer, track) = producer(cfg(100));
for n in 0..=20 {
producer.update(&json!({ "n": n })).unwrap();
}
producer.finish().unwrap();
assert_eq!(track.latest(), Some(0));
let values = drain(track);
assert_eq!(
values,
vec![json!({ "n": 20 })],
"backlog should collapse to the latest value"
);
}
#[test]
fn compressed_late_joiner_collapses_backlog_to_latest() {
let (mut producer, track) = producer(cfg_deflate(100));
for n in 0..=20 {
producer.update(&json!({ "n": n })).unwrap();
}
producer.finish().unwrap();
assert_eq!(track.latest(), Some(0));
let values = drain_with(deflate_consumer(track));
assert_eq!(
values,
vec![json!({ "n": 20 })],
"compressed backlog should collapse to the latest"
);
}
#[test]
fn compressed_snapshot_per_group_roundtrips() {
let (mut producer, track) = producer(cfg_deflate(0));
producer.update(&json!({ "a": 1 })).unwrap();
producer.update(&json!({ "a": 2 })).unwrap();
producer.finish().unwrap();
assert_eq!(track.latest(), Some(1));
let values = drain_with(deflate_consumer(track));
assert_eq!(values, vec![json!({ "a": 2 })]);
}
#[test]
fn compressed_deltas_share_one_group() {
let (mut producer, track) = producer(cfg_deflate(100));
producer.update(&json!({ "a": 1, "b": 1 })).unwrap();
producer.update(&json!({ "a": 1, "b": 2 })).unwrap();
producer.update(&json!({ "a": 1, "b": 3 })).unwrap();
producer.finish().unwrap();
assert_eq!(track.latest(), Some(0));
let values = drain_with(deflate_consumer(track));
assert_eq!(values.last().unwrap(), &json!({ "a": 1, "b": 3 }));
}
#[test]
fn compressed_late_joiner_reconstructs_from_deltas() {
let (mut producer, track) = producer(cfg_deflate(100));
producer.update(&json!({ "a": 1, "b": 1 })).unwrap();
producer.update(&json!({ "a": 1, "b": 2 })).unwrap();
producer.update(&json!({ "a": 5, "b": 2 })).unwrap();
producer.finish().unwrap();
let values = drain_with(deflate_consumer(track));
assert_eq!(values.last().unwrap(), &json!({ "a": 5, "b": 2 }));
}
#[test]
fn compressed_deltas_roll_on_compressed_budget() {
let (mut producer, track) = producer(cfg_deflate(2));
for n in 0..=40 {
producer.update(&json!({ "n": n })).unwrap();
}
producer.finish().unwrap();
assert!(
track.latest().unwrap() > 0,
"a tight ratio should roll at least one compressed group"
);
assert_eq!(drain_with(deflate_consumer(track)).last().unwrap(), &json!({ "n": 40 }));
}
#[test]
fn compressed_cloned_consumer_reconstructs_mid_group() {
let (mut producer, track) = producer(cfg_deflate(100));
let mut consumer = deflate_consumer(track);
let waiter = kio::Waiter::noop();
producer.update(&json!({ "a": 1, "b": 1 })).unwrap(); match consumer.poll_next(&waiter) {
Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "a": 1, "b": 1 })),
other => panic!("expected snapshot, got {other:?}"),
}
let mut clone = consumer.clone();
producer.update(&json!({ "a": 1, "b": 2 })).unwrap(); producer.finish().unwrap();
let expected = json!({ "a": 1, "b": 2 });
for consumer in [&mut consumer, &mut clone] {
match consumer.poll_next(&waiter) {
Poll::Ready(Ok(Some(value))) => assert_eq!(value, expected),
other => panic!("expected delta, got {other:?}"),
}
}
}
#[test]
fn compression_shrinks_wire_frames() {
let value = json!({ "renditions": ["video".repeat(50), "video".repeat(50), "video".repeat(50)] });
let plaintext_bytes = wire_frame_len(cfg(0), &value);
let compressed_bytes = wire_frame_len(cfg_deflate(0), &value);
assert!(
compressed_bytes < plaintext_bytes,
"compressed frame {compressed_bytes} should be smaller than plaintext {plaintext_bytes}"
);
}
#[test]
fn compressed_deltas_reuse_window() {
let (mut producer, mut track) = producer(cfg_deflate(100));
let phrase = "Media over QUIC delivers real-time latency at massive scale";
producer.update(&json!({ "note": phrase })).unwrap();
producer.update(&json!({ "note": phrase, "echo": phrase })).unwrap();
producer.finish().unwrap();
let waiter = kio::Waiter::noop();
let Poll::Ready(Ok(Some(mut group))) = track.poll_next_group(&waiter) else {
panic!("expected a group");
};
let mut frames = Vec::new();
while let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) {
frames.push(frame);
}
assert_eq!(frames.len(), 2, "snapshot + one delta in a single group");
let raw_delta = serde_json::to_vec(&json!({ "echo": phrase })).unwrap();
assert!(
frames[1].len() < raw_delta.len() / 2,
"windowed delta {} should be far below the raw patch {}",
frames[1].len(),
raw_delta.len()
);
}
fn wire_frame_len(config: ProducerConfig, value: &Value) -> usize {
let (mut producer, mut track) = producer(config);
producer.update(value).unwrap();
producer.finish().unwrap();
let waiter = kio::Waiter::noop();
let Poll::Ready(Ok(Some(mut group))) = track.poll_next_group(&waiter) else {
panic!("expected a group");
};
let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) else {
panic!("expected a frame");
};
frame.len()
}
}