Skip to main content

moq_json/stream/
decoder.rs

1//! The track-free half of stream consuming: frame payloads in, records out.
2
3use std::marker::PhantomData;
4
5use serde::de::DeserializeOwned;
6
7use super::Config;
8use crate::Result;
9
10/// Decodes JSON records from frame payloads, sharing one DEFLATE window across the log.
11///
12/// The track-free core of [`Consumer`](super::Consumer), and the mirror of
13/// [`Encoder`](super::Encoder). Payloads must be fed in the order they were encoded, since each one
14/// builds on the window the earlier ones left behind. Call [`reset`](Self::reset) at a group
15/// boundary, matching the encoder.
16pub struct Decoder<T> {
17	/// The DEFLATE decoder (one window for the whole log), `Some` while decompressing.
18	flate: Option<moq_flate::Decoder>,
19	compression: bool,
20	_marker: PhantomData<fn() -> T>,
21}
22
23impl<T> Decoder<T> {
24	/// Create a decoder with a cold window.
25	pub fn new(config: Config) -> Self {
26		Self {
27			flate: config.compression.is_deflate().then(moq_flate::Decoder::new),
28			compression: config.compression.is_deflate(),
29			_marker: PhantomData,
30		}
31	}
32
33	/// Start a cold DEFLATE window, for a caller that has just moved to a new group.
34	pub fn reset(&mut self) {
35		self.flate = self.compression.then(moq_flate::Decoder::new);
36	}
37}
38
39impl<T: DeserializeOwned> Decoder<T> {
40	/// Decode the next frame payload back into a record.
41	pub fn decode(&mut self, payload: &[u8]) -> Result<T> {
42		Ok(match self.flate.as_mut() {
43			Some(flate) => serde_json::from_slice(&flate.frame(payload)?)?,
44			None => serde_json::from_slice(payload)?,
45		})
46	}
47}
48
49#[cfg(test)]
50mod test {
51	use super::super::{Config, Encoder};
52	use super::*;
53	use crate::Compression;
54	use serde_json::{Value, json};
55
56	fn cfg(compression: bool) -> Config {
57		Config {
58			compression: if compression {
59				Compression::Deflate
60			} else {
61				Compression::None
62			},
63		}
64	}
65
66	/// Round-trip a sequence of records through an encoder and decoder.
67	fn roundtrip(compression: bool, values: &[Value]) -> Vec<Value> {
68		let mut encoder = Encoder::<Value>::new(cfg(compression));
69		let mut decoder = Decoder::<Value>::new(cfg(compression));
70
71		values
72			.iter()
73			.map(|value| {
74				let record = encoder.encode(value).unwrap();
75				let decoded = decoder.decode(record.payload()).unwrap();
76				record.commit();
77				decoded
78			})
79			.collect()
80	}
81
82	#[test]
83	fn plaintext_roundtrip_in_order() {
84		let values: Vec<Value> = (0..5).map(|n| json!({ "n": n })).collect();
85		assert_eq!(roundtrip(false, &values), values);
86	}
87
88	#[test]
89	fn compressed_roundtrip_in_order() {
90		let values: Vec<Value> = (0..20).map(|n| json!({ "group": n, "pts": n * 2_000 })).collect();
91		assert_eq!(roundtrip(true, &values), values);
92	}
93
94	#[test]
95	fn the_shared_window_shrinks_repetitive_records() {
96		let mut encoder = Encoder::<Value>::new(cfg(true));
97		let sizes: Vec<usize> = (0..8)
98			.map(|n| {
99				let record = encoder.encode(&json!({ "group": n, "pts": n * 2_000 })).unwrap();
100				let len = record.payload().len();
101				record.commit();
102				len
103			})
104			.collect();
105
106		let raw = serde_json::to_vec(&json!({ "group": 7, "pts": 14_000 })).unwrap().len();
107		assert!(
108			*sizes.last().unwrap() < raw / 2,
109			"windowed record {} should be far below its raw size {raw}",
110			sizes.last().unwrap()
111		);
112	}
113
114	/// A caller that rolls a group has to restart both windows, or the new group's frames decode
115	/// against context the decoder on the other side never received.
116	#[test]
117	fn reset_starts_a_cold_window_on_both_sides() {
118		let mut encoder = Encoder::<Value>::new(cfg(true));
119		let mut decoder = Decoder::<Value>::new(cfg(true));
120
121		for n in 0..4 {
122			let record = encoder.encode(&json!({ "n": n })).unwrap();
123			assert_eq!(decoder.decode(record.payload()).unwrap(), json!({ "n": n }));
124			record.commit();
125		}
126
127		encoder.reset();
128		decoder.reset();
129
130		let record = encoder.encode(&json!({ "n": 99 })).unwrap();
131		assert_eq!(decoder.decode(record.payload()).unwrap(), json!({ "n": 99 }));
132		record.commit();
133	}
134}
135
136#[cfg(test)]
137mod desync_test {
138	use super::super::{Config, Encoder};
139	use super::*;
140	use crate::Compression;
141	use serde_json::{Value, json};
142
143	fn deflate() -> Config {
144		Config {
145			compression: Compression::Deflate,
146		}
147	}
148
149	/// A compressed record that never reached the wire leaves the window ahead of the consumer, and a
150	/// log has no keyframe to resynchronize on. Continuing would emit frames nothing can decode, so
151	/// the encoder refuses until the caller rolls a new group and resets.
152	#[test]
153	fn an_uncommitted_compressed_record_stops_the_encoder() {
154		let mut encoder = Encoder::<Value>::new(deflate());
155		encoder.encode(&json!({ "n": 0 })).unwrap().commit();
156
157		// This one fails to write, so the caller never commits it.
158		drop(encoder.encode(&json!({ "n": 1 })).unwrap());
159
160		assert!(matches!(encoder.encode(&json!({ "n": 2 })), Err(crate::Error::Desync)));
161
162		// Rolling a new group gives the consumer a cold window too, so the reset clears it.
163		encoder.reset();
164		let record = encoder.encode(&json!({ "n": 2 })).unwrap();
165		let mut decoder = Decoder::<Value>::new(deflate());
166		assert_eq!(decoder.decode(record.payload()).unwrap(), json!({ "n": 2 }));
167		record.commit();
168	}
169
170	/// Without compression a record carries no shared state, so a dropped one leaves a gap in the log
171	/// rather than an undecodable stream, and the encoder keeps going.
172	#[test]
173	fn an_uncommitted_plaintext_record_does_not_stop_the_encoder() {
174		let mut encoder = Encoder::<Value>::new(Config::default());
175		drop(encoder.encode(&json!({ "n": 0 })).unwrap());
176
177		let record = encoder
178			.encode(&json!({ "n": 1 }))
179			.expect("plaintext records are independent");
180		let mut decoder = Decoder::<Value>::new(Config::default());
181		assert_eq!(decoder.decode(record.payload()).unwrap(), json!({ "n": 1 }));
182		record.commit();
183	}
184}