Skip to main content

moq_json/window/
decoder.rs

1//! The track-free half of window consumption: frame payloads in, window events out.
2
3use std::collections::VecDeque;
4
5use serde::de::DeserializeOwned;
6
7use super::encoder::MAX_INDEX;
8use super::op::{Header, Op};
9use crate::{Error, Result};
10
11/// Configuration for a [`Decoder`], and so for the [`Consumer`](super::Consumer) wrapping one.
12#[derive(Debug, Clone, Default)]
13#[non_exhaustive]
14pub struct ConsumerConfig {
15	/// Read frames written with
16	/// [`ProducerConfig::compression`](super::ProducerConfig::compression) on.
17	pub compression: bool,
18}
19
20impl ConsumerConfig {
21	/// Set [`compression`](Self::compression) (a builder, since the struct is `#[non_exhaustive]`).
22	pub fn with_compression(mut self, compression: bool) -> Self {
23		self.compression = compression;
24		self
25	}
26}
27
28/// One change to the window, as the consumer sees it.
29///
30/// A record is `Push`ed when it first reaches this consumer. Contiguous ranges are `Pop`ped when
31/// they leave the window or `Skip`ped when they were dropped before this consumer saw them.
32#[derive(Debug, Clone, PartialEq)]
33#[non_exhaustive]
34pub enum Event<T> {
35	/// This record joined the window, at this absolute index.
36	Push {
37		/// Absolute index assigned to the record.
38		index: u64,
39		/// The decoded record.
40		value: T,
41	},
42
43	/// These records left the window.
44	Pop(std::ops::Range<u64>),
45
46	/// These records existed but will never be delivered: they were pushed and dropped while this
47	/// consumer was behind.
48	Skip(std::ops::Range<u64>),
49}
50
51/// An event ready to return, or a header's unseen records waiting to become push events.
52enum Queued<T> {
53	Event(Event<T>),
54	Push { index: u64, records: std::vec::IntoIter<T> },
55}
56
57/// Decodes one MoQ group's frames while borrowing the continuous window state.
58pub struct Group<'a, T> {
59	decoder: &'a mut Decoder<T>,
60	codec: Codec,
61}
62
63/// Group-local decoding state used by both [`Group`] and [`Consumer`](super::Consumer).
64pub(super) struct Codec {
65	/// The group's DEFLATE decoder, `Some` while reading compressed frames.
66	flate: Option<moq_flate::Decoder>,
67
68	/// Whether the required frame-zero header has been decoded.
69	positioned: bool,
70}
71
72impl Codec {
73	pub(super) fn new() -> Self {
74		Self {
75			flate: None,
76			positioned: false,
77		}
78	}
79}
80
81/// Reconstructs window events from frame payloads.
82///
83/// The track-free core of [`Consumer`](super::Consumer). It tracks indices, not contents: it knows
84/// where the window starts and how far it has delivered, which is all it needs to turn a header into
85/// the pushes, pops, and skips the reader has not already been told about.
86///
87/// Group rolls are invisible here on purpose. A header restates the window, and this decoder emits
88/// only what is new, so a reader sees one continuous stream of edits no matter how often the
89/// publisher rolled for compression's sake.
90pub struct Decoder<T> {
91	config: ConsumerConfig,
92
93	/// Absolute index of the window's front, once a group header has positioned us.
94	front: u64,
95
96	/// Records currently in the window.
97	len: u64,
98
99	/// Next index to deliver, or `None` before the first header. A fresh consumer adopts the first
100	/// header's offset rather than skipping everything that came before it.
101	delivered: Option<u64>,
102
103	/// Events produced by the frames decoded so far, oldest first.
104	events: VecDeque<Queued<T>>,
105}
106
107impl<T> Decoder<T> {
108	/// Create a decoder that has not yet been positioned by a group header.
109	pub fn new(config: ConsumerConfig) -> Self {
110		Self {
111			config,
112			front: 0,
113			len: 0,
114			delivered: None,
115			events: VecDeque::new(),
116		}
117	}
118
119	/// Borrow this decoder for one MoQ group.
120	pub fn group(&mut self) -> Group<'_, T> {
121		Group {
122			decoder: self,
123			codec: Codec::new(),
124		}
125	}
126
127	/// Take the next event produced by the frames decoded so far.
128	///
129	/// Returns `None` once the queue is drained, which is a request for more frames rather than the
130	/// end of anything: [`Group::decode`] refills it. Deliberately not [`Iterator`], whose
131	/// `None` a caller would reasonably read as exhausted.
132	pub fn next_event(&mut self) -> Option<Event<T>> {
133		match self.events.pop_front()? {
134			Queued::Event(event) => Some(event),
135			Queued::Push { index, mut records } => {
136				let value = records.next().expect("queued push batch is not empty");
137				if !records.as_slice().is_empty() {
138					self.events.push_front(Queued::Push {
139						index: index + 1,
140						records,
141					});
142				}
143				Some(Event::Push { index, value })
144			}
145		}
146	}
147
148	/// Absolute index of the oldest record in the window, and of the next to arrive.
149	pub fn range(&self) -> std::ops::Range<u64> {
150		self.front..self.front + self.len
151	}
152}
153
154impl<T: DeserializeOwned> Decoder<T> {
155	/// Decode one frame, queueing the events it implies.
156	pub(super) fn decode(&mut self, group: &mut Codec, payload: &[u8]) -> Result<()> {
157		let inflated = match self.config.compression {
158			true => Some(group.flate.get_or_insert_with(moq_flate::Decoder::new).frame(payload)?),
159			false => None,
160		};
161		let bytes = inflated.as_deref().unwrap_or(payload);
162
163		if !group.positioned {
164			let header: Header<T> = parse(bytes)?;
165			self.apply_header(header.offset, header.start.unwrap_or(header.offset), header.records)?;
166			group.positioned = true;
167			return Ok(());
168		}
169
170		match parse(bytes)? {
171			Op::Push(record) => self.apply_push(record),
172			Op::Pop(count) => self.apply_pop(count),
173		}
174	}
175
176	/// Apply a logical window range and its decodable suffix.
177	fn apply_header(&mut self, offset: u64, start: u64, records: Vec<T>) -> Result<()> {
178		if offset > MAX_INDEX {
179			return Err(Error::Json("window offset exceeds the safe integer range".into()));
180		}
181		if start < offset {
182			return Err(Error::Json("window checkpoint starts before its offset".into()));
183		}
184		let len = u64::try_from(records.len()).map_err(|_| Error::Json("window length exceeds u64".into()))?;
185		let end = start
186			.checked_add(len)
187			.filter(|end| *end <= MAX_INDEX)
188			.ok_or_else(|| Error::Json("window range exceeds the safe integer range".into()))?;
189
190		let delivered = match self.delivered {
191			// First position: adopt the publisher's offset rather than skipping all of history.
192			None => {
193				if offset < start {
194					self.events.push_back(Queued::Event(Event::Skip(offset..start)));
195				}
196				offset
197			}
198			Some(delivered) => {
199				if offset < self.front || end < delivered {
200					return Err(Error::Json("window header moved backwards".into()));
201				}
202
203				// Records that left the window while we were away. Those we had delivered are pops; those
204				// we never saw are skips. Keep each gap compact: the offset is untrusted and may jump by
205				// far more indices than a consumer could materialize individually.
206				let popped = self.front..delivered.min(offset);
207				if !popped.is_empty() {
208					self.events.push_back(Queued::Event(Event::Pop(popped)));
209				}
210				let skipped = delivered..start;
211				if !skipped.is_empty() {
212					self.events.push_back(Queued::Event(Event::Skip(skipped)));
213				}
214				delivered
215			}
216		};
217
218		// Keep the unseen tail as one batch and materialize each push only when the caller asks for it.
219		let skip = usize::try_from(delivered.saturating_sub(start))
220			.map_err(|_| Error::Json("window length exceeds usize".into()))?;
221		let mut records = records.into_iter();
222		if skip > 0 {
223			records.nth(skip - 1);
224		}
225		if !records.as_slice().is_empty() {
226			self.events.push_back(Queued::Push {
227				index: start + skip as u64,
228				records,
229			});
230		}
231
232		self.front = offset;
233		self.len = end - offset;
234		self.delivered = Some(delivered.max(end));
235		Ok(())
236	}
237
238	/// One record joined the back.
239	fn apply_push(&mut self, record: T) -> Result<()> {
240		let delivered = self.delivered.expect("group header positioned the decoder");
241
242		let index = self
243			.front
244			.checked_add(self.len)
245			.ok_or_else(|| Error::Json("window range exceeds u64".into()))?;
246		let end = index
247			.checked_add(1)
248			.filter(|end| *end <= MAX_INDEX)
249			.ok_or_else(|| Error::Json("window range exceeds the safe integer range".into()))?;
250		self.len = end - self.front;
251
252		if index >= delivered {
253			self.events
254				.push_back(Queued::Event(Event::Push { index, value: record }));
255			self.delivered = Some(end);
256		}
257
258		Ok(())
259	}
260
261	/// Records left the front.
262	fn apply_pop(&mut self, count: u64) -> Result<()> {
263		let delivered = self.delivered.expect("group header positioned the decoder");
264		if count > self.len {
265			return Err(Error::Json(format!(
266				"pop of {count} exceeds the {} record(s) in the window",
267				self.len
268			)));
269		}
270
271		let end = self
272			.front
273			.checked_add(count)
274			.ok_or_else(|| Error::Json("window range exceeds u64".into()))?;
275		let popped = self.front..delivered.min(end);
276		if !popped.is_empty() {
277			self.events.push_back(Queued::Event(Event::Pop(popped)));
278		}
279		let skipped = delivered.max(self.front)..end;
280		if !skipped.is_empty() {
281			self.events.push_back(Queued::Event(Event::Skip(skipped)));
282		}
283
284		self.front = end;
285		self.len -= count;
286		self.delivered = Some(delivered.max(self.front));
287
288		Ok(())
289	}
290}
291
292/// Deserialize one whole frame, naming the JSON path of any failure.
293fn parse<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
294	// Tracking the path allocates for every key walked, which dwarfed the decode itself, so it only
295	// runs again to explain a failure. Trailing data has no path, so it keeps the plain error.
296	serde_json::from_slice(bytes).map_err(|err| {
297		let tracked = serde_path_to_error::deserialize::<_, T>(&mut serde_json::Deserializer::from_slice(bytes));
298		Error::Json(tracked.err().map_or_else(|| err.to_string(), |err| err.to_string()))
299	})
300}
301
302impl<T> Group<'_, T> {
303	/// Take the next event produced by this group's frames so far.
304	pub fn next_event(&mut self) -> Option<Event<T>> {
305		self.decoder.next_event()
306	}
307
308	/// Absolute index of the oldest record in the window, and of the next to arrive.
309	pub fn range(&self) -> std::ops::Range<u64> {
310		self.decoder.range()
311	}
312}
313
314impl<T: DeserializeOwned> Group<'_, T> {
315	/// Decode the next frame in this group.
316	pub fn decode(&mut self, payload: &[u8]) -> Result<()> {
317		self.decoder.decode(&mut self.codec, payload)
318	}
319}