use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use std::task::Poll;
use bytes::Bytes;
use moq_flate::{Decoder, Encoder};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::Result;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ProducerConfig {
pub compression: bool,
}
impl ProducerConfig {
pub fn with_compression(mut self, compression: bool) -> Self {
self.compression = compression;
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ConsumerConfig {
pub compression: bool,
}
impl ConsumerConfig {
pub fn with_compression(mut self, compression: bool) -> Self {
self.compression = compression;
self
}
}
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,
config,
})),
_marker: PhantomData,
}
}
pub fn append(&mut self, value: &T) -> Result<()> {
self.inner.lock().unwrap().append(value)
}
pub fn finish(&mut self) -> Result<()> {
self.inner.lock().unwrap().finish()
}
}
struct Inner {
track: moq_net::TrackProducer,
group: Option<moq_net::GroupProducer>,
encoder: Option<Encoder>,
config: ProducerConfig,
}
impl Inner {
fn append<T: Serialize>(&mut self, value: &T) -> Result<()> {
let payload = Bytes::from(serde_json::to_vec(value)?);
if self.group.is_none() {
self.group = Some(self.track.append_group()?);
self.encoder = self.config.compression.then(Encoder::new);
}
let slice = match self.encoder.as_mut() {
Some(encoder) => encoder.frame(&payload),
None => payload,
};
self.group.as_mut().expect("a group is open").write_frame(slice)?;
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>,
_marker: PhantomData<fn() -> T>,
}
impl<T: DeserializeOwned> Consumer<T> {
pub fn new(track: moq_net::TrackConsumer, config: ConsumerConfig) -> Self {
Self {
track,
group: None,
compressed: config.compression,
decoder: None,
_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>>> {
loop {
let Some(group) = &mut self.group else {
match self.track.poll_next_group(waiter)? {
Poll::Ready(Some(group)) => {
self.decoder = self.compressed.then(Decoder::new);
self.group = Some(group);
continue;
}
Poll::Ready(None) => return Poll::Ready(Ok(None)),
Poll::Pending => return Poll::Pending,
}
};
match group.poll_read_frame(waiter)? {
Poll::Ready(Some(frame)) => {
let plain = match self.decoder.as_mut() {
Some(decoder) => decoder.frame(&frame)?,
None => frame,
};
return Poll::Ready(Ok(Some(serde_json::from_slice(&plain)?)));
}
Poll::Ready(None) => {
self.group = None;
self.decoder = None;
}
Poll::Pending => return Poll::Pending,
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use serde_json::{Value, json};
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 compressed() -> ProducerConfig {
ProducerConfig { compression: true }
}
fn consumer(track: moq_net::TrackConsumer, compression: bool) -> Consumer<Value> {
Consumer::new(track, ConsumerConfig { compression })
}
fn drain(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 plaintext_roundtrip_in_order() {
let (mut producer, track) = producer(ProducerConfig::default());
for n in 0..5 {
producer.append(&json!({ "n": n })).unwrap();
}
producer.finish().unwrap();
let records = drain(consumer(track, false));
assert_eq!(records, (0..5).map(|n| json!({ "n": n })).collect::<Vec<_>>());
}
#[test]
fn compressed_roundtrip_in_order() {
let (mut producer, track) = producer(compressed());
for n in 0..20 {
producer.append(&json!({ "group": n, "pts": n * 2_000 })).unwrap();
}
producer.finish().unwrap();
let records = drain(consumer(track, true));
assert_eq!(records.len(), 20);
assert_eq!(records[7], json!({ "group": 7, "pts": 14_000 }));
}
#[test]
fn all_records_ride_one_group() {
let (mut producer, track) = producer(compressed());
for n in 0..50 {
producer.append(&json!({ "n": n })).unwrap();
}
producer.finish().unwrap();
assert_eq!(track.latest(), Some(0));
assert_eq!(drain(consumer(track, true)).len(), 50);
}
#[test]
fn live_consumer_sees_each_record() {
let (mut producer, track) = producer(compressed());
let mut consumer = consumer(track, true);
let waiter = kio::Waiter::noop();
for n in 0..3 {
producer.append(&json!({ "n": n })).unwrap();
match consumer.poll_next(&waiter) {
Poll::Ready(Ok(Some(value))) => assert_eq!(value, json!({ "n": n })),
other => panic!("expected record, got {other:?}"),
}
}
assert!(matches!(consumer.poll_next(&waiter), Poll::Pending));
producer.finish().unwrap();
}
#[test]
fn shared_window_shrinks_repetitive_records() {
let (mut producer, mut track) = producer(compressed());
for n in 0..8 {
producer.append(&json!({ "group": n, "pts": n * 2_000 })).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 sizes = Vec::new();
while let Poll::Ready(Ok(Some(frame))) = group.poll_read_frame(&waiter) {
sizes.push(frame.len());
}
assert_eq!(sizes.len(), 8);
let raw = serde_json::to_vec(&json!({ "group": 7, "pts": 14_000 })).unwrap().len();
assert!(
*sizes.last().unwrap() < raw / 2,
"windowed record {} should be far below its raw size {raw}",
sizes.last().unwrap()
);
}
#[test]
fn embedded_newlines_survive() {
let (mut producer, track) = producer(compressed());
let value = json!({ "s": "line1\nline2\ttab", "u": "a\u{000a}b" });
for _ in 0..4 {
producer.append(&value).unwrap();
}
producer.finish().unwrap();
let records = drain(consumer(track, true));
assert_eq!(records, vec![value.clone(), value.clone(), value.clone(), value]);
}
}