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> = serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(bytes))
165				.map_err(|err| Error::Json(err.to_string()))?;
166			self.apply_header(header.offset, header.start.unwrap_or(header.offset), header.records)?;
167			group.positioned = true;
168			return Ok(());
169		}
170
171		match serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_slice(bytes))
172			.map_err(|err| Error::Json(err.to_string()))?
173		{
174			Op::Push(record) => self.apply_push(record),
175			Op::Pop(count) => self.apply_pop(count),
176		}
177	}
178
179	/// Apply a logical window range and its decodable suffix.
180	fn apply_header(&mut self, offset: u64, start: u64, records: Vec<T>) -> Result<()> {
181		if offset > MAX_INDEX {
182			return Err(Error::Json("window offset exceeds the safe integer range".into()));
183		}
184		if start < offset {
185			return Err(Error::Json("window checkpoint starts before its offset".into()));
186		}
187		let len = u64::try_from(records.len()).map_err(|_| Error::Json("window length exceeds u64".into()))?;
188		let end = start
189			.checked_add(len)
190			.filter(|end| *end <= MAX_INDEX)
191			.ok_or_else(|| Error::Json("window range exceeds the safe integer range".into()))?;
192
193		let delivered = match self.delivered {
194			// First position: adopt the publisher's offset rather than skipping all of history.
195			None => {
196				if offset < start {
197					self.events.push_back(Queued::Event(Event::Skip(offset..start)));
198				}
199				offset
200			}
201			Some(delivered) => {
202				if offset < self.front || end < delivered {
203					return Err(Error::Json("window header moved backwards".into()));
204				}
205
206				// Records that left the window while we were away. Those we had delivered are pops; those
207				// we never saw are skips. Keep each gap compact: the offset is untrusted and may jump by
208				// far more indices than a consumer could materialize individually.
209				let popped = self.front..delivered.min(offset);
210				if !popped.is_empty() {
211					self.events.push_back(Queued::Event(Event::Pop(popped)));
212				}
213				let skipped = delivered..start;
214				if !skipped.is_empty() {
215					self.events.push_back(Queued::Event(Event::Skip(skipped)));
216				}
217				delivered
218			}
219		};
220
221		// Keep the unseen tail as one batch and materialize each push only when the caller asks for it.
222		let skip = usize::try_from(delivered.saturating_sub(start))
223			.map_err(|_| Error::Json("window length exceeds usize".into()))?;
224		let mut records = records.into_iter();
225		if skip > 0 {
226			records.nth(skip - 1);
227		}
228		if !records.as_slice().is_empty() {
229			self.events.push_back(Queued::Push {
230				index: start + skip as u64,
231				records,
232			});
233		}
234
235		self.front = offset;
236		self.len = end - offset;
237		self.delivered = Some(delivered.max(end));
238		Ok(())
239	}
240
241	/// One record joined the back.
242	fn apply_push(&mut self, record: T) -> Result<()> {
243		let delivered = self.delivered.expect("group header positioned the decoder");
244
245		let index = self
246			.front
247			.checked_add(self.len)
248			.ok_or_else(|| Error::Json("window range exceeds u64".into()))?;
249		let end = index
250			.checked_add(1)
251			.filter(|end| *end <= MAX_INDEX)
252			.ok_or_else(|| Error::Json("window range exceeds the safe integer range".into()))?;
253		self.len = end - self.front;
254
255		if index >= delivered {
256			self.events
257				.push_back(Queued::Event(Event::Push { index, value: record }));
258			self.delivered = Some(end);
259		}
260
261		Ok(())
262	}
263
264	/// Records left the front.
265	fn apply_pop(&mut self, count: u64) -> Result<()> {
266		let delivered = self.delivered.expect("group header positioned the decoder");
267		if count > self.len {
268			return Err(Error::Json(format!(
269				"pop of {count} exceeds the {} record(s) in the window",
270				self.len
271			)));
272		}
273
274		let end = self
275			.front
276			.checked_add(count)
277			.ok_or_else(|| Error::Json("window range exceeds u64".into()))?;
278		let popped = self.front..delivered.min(end);
279		if !popped.is_empty() {
280			self.events.push_back(Queued::Event(Event::Pop(popped)));
281		}
282		let skipped = delivered.max(self.front)..end;
283		if !skipped.is_empty() {
284			self.events.push_back(Queued::Event(Event::Skip(skipped)));
285		}
286
287		self.front = end;
288		self.len -= count;
289		self.delivered = Some(delivered.max(self.front));
290
291		Ok(())
292	}
293}
294
295impl<T> Group<'_, T> {
296	/// Take the next event produced by this group's frames so far.
297	pub fn next_event(&mut self) -> Option<Event<T>> {
298		self.decoder.next_event()
299	}
300
301	/// Absolute index of the oldest record in the window, and of the next to arrive.
302	pub fn range(&self) -> std::ops::Range<u64> {
303		self.decoder.range()
304	}
305}
306
307impl<T: DeserializeOwned> Group<'_, T> {
308	/// Decode the next frame in this group.
309	pub fn decode(&mut self, payload: &[u8]) -> Result<()> {
310		self.decoder.decode(&mut self.codec, payload)
311	}
312}