Skip to main content

moq_json/snapshot/
decoder.rs

1//! The track-free half of snapshot consuming: frame payloads in, values out.
2
3use std::cell::RefCell;
4use std::marker::PhantomData;
5
6use serde::de::DeserializeOwned;
7use serde_json::Value;
8
9use super::consumer::Config;
10use crate::{Error, Result};
11
12/// Reconstructs a JSON value from the snapshot and delta frames of a group.
13///
14/// The track-free core of [`Consumer`](super::Consumer), and the mirror of
15/// [`Encoder`](super::Encoder). The caller reads frames from wherever it likes and routes each one
16/// by its position in the group: the first frame of every group is a
17/// [`snapshot`](Self::snapshot), the rest are [`delta`](Self::delta)s.
18///
19/// ```ignore
20/// match frame.keyframe {
21///     true => decoder.snapshot(&frame.payload)?,
22///     false => decoder.delta(&frame.payload)?,
23/// }
24/// let value = decoder.decode()?;
25/// ```
26///
27/// Applying and materializing are separate on purpose. Frames must be applied in order (the merge
28/// patches and the DEFLATE window are both sequential), but a consumer catching up on a backlog only
29/// wants the value at the head, so it applies every frame and calls [`decode`](Self::decode) once.
30/// A caller that wants a value per frame just calls it every time.
31pub struct Decoder<T> {
32	/// Whether frames are DEFLATE-compressed, matching the encoder's config.
33	compression: bool,
34
35	/// The current group's DEFLATE decoder (one window per group), rebuilt at each snapshot.
36	flate: Option<moq_flate::Decoder>,
37
38	/// Reused output for inflated delta frames.
39	plain: Vec<u8>,
40
41	/// Reused key buffers for validating remote patches before changing the baseline.
42	check: RefCell<crate::merge::CheckScratch>,
43
44	/// The reconstructed value, `None` until the first snapshot.
45	current: Option<Value>,
46
47	_marker: PhantomData<fn() -> T>,
48}
49
50impl<T> Decoder<T> {
51	/// Create a decoder with no value, awaiting its first [`snapshot`](Self::snapshot).
52	pub fn new(config: Config) -> Self {
53		Self {
54			compression: config.compression.is_deflate(),
55			flate: None,
56			plain: Vec::new(),
57			check: RefCell::new(crate::merge::CheckScratch::default()),
58			current: None,
59			_marker: PhantomData,
60		}
61	}
62
63	/// Apply a group's first frame: a full snapshot that replaces the current value.
64	///
65	/// Also starts the group's DEFLATE window, so this must be called at every group boundary, not
66	/// only the first.
67	pub fn snapshot(&mut self, payload: &[u8]) -> Result<()> {
68		// Each group is its own compressed stream, so the window starts cold here.
69		self.flate = self.compression.then(moq_flate::Decoder::new);
70		self.current = Some(match self.flate.as_mut() {
71			Some(flate) => serde_json::from_slice(&flate.frame(payload)?)?,
72			None => serde_json::from_slice(payload)?,
73		});
74		Ok(())
75	}
76
77	/// Apply one of a group's later frames: an [RFC 7396](https://www.rfc-editor.org/rfc/rfc7396.html)
78	/// merge patch against the current value.
79	///
80	/// Errors with [`Error::MissingSnapshot`] when no snapshot has been applied yet, since a patch
81	/// has nothing to apply to.
82	pub fn delta(&mut self, payload: &[u8]) -> Result<()> {
83		if self.current.is_none() {
84			return Err(Error::MissingSnapshot);
85		}
86
87		let plain = match self.flate.as_mut() {
88			Some(flate) => {
89				flate.frame_into(payload, &mut self.plain)?;
90				self.plain.as_slice()
91			}
92			None => payload,
93		};
94		crate::merge::apply_bytes(
95			self.current.as_mut().expect("a snapshot precedes any delta"),
96			plain,
97			&self.check,
98		)?;
99		Ok(())
100	}
101
102	/// The reconstructed value as raw JSON, or `None` before the first snapshot.
103	pub fn value(&self) -> Option<&Value> {
104		self.current.as_ref()
105	}
106}
107
108impl<T: DeserializeOwned> Decoder<T> {
109	/// Materialize the reconstructed value as `T`, or `None` before the first snapshot.
110	///
111	/// Deserializing from the reconstructed [`Value`] rather than the frame bytes costs the line and
112	/// column a parse error would carry, so the error is prefixed with the JSON path of the offending
113	/// field instead. Without it a rejected field deep in a document reports only its own complaint,
114	/// with nothing to say where it came from.
115	pub fn decode(&self) -> Result<Option<T>> {
116		let Some(current) = self.current.as_ref() else {
117			return Ok(None);
118		};
119
120		// Tracking the path allocates for every key walked, which dwarfed the decode itself on large
121		// documents, so it only runs again to explain a failure.
122		if let Ok(value) = T::deserialize(current) {
123			return Ok(Some(value));
124		}
125
126		let value = serde_path_to_error::deserialize(current).map_err(|err| {
127			let path = err.path().to_string();
128			match path.as_str() {
129				// The whole document, not a field within it: nothing useful to prefix.
130				"." => Error::Json(err.into_inner().to_string()),
131				_ => Error::Json(format!("{}: {}", path, err.into_inner())),
132			}
133		})?;
134
135		Ok(Some(value))
136	}
137}
138
139#[cfg(test)]
140mod test {
141	use super::super::consumer::Config as ConsumerConfig;
142	use super::super::{Config, Encoder};
143	use super::*;
144	use crate::Compression;
145	use serde_json::{Value, json};
146
147	fn consume(compression: Compression) -> ConsumerConfig {
148		ConsumerConfig { compression }
149	}
150
151	fn deflate() -> Config {
152		Config {
153			compression: Compression::Deflate,
154			..Default::default()
155		}
156	}
157
158	/// Round-trip a sequence of values through an encoder and decoder, yielding the value the
159	/// decoder reconstructs after each frame.
160	fn roundtrip(config: Config, values: &[Value]) -> Vec<Value> {
161		let compression = config.compression;
162		let mut encoder = Encoder::<Value>::new(config);
163		let mut decoder = Decoder::<Value>::new(consume(compression));
164
165		let mut out = Vec::new();
166		for value in values {
167			let Some(frame) = encoder.update(value).unwrap() else {
168				continue;
169			};
170			match frame.keyframe {
171				true => decoder.snapshot(&frame.payload).unwrap(),
172				false => decoder.delta(&frame.payload).unwrap(),
173			}
174			frame.commit();
175			out.push(decoder.decode().unwrap().unwrap());
176		}
177		out
178	}
179
180	#[test]
181	fn plaintext_roundtrip() {
182		let values = vec![
183			json!({ "a": 1, "b": 1 }),
184			json!({ "a": 1, "b": 2 }),
185			json!({ "a": 5, "b": 2 }),
186		];
187		assert_eq!(roundtrip(Config::default(), &values), values);
188	}
189
190	#[test]
191	fn compressed_roundtrip() {
192		let values = vec![
193			json!({ "a": 1, "b": 1 }),
194			json!({ "a": 1, "b": 2 }),
195			json!({ "a": 5, "b": 2 }),
196		];
197		assert_eq!(roundtrip(deflate(), &values), values);
198	}
199
200	/// The window is per group, so a keyframe mid-stream has to restart it on both sides. A decoder
201	/// that kept the old window here would fail to inflate the new group's snapshot.
202	#[test]
203	fn compressed_roundtrip_across_a_group_boundary() {
204		// A tight ratio guarantees at least one roll partway through.
205		let values: Vec<Value> = (0..=40).map(|n| json!({ "n": n })).collect();
206		let config = deflate().with_delta_ratio(2);
207		assert_eq!(roundtrip(config, &values).last().unwrap(), &json!({ "n": 40 }));
208	}
209
210	#[test]
211	fn no_value_before_the_first_snapshot() {
212		let decoder = Decoder::<Value>::new(ConsumerConfig::default());
213		assert_eq!(decoder.value(), None);
214		assert_eq!(decoder.decode().unwrap(), None);
215	}
216
217	#[test]
218	fn a_delta_before_a_snapshot_is_an_error() {
219		let mut decoder = Decoder::<Value>::new(ConsumerConfig::default());
220		assert!(matches!(decoder.delta(br#"{"a":1}"#), Err(Error::MissingSnapshot)));
221	}
222
223	/// A backlog is applied in full but materialized once: the intermediate reconstructions are
224	/// stale, and deserializing each one is exactly the cost the split exists to avoid.
225	#[test]
226	fn frames_apply_without_materializing() {
227		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(100));
228		let mut decoder = Decoder::<Value>::new(ConsumerConfig::default());
229
230		for n in 0..=20 {
231			let frame = encoder.update(&json!({ "n": n })).unwrap().unwrap();
232			match frame.keyframe {
233				true => decoder.snapshot(&frame.payload).unwrap(),
234				false => decoder.delta(&frame.payload).unwrap(),
235			}
236			frame.commit();
237		}
238
239		assert_eq!(decoder.decode().unwrap(), Some(json!({ "n": 20 })));
240	}
241
242	#[test]
243	fn a_rejected_field_names_its_path() {
244		#[derive(serde::Deserialize, Debug)]
245		#[allow(dead_code)]
246		struct Inner {
247			count: u8,
248		}
249		#[derive(serde::Deserialize, Debug)]
250		#[allow(dead_code)]
251		struct Outer {
252			inner: Inner,
253		}
254
255		let mut decoder = Decoder::<Outer>::new(ConsumerConfig::default());
256		decoder.snapshot(br#"{"inner":{"count":300}}"#).unwrap();
257
258		let err = decoder.decode().unwrap_err();
259		assert!(err.to_string().starts_with("json: inner.count: "), "{err}");
260	}
261}