Skip to main content

moq_json/snapshot/
encoder.rs

1//! The track-free half of snapshot publishing: values in, frame payloads out.
2
3use std::cell::RefCell;
4use std::marker::PhantomData;
5use std::sync::OnceLock;
6
7use bytes::Bytes;
8use serde::Serialize;
9use serde_json::Value;
10
11use crate::{Compression, Result};
12
13/// Maximum frames (snapshot + deltas) in a single group before a new snapshot is forced.
14///
15/// Kept well below moq-net's per-group frame cap so a late joiner can always read the snapshot
16/// at frame 0 before the group is evicted.
17pub(super) const MAX_DELTA_FRAMES: usize = 256;
18
19/// What an [`Encoder`] keeps of the value it last emitted.
20///
21/// A delta is a diff against the previous value, so one has to be parsed to diff against
22/// whenever deltas are possible. With `delta_ratio = 0` none ever are, and the only question
23/// an update asks of the baseline is whether the value changed at all, which the encoded
24/// bytes answer directly. The parse is deferred in that case, and a value that is only ever
25/// published never pays for one.
26enum Baseline {
27	/// Deltas are possible, so the baseline is kept parsed and ready to diff against.
28	Parsed(Value),
29
30	/// Deltas are disabled. The emitted bytes (shared with the frame payload when not
31	/// compressing) stand in for the value, parsed only if a caller reads it back.
32	Encoded {
33		bytes: Bytes,
34		parsed: OnceLock<Option<Value>>,
35	},
36}
37
38impl Baseline {
39	/// The baseline as a parsed value, parsing the encoded bytes on first use.
40	fn value(&self) -> Option<&Value> {
41		match self {
42			Self::Parsed(value) => Some(value),
43			// Serialized by us, so this parses unless the caller's `Serialize` emitted
44			// something `serde_json` will not read back.
45			Self::Encoded { bytes, parsed } => parsed.get_or_init(|| serde_json::from_slice(bytes).ok()).as_ref(),
46		}
47	}
48}
49
50/// Codec options for an [`Encoder`], and so for the [`Producer`](super::Producer) wrapping one.
51///
52/// Build from [`Default`] and override fields (the struct is `#[non_exhaustive]`, so new
53/// options stay additive), or chain [`with_delta_ratio`](Self::with_delta_ratio).
54#[derive(Debug, Clone)]
55#[non_exhaustive]
56pub struct Config {
57	/// Controls how aggressively the encoder emits deltas (merge patches) instead of full snapshots.
58	///
59	/// A ratio of `0` disables deltas: every change is encoded as a new snapshot.
60	///
61	/// A positive ratio enables deltas. A new snapshot is emitted once the deltas *already written*
62	/// to the current group (excluding the snapshot frame) exceed `ratio` times the snapshot size.
63	/// The pending delta is excluded from that check, so the one that first crosses the budget
64	/// still lands before the group rolls. So `1` allows roughly one snapshot's worth of deltas before
65	/// rolling, and a larger ratio tolerates more.
66	///
67	/// When [`compression`](Self::compression) is [`Compression::Deflate`], both sides of the
68	/// comparison are measured on the *compressed* frame sizes (the real wire cost).
69	///
70	/// Defaults to `8`.
71	pub delta_ratio: u32,
72
73	/// Compress each group as one sync-flushed DEFLATE stream, so deltas reuse the snapshot as
74	/// context and shrink sharply.
75	///
76	/// [`Compression::None`] (the default) emits plaintext JSON frames, identical on the wire to an
77	/// uncompressed track. A [`Decoder`](super::Decoder) reading them must set the same
78	/// [`compression`](Self::compression).
79	pub compression: Compression,
80}
81
82impl Config {
83	/// Set [`delta_ratio`](Self::delta_ratio) (a builder, since the struct is `#[non_exhaustive]`).
84	pub fn with_delta_ratio(mut self, delta_ratio: u32) -> Self {
85		self.delta_ratio = delta_ratio;
86		self
87	}
88}
89
90impl Default for Config {
91	fn default() -> Self {
92		Self {
93			delta_ratio: 8,
94			compression: Compression::None,
95		}
96	}
97}
98
99/// One encoded frame, and the group boundary it implies.
100#[derive(Clone, Debug)]
101pub struct Encoded {
102	/// The frame payload, DEFLATE-compressed when [`Config::compression`] is [`Compression::Deflate`].
103	pub payload: Bytes,
104
105	/// Whether this frame is a full snapshot, which must open a new group.
106	///
107	/// `true` means the caller writes it as the first frame of a fresh group; `false` means it is a
108	/// merge patch that must be appended to the group the last snapshot opened. Mapping straight onto
109	/// [`moq_mux::container::Frame::keyframe`] is the point of the name.
110	///
111	/// The encoder decides this, never the caller: a value that sets a field to JSON null, or whose
112	/// root isn't an object, cannot be expressed as a merge patch at all, and the delta budget and
113	/// frame cap force a snapshot independently of what the caller wanted.
114	///
115	/// [`moq_mux::container::Frame::keyframe`]: https://docs.rs/moq-mux/latest/moq_mux/container/struct.Frame.html
116	pub keyframe: bool,
117}
118
119/// An encoded frame the caller has not yet acknowledged writing.
120///
121/// Returned by [`Encoder::update`]. Read [`payload`](Encoded::payload) and
122/// [`keyframe`](Encoded::keyframe) through the [`Deref`](std::ops::Deref) to [`Encoded`], write the
123/// frame, then [`commit`](Self::commit).
124///
125/// Dropping it uncommitted [`Encoder::reset`]s, so a frame that never reached the wire leaves the
126/// encoder resynchronizing with a fresh snapshot rather than emitting deltas against a baseline no
127/// consumer received. Note that this is a recovery, not a rollback: producing a delta payload
128/// advances the group's DEFLATE window, and that can't be undone, so a snapshot is the only sound
129/// way back. Forgetting to commit a frame that *was* written is therefore merely wasteful (one
130/// redundant snapshot), never incorrect.
131#[must_use = "the frame must be written and committed, or dropped to resynchronize the encoder"]
132pub struct Pending<'a, T> {
133	encoder: &'a mut Encoder<T>,
134	encoded: Encoded,
135	committed: bool,
136}
137
138impl<T> Pending<'_, T> {
139	/// Acknowledge that the frame reached the wire, keeping the encoder's state.
140	///
141	/// Only call this once the write has actually succeeded. Committing a frame that failed to write
142	/// is the one thing that corrupts the stream.
143	pub fn commit(mut self) {
144		self.committed = true;
145	}
146}
147
148impl<T> std::ops::Deref for Pending<'_, T> {
149	type Target = Encoded;
150
151	fn deref(&self) -> &Encoded {
152		&self.encoded
153	}
154}
155
156impl<T> Drop for Pending<'_, T> {
157	fn drop(&mut self) {
158		if !self.committed {
159			self.encoder.reset();
160		}
161	}
162}
163
164/// Encodes a JSON value into frame payloads, choosing snapshots and deltas automatically.
165///
166/// The track-free core of [`Producer`](super::Producer): it decides *what bytes go in a frame* and
167/// *where the group boundaries fall*, and leaves writing them to the caller. Reach for it when
168/// something else already owns the track, for example a
169/// [`moq_mux::container::Producer`](https://docs.rs/moq-mux/latest/moq_mux/container/struct.Producer.html)
170/// that is also managing a timeline and a catalog estimate:
171///
172/// ```ignore
173/// if let Some(frame) = encoder.update(&value)? {
174///     container.write(moq_mux::container::Frame {
175///         timestamp,
176///         duration: None,
177///         payload: frame.payload.clone(),
178///         keyframe: frame.keyframe,
179///     })?; // an early return here drops `frame`, resetting the encoder
180///     frame.commit();
181/// }
182/// ```
183///
184/// Frames must reach the wire in the order they were encoded, and a frame with
185/// [`keyframe`](Encoded::keyframe) set must open a new group: both the merge patches and the
186/// group-scoped DEFLATE window depend on it. [`update`](Self::update) hands back a [`Pending`]
187/// rather than a bare [`Encoded`] so a frame that never reaches the wire can't silently desync the
188/// encoder: dropping it uncommitted [`reset`](Self::reset)s, and the next value is encoded as a
189/// fresh snapshot. Committing a frame you failed to write is the one way to corrupt the stream.
190///
191/// If the caller cuts a group for its own reasons (a `cut`, `seek`, or discontinuity), call
192/// [`reset`](Self::reset) directly so the next value opens the new group with a snapshot.
193pub struct Encoder<T> {
194	config: Config,
195
196	/// The last encoded value, the baseline every delta is diffed against. `None` until the first
197	/// snapshot, which is what makes that first [`update`](Self::update) a keyframe.
198	last: Option<Baseline>,
199
200	/// Reused key buffers for comparing unchanged fields without per-update allocations, and the
201	/// memoized root entries that let an unchanged entry skip the baseline walk.
202	scratch: RefCell<crate::diff::Scratch>,
203
204	/// The current group's DEFLATE encoder (one window per group), `Some` while compressing.
205	flate: Option<moq_flate::Encoder>,
206
207	/// Bytes of deltas emitted into the current group, excluding the snapshot frame. Compressed
208	/// slice sizes when compressing, raw patch sizes otherwise.
209	delta_bytes: u64,
210
211	/// Reference size the delta budget is measured against: the current group's snapshot frame.
212	/// Its compressed slice size when compressing, raw otherwise.
213	snapshot_len: u64,
214
215	/// Frames emitted into the current group, snapshot included.
216	group_frames: usize,
217
218	/// Whether the next frame has to be a full snapshot, because a frame was lost or the caller cut
219	/// the group. Kept separate from [`last`](Self::last) so a resync doesn't erase the value: that
220	/// field is also what [`Producer::modify`](super::Producer::modify) seeds an edit from, and dropping
221	/// it there would publish a document with every other field missing.
222	resync: bool,
223
224	_marker: PhantomData<fn(T)>,
225}
226
227impl<T> Encoder<T> {
228	/// Create an encoder with a cold baseline, so the first [`update`](Self::update) is a snapshot.
229	pub fn new(config: Config) -> Self {
230		Self {
231			config,
232			last: None,
233			scratch: RefCell::new(crate::diff::Scratch::memoized()),
234			flate: None,
235			delta_bytes: 0,
236			snapshot_len: 0,
237			group_frames: 0,
238			resync: false,
239			_marker: PhantomData,
240		}
241	}
242
243	/// The last encoded value, or `None` before the first snapshot.
244	///
245	/// This is the baseline the next delta is diffed against, which is what a caller editing the
246	/// value in place needs to start from.
247	///
248	/// With deltas disabled the baseline is held as the encoded bytes, so the first call parses
249	/// them; the result is cached, and callers that never read the value never pay for it.
250	pub fn value(&self) -> Option<&Value> {
251		self.last.as_ref()?.value()
252	}
253
254	/// Force the next [`update`](Self::update) to emit a full snapshot, even for an unchanged value.
255	///
256	/// Call this whenever the caller closes the current group behind the encoder's back (a
257	/// `cut`, a `seek`, a discontinuity). Without it the next value may be encoded as a delta
258	/// against a DEFLATE window and a baseline that the new group doesn't carry.
259	///
260	/// [`value`](Self::value) survives: the snapshot republishes it in full anyway, and it is what a
261	/// caller editing in place starts from.
262	pub fn reset(&mut self) {
263		self.flate = None;
264		self.delta_bytes = 0;
265		self.snapshot_len = 0;
266		self.group_frames = 0;
267		self.resync = true;
268	}
269}
270
271impl<T: Serialize> Encoder<T> {
272	/// Encode a new value, as a snapshot or a delta.
273	///
274	/// Returns `None` when the value is unchanged from the last one encoded, so nothing needs to be
275	/// written. Otherwise the frame comes back as a [`Pending`] the caller writes and then
276	/// [`commit`](Pending::commit)s; dropping it uncommitted resynchronizes the encoder.
277	pub fn update(&mut self, value: &T) -> Result<Option<Pending<'_, T>>> {
278		Ok(self.encode(value)?.map(|encoded| Pending {
279			encoder: self,
280			encoded,
281			committed: false,
282		}))
283	}
284
285	/// Encode a new value into a bare frame, advancing the encoder's state.
286	///
287	/// The state change is what [`Pending`] guards, so this stays private: every caller goes through
288	/// [`update`](Self::update) and has to say whether the frame reached the wire.
289	fn encode(&mut self, value: &T) -> Result<Option<Encoded>> {
290		// A lost frame, or a group the caller cut, leaves the consumer's state unknown. Re-seed with a
291		// full snapshot even when the value is unchanged, since the frame that carried it may never
292		// have landed.
293		if self.resync {
294			return self.snapshot(value).map(Some);
295		}
296
297		// With deltas disabled there is nothing to diff, so the only question is whether the value
298		// changed: compare the encodings rather than parsing a baseline to diff against. The bytes
299		// are handed straight to the snapshot when it did change, so an update still serializes
300		// `T` exactly once.
301		if let Some(Baseline::Encoded { bytes, .. }) = self.last.as_ref() {
302			let bytes = bytes.clone();
303			let next = serde_json::to_vec(value)?;
304			if next.as_slice() == bytes.as_ref() {
305				return Ok(None);
306			}
307			return self.snapshot_encoded(next).map(Some);
308		}
309
310		// The first update has no baseline to diff against, so it seeds the stream with a snapshot.
311		let Some(Baseline::Parsed(last)) = self.last.as_ref() else {
312			return self.snapshot(value).map(Some);
313		};
314
315		// Diff straight off `T`, without building a full `Value` for the new value first.
316		let crate::diff::PatchBytes { patch, forced_snapshot } =
317			crate::diff::bytes(last, value, &self.scratch).map_err(crate::Error::Json)?;
318
319		// An empty object patch with no forced null means the value is unchanged: encode nothing.
320		if !forced_snapshot && patch.is_empty() {
321			self.scratch.get_mut().commit_memo();
322			return Ok(None);
323		}
324
325		// A forced snapshot (a genuine null, or a non-object root) or an exhausted delta budget starts a
326		// new group; otherwise the change rides as a delta in the open one.
327		if forced_snapshot || !self.delta_allowed() {
328			return self.snapshot(value).map(Some);
329		}
330
331		// Compress into the per-group window only now, for a frame we are committed to emitting.
332		let bytes = Bytes::from(patch);
333
334		// Same cap as a snapshot, on the patch's plaintext: a delta that decompresses past the
335		// consumer's limit makes the whole group unreadable, since there is no keyframe after it to
336		// resynchronize on. Rejecting here leaves the encoder to reset and the group as it was.
337		if self.config.compression.is_deflate() && bytes.len() as u64 > moq_flate::DEFAULT_MAX_FRAME_SIZE {
338			return Err(moq_flate::Error::TooLarge(moq_flate::DEFAULT_MAX_FRAME_SIZE).into());
339		}
340		let payload = match self.flate.as_mut() {
341			Some(flate) => flate.frame(&bytes),
342			None => bytes.clone(),
343		};
344
345		// A delta is only readable while the group still holds the snapshot it applies to.
346		// Admitting a patch that pushes the group past that budget would abort it
347		// (`GroupTooLarge`), leaving a late subscriber with no value. Roll a fresh snapshot
348		// instead, which is cheap next to losing the value.
349		//
350		// Measured on the encoded payload rather than the plaintext: a sync-flushed DEFLATE frame can
351		// come out slightly larger than its input, so the plaintext is not an upper bound. Compressing
352		// first advances the window, but [`Self::snapshot`] opens a fresh one, so an over-budget delta
353		// costs only the wasted compression.
354		if self.snapshot_len + self.delta_bytes + payload.len() as u64 > moq_net::group::MAX_CACHE_BYTES {
355			return self.snapshot(value).map(Some);
356		}
357
358		self.delta_bytes += payload.len() as u64;
359		self.group_frames += 1;
360
361		// Fold the delta into the baseline so the next diff is against the value we just encoded.
362		// Reaching a delta means `delta_allowed`, which means a non-zero ratio, which is what keeps
363		// the baseline parsed.
364		let Some(Baseline::Parsed(last)) = self.last.as_mut() else {
365			unreachable!("a parsed snapshot precedes any delta")
366		};
367		crate::merge::apply_generated_bytes(last, &bytes)?;
368		self.scratch.get_mut().commit_memo();
369
370		Ok(Some(Encoded {
371			payload,
372			keyframe: false,
373		}))
374	}
375
376	/// Whether the current change may ride as a delta in the open group.
377	///
378	/// The budget gate measures the deltas *already emitted* (excluding the frame about to land)
379	/// against the group's snapshot frame. Both are compressed sizes when compressing and raw
380	/// otherwise, so the comparison is like-for-like. Because the pending frame is excluded, the delta
381	/// that tips the group past `ratio * snapshot` still lands: a group overshoots by at most one delta
382	/// before rolling.
383	fn delta_allowed(&self) -> bool {
384		let ratio = u64::from(self.config.delta_ratio);
385		ratio != 0
386			&& self.group_frames > 0
387			&& self.group_frames < MAX_DELTA_FRAMES
388			&& self.delta_bytes <= ratio * self.snapshot_len
389	}
390
391	/// Encode a full snapshot of `value`, opening a new group and reseeding the baseline.
392	fn snapshot(&mut self, value: &T) -> Result<Encoded> {
393		// Serialize directly from `value` so the snapshot frame preserves the type's own field order,
394		// keeping the wire bytes identical to serializing `T` straight to a frame.
395		let snapshot = serde_json::to_vec(value)?;
396		self.snapshot_encoded(snapshot)
397	}
398
399	/// [`snapshot`](Self::snapshot) for a value that is already serialized, so an update that
400	/// encoded `T` to compare it against a byte baseline does not encode it a second time.
401	fn snapshot_encoded(&mut self, snapshot: Vec<u8>) -> Result<Encoded> {
402		// Every consumer decodes with moq-flate's default output cap, so a value past it would be
403		// unreadable however small it compresses to. Reject it before anything is published, so the
404		// previously published value stands rather than being superseded by one nothing can read.
405		if self.config.compression.is_deflate() && snapshot.len() as u64 > moq_flate::DEFAULT_MAX_FRAME_SIZE {
406			return Err(moq_flate::Error::TooLarge(moq_flate::DEFAULT_MAX_FRAME_SIZE).into());
407		}
408
409		// With deltas possible, read the baseline back out of those same bytes rather than
410		// serializing `value` a second time, so the baseline IS the emitted snapshot by
411		// construction. A `Serialize` impl reading a clock or interior mutable state would otherwise
412		// seed the baseline with a value no consumer ever received, and every later delta would
413		// rebase them onto it. `T` is also only visited once, which is what a caller with an
414		// expensive or effectful `Serialize` pays for.
415		//
416		// That trades a second walk of `T` for a parse of the bytes, so it is not automatically
417		// cheaper than `to_value` (see the `baseline` benchmark); consistency is the reason. With
418		// deltas off there is no diff to rebase and no reason to pay it at all.
419		//
420		// Every fallible step runs before any state changes, so a failure leaves the encoder exactly
421		// as it was rather than half-advanced with no frame to show for it.
422		let snapshot = Bytes::from(snapshot);
423		let last = if self.config.delta_ratio == 0 {
424			// No delta will ever diff against this, so hold the bytes instead. Uncompressed, they are
425			// the same allocation the payload carries, so the baseline costs a refcount.
426			Baseline::Encoded {
427				bytes: snapshot.clone(),
428				parsed: OnceLock::new(),
429			}
430		} else {
431			Baseline::Parsed(serde_json::from_slice(&snapshot)?)
432		};
433
434		// Open a fresh per-group encoder (cold window) and compress the snapshot as frame 0, recording
435		// its wire size as the delta anchor.
436		let (payload, flate) = match self.config.compression {
437			Compression::Deflate => {
438				let mut flate = moq_flate::Encoder::new();
439				let payload = flate.frame(&snapshot);
440				(payload, Some(flate))
441			}
442			Compression::None => (snapshot, None),
443		};
444
445		self.snapshot_len = payload.len() as u64;
446		self.delta_bytes = 0;
447		self.group_frames = 1;
448		self.flate = flate;
449		self.last = Some(last);
450		self.resync = false;
451		// Seeded from the snapshot rather than a diff, so no root entry is memoized against it yet.
452		self.scratch.get_mut().clear_memo();
453
454		Ok(Encoded {
455			payload,
456			keyframe: true,
457		})
458	}
459}
460
461#[cfg(test)]
462mod test {
463	use super::*;
464	use serde_json::json;
465
466	#[test]
467	fn duplicate_serialized_keys_are_refused() {
468		use serde::ser::SerializeMap;
469		struct Duplicate {
470			duplicate: bool,
471		}
472		impl Serialize for Duplicate {
473			fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
474				let mut map = serializer.serialize_map(Some(2 + usize::from(self.duplicate)))?;
475				map.serialize_entry("a", &1)?;
476				map.serialize_entry("b", &2)?;
477				if self.duplicate {
478					map.serialize_entry("a", &3)?;
479				}
480				map.end()
481			}
482		}
483		let mut encoder = Encoder::<Duplicate>::new(Config::default());
484		encoder
485			.update(&Duplicate { duplicate: false })
486			.unwrap()
487			.unwrap()
488			.commit();
489		let err = encoder.encode(&Duplicate { duplicate: true }).unwrap_err();
490		assert!(err.to_string().contains("duplicate JSON object key"));
491	}
492
493	/// Encode a sequence of values, committing each frame, and return `(keyframe, payload_len)` per
494	/// emitted frame.
495	fn encode(config: Config, values: &[Value]) -> Vec<(bool, usize)> {
496		let mut encoder = Encoder::<Value>::new(config);
497		let mut out = Vec::new();
498		for value in values {
499			if let Some(frame) = encoder.update(value).unwrap() {
500				out.push((frame.keyframe, frame.payload.len()));
501				frame.commit();
502			}
503		}
504		out
505	}
506
507	/// Encode one value and commit it, returning the frame.
508	fn commit(encoder: &mut Encoder<Value>, value: &Value) -> Option<Encoded> {
509		let frame = encoder.update(value).unwrap()?;
510		let encoded = Encoded {
511			payload: frame.payload.clone(),
512			keyframe: frame.keyframe,
513		};
514		frame.commit();
515		Some(encoded)
516	}
517
518	#[test]
519	fn first_update_is_a_keyframe() {
520		let frames = encode(Config::default(), &[json!({ "a": 1 })]);
521		assert_eq!(frames.len(), 1);
522		assert!(frames[0].0);
523	}
524
525	#[test]
526	fn unchanged_value_encodes_nothing() {
527		let frames = encode(Config::default(), &[json!({ "a": 1 }), json!({ "a": 1 })]);
528		assert_eq!(frames.len(), 1);
529	}
530
531	#[test]
532	fn changes_ride_as_deltas() {
533		let frames = encode(
534			Config::default().with_delta_ratio(100),
535			&[
536				json!({ "a": 1, "b": 1 }),
537				json!({ "a": 1, "b": 2 }),
538				json!({ "a": 1, "b": 3 }),
539			],
540		);
541		assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, false, false]);
542	}
543
544	#[test]
545	fn deltas_off_forces_a_keyframe_per_change() {
546		let frames = encode(
547			Config::default().with_delta_ratio(0),
548			&[json!({ "a": 1 }), json!({ "a": 2 })],
549		);
550		assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
551	}
552
553	/// Deltas off keeps the baseline as bytes rather than a parsed value, so the unchanged check
554	/// runs on the encoding. It still has to suppress a republish, or every stats tick would
555	/// re-emit an identical frame.
556	#[test]
557	fn deltas_off_still_skips_an_unchanged_value() {
558		let frames = encode(
559			Config::default().with_delta_ratio(0),
560			&[json!({ "a": 1 }), json!({ "a": 1 }), json!({ "a": 1 })],
561		);
562		assert_eq!(frames.len(), 1);
563	}
564
565	/// Field order is part of the encoding, so a byte baseline only answers "unchanged" correctly
566	/// because `T` serializes deterministically. Same keys, different values, must still emit.
567	#[test]
568	fn deltas_off_detects_a_change_under_the_same_keys() {
569		let frames = encode(
570			Config::default().with_delta_ratio(0),
571			&[json!({ "a": 1, "b": 2 }), json!({ "a": 1, "b": 3 })],
572		);
573		assert_eq!(frames.len(), 2);
574	}
575
576	/// The byte baseline is parsed on demand, so `value` (and so `Producer::modify`, which seeds an
577	/// edit from it) keeps working with deltas off. Dropping the baseline instead would make
578	/// `modify` start from `T::default()` and publish a document with every other field missing.
579	#[test]
580	fn deltas_off_still_exposes_the_value() {
581		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(0));
582		assert_eq!(encoder.value(), None);
583
584		commit(&mut encoder, &json!({ "a": 1, "b": 2 })).unwrap();
585		assert_eq!(encoder.value(), Some(&json!({ "a": 1, "b": 2 })));
586
587		commit(&mut encoder, &json!({ "a": 1, "b": 3 })).unwrap();
588		assert_eq!(encoder.value(), Some(&json!({ "a": 1, "b": 3 })));
589	}
590
591	/// Compressing shares no allocation between the baseline and the payload, so the byte baseline
592	/// has to hold the plaintext rather than the compressed frame.
593	#[test]
594	fn deltas_off_while_compressing_keeps_the_plaintext_baseline() {
595		let mut config = Config::default().with_delta_ratio(0);
596		config.compression = Compression::Deflate;
597
598		let mut encoder = Encoder::<Value>::new(config);
599		commit(&mut encoder, &json!({ "a": 1 })).unwrap();
600		assert_eq!(encoder.value(), Some(&json!({ "a": 1 })));
601		assert!(commit(&mut encoder, &json!({ "a": 1 })).is_none());
602	}
603
604	/// A value the caller might reasonably expect to be a delta, but that merge patch can't express:
605	/// setting a field to JSON null reads as a key deletion. The encoder has to override the caller
606	/// here, which is why `keyframe` is a return value rather than a parameter.
607	#[test]
608	fn a_null_field_forces_a_keyframe() {
609		let frames = encode(
610			Config::default().with_delta_ratio(100),
611			&[json!({ "a": 1, "b": 1 }), json!({ "a": 1, "b": null })],
612		);
613		assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
614	}
615
616	/// Same story for a root that isn't an object: there is no recursive merge patch for it.
617	#[test]
618	fn a_non_object_root_forces_a_keyframe() {
619		let frames = encode(
620			Config::default().with_delta_ratio(100),
621			&[json!({ "a": 1 }), json!([1, 2, 3])],
622		);
623		assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
624	}
625
626	#[test]
627	fn frame_cap_forces_a_keyframe() {
628		let values: Vec<Value> = (0..=MAX_DELTA_FRAMES).map(|n| json!({ "n": n })).collect();
629		let frames = encode(Config::default().with_delta_ratio(1_000_000), &values);
630
631		// The snapshot plus MAX_DELTA_FRAMES - 1 deltas fill the group, then the cap rolls it.
632		assert_eq!(frames.len(), MAX_DELTA_FRAMES + 1);
633		assert_eq!(frames.iter().filter(|f| f.0).count(), 2);
634		assert!(frames[MAX_DELTA_FRAMES].0);
635	}
636
637	/// A caller that cuts the group behind the encoder's back has to say so, or the next value would
638	/// be a delta against a window and a baseline the new group never carried.
639	#[test]
640	fn reset_forces_the_next_update_to_be_a_keyframe() {
641		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(100));
642		assert!(commit(&mut encoder, &json!({ "a": 1 })).unwrap().keyframe);
643		assert!(!commit(&mut encoder, &json!({ "a": 2 })).unwrap().keyframe);
644
645		encoder.reset();
646		assert!(commit(&mut encoder, &json!({ "a": 3 })).unwrap().keyframe);
647	}
648
649	/// A frame the caller never wrote must not leave the encoder emitting deltas against a baseline
650	/// no consumer received. Dropping the [`Pending`] uncommitted is what a failed write looks like,
651	/// and it has to resynchronize on its own: a caller cannot be relied on to remember.
652	#[test]
653	fn an_uncommitted_frame_resynchronizes_the_encoder() {
654		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(100));
655		commit(&mut encoder, &json!({ "a": 1 })).unwrap();
656
657		// The caller wrote this one and said so, so the next value can still ride as a delta.
658		commit(&mut encoder, &json!({ "a": 2 })).unwrap();
659
660		// This one fails to write, so the caller drops it without committing.
661		drop(encoder.update(&json!({ "a": 3 })).unwrap().expect("a delta"));
662
663		// The next value opens a new group with a full snapshot rather than patching a state the
664		// consumer never reached.
665		let recovered = commit(&mut encoder, &json!({ "a": 4 })).expect("a resynchronizing snapshot");
666		assert!(recovered.keyframe);
667		assert_eq!(
668			serde_json::from_slice::<Value>(&recovered.payload).unwrap(),
669			json!({ "a": 4 }),
670			"the snapshot carries the whole value, not a patch"
671		);
672	}
673
674	/// The same recovery when the very first frame is lost: the encoder must not treat the value as
675	/// already published and skip it as unchanged.
676	#[test]
677	fn an_uncommitted_first_frame_is_reencoded() {
678		let mut encoder = Encoder::<Value>::new(Config::default());
679		drop(encoder.update(&json!({ "a": 1 })).unwrap().expect("a snapshot"));
680
681		let retried = commit(&mut encoder, &json!({ "a": 1 })).expect("the same value, re-encoded");
682		assert!(retried.keyframe);
683	}
684
685	/// A reset value is republished even when it matches the last one encoded: the new group has to
686	/// open with a snapshot, so "unchanged" can't mean "write nothing" there.
687	#[test]
688	fn reset_republishes_an_unchanged_value() {
689		let mut encoder = Encoder::<Value>::new(Config::default());
690		commit(&mut encoder, &json!({ "a": 1 })).unwrap();
691
692		encoder.reset();
693		assert!(
694			commit(&mut encoder, &json!({ "a": 1 }))
695				.expect("a fresh snapshot")
696				.keyframe
697		);
698	}
699
700	#[test]
701	fn compressed_deltas_reuse_the_group_window() {
702		let phrase = "Media over QUIC delivers real-time latency at massive scale";
703		let frames = encode(
704			Config {
705				delta_ratio: 100,
706				compression: Compression::Deflate,
707			},
708			&[json!({ "note": phrase }), json!({ "note": phrase, "echo": phrase })],
709		);
710
711		// The raw patch repeats the whole phrase; compressed against the window it's a fraction.
712		let raw = serde_json::to_vec(&json!({ "echo": phrase })).unwrap().len();
713		assert_eq!(frames.len(), 2);
714		assert!(
715			frames[1].1 < raw / 2,
716			"windowed delta {} vs raw patch {raw}",
717			frames[1].1
718		);
719	}
720
721	/// A value whose serialization changes on every call, standing in for a `Serialize` impl backed by
722	/// a clock, an atomic, or interior mutable state.
723	struct Ticking(std::cell::Cell<u32>);
724
725	impl serde::Serialize for Ticking {
726		fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
727			use serde::ser::SerializeMap;
728
729			let n = self.0.get();
730			self.0.set(n + 1);
731
732			let mut map = serializer.serialize_map(Some(1))?;
733			map.serialize_entry("n", &n)?;
734			map.end()
735		}
736	}
737
738	/// The snapshot frame and the baseline must come from a single pass over the value. Serializing
739	/// twice costs a second traversal, and for a value like this one it seeds the baseline with
740	/// something no consumer ever received, so every later delta rebases them onto a phantom state.
741	#[test]
742	fn a_snapshot_serializes_its_value_once() {
743		let value = Ticking(std::cell::Cell::new(0));
744		let mut encoder = Encoder::<Ticking>::new(Config::default());
745		let payload = {
746			let frame = encoder.update(&value).unwrap().expect("a snapshot");
747			let payload = frame.payload.clone();
748			frame.commit();
749			payload
750		};
751
752		assert_eq!(value.0.get(), 1, "the value should be serialized exactly once");
753
754		let emitted: Value = serde_json::from_slice(&payload).unwrap();
755		assert_eq!(emitted, json!({ "n": 0 }));
756		assert_eq!(encoder.value(), Some(&emitted), "the baseline must be what was emitted");
757	}
758
759	/// A root entry the memo has not seen yet is diffed from the bytes the memo recorded, not
760	/// serialized again: a second pass could disagree with the first, leaving the memo describing a
761	/// value the baseline never held.
762	#[test]
763	fn a_delta_serializes_each_entry_once() {
764		let value = std::collections::BTreeMap::from([("row", Ticking(std::cell::Cell::new(0)))]);
765		let mut encoder = Encoder::new(Config::default().with_delta_ratio(100));
766		encoder.update(&value).unwrap().expect("a snapshot").commit();
767
768		let frame = encoder.update(&value).unwrap().expect("a delta");
769		assert!(!frame.keyframe);
770		let emitted: Value = serde_json::from_slice(&frame.payload).unwrap();
771		frame.commit();
772
773		assert_eq!(
774			value["row"].0.get(),
775			2,
776			"each update should serialize the entry exactly once"
777		);
778		assert_eq!(emitted, json!({ "row": { "n": 1 } }));
779		assert_eq!(encoder.value(), Some(&emitted), "the baseline must be what was emitted");
780	}
781
782	/// A key repeated below the root is refused whether the memo meets it in a new entry or in a
783	/// value replaced wholesale, as the value diff refuses it. Letting one into the memo would pair
784	/// the repeats by position, where the consumer keeps the last.
785	#[test]
786	fn a_repeated_nested_key_is_refused_through_the_memo() {
787		use serde::ser::SerializeMap;
788
789		/// `{"row": {"o": ..}}`, where `o` is `1` or an object that repeats a key.
790		struct Doc {
791			repeat: bool,
792		}
793		struct Row<'a>(&'a Doc);
794		struct Repeat;
795
796		impl Serialize for Repeat {
797			fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
798				let mut map = serializer.serialize_map(Some(2))?;
799				map.serialize_entry("x", &1)?;
800				map.serialize_entry("x", &2)?;
801				map.end()
802			}
803		}
804		impl Serialize for Row<'_> {
805			fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
806				let mut map = serializer.serialize_map(Some(1))?;
807				match self.0.repeat {
808					true => map.serialize_entry("o", &Repeat)?,
809					false => map.serialize_entry("o", &1)?,
810				}
811				map.end()
812			}
813		}
814		impl Serialize for Doc {
815			fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
816				let mut map = serializer.serialize_map(Some(1))?;
817				map.serialize_entry("row", &Row(self))?;
818				map.end()
819			}
820		}
821
822		let config = Config::default().with_delta_ratio(100);
823		let (plain, repeat) = (Doc { repeat: false }, Doc { repeat: true });
824
825		// A new entry: the first diff after a snapshot has nothing memoized yet.
826		let mut encoder = Encoder::<Doc>::new(config.clone());
827		encoder.update(&plain).unwrap().expect("a snapshot").commit();
828		let err = encoder.encode(&repeat).unwrap_err();
829		assert!(err.to_string().contains("duplicate JSON object key"), "{err}");
830
831		// A memoized entry whose scalar becomes an object.
832		let mut encoder = Encoder::<Doc>::new(config);
833		encoder.update(&plain).unwrap().expect("a snapshot").commit();
834		assert!(encoder.update(&plain).unwrap().is_none(), "unchanged, now memoized");
835		let err = encoder.encode(&repeat).unwrap_err();
836		assert!(err.to_string().contains("duplicate JSON object key"), "{err}");
837	}
838
839	/// A root object whose entries serialize in the order given, sorted or not.
840	struct Rows(Vec<(String, Value)>);
841
842	impl Serialize for Rows {
843		fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
844			serializer.collect_map(self.0.iter().map(|(key, value)| (key, value)))
845		}
846	}
847
848	/// A deterministic xorshift, so a failure replays.
849	struct Rng(u64);
850
851	impl Rng {
852		fn below(&mut self, n: u64) -> u64 {
853			self.0 ^= self.0 << 13;
854			self.0 ^= self.0 >> 7;
855			self.0 ^= self.0 << 17;
856			self.0 % n
857		}
858
859		/// A row value covering what the memo has to get right: nested objects that gain and lose
860		/// keys, values that change type, nulls in and out of arrays, and strings that look like JSON.
861		fn row(&mut self) -> Value {
862			let strings = ["plain", "q\"uote", "back\\slash", "},{\"x\":1", "null", "a:b,c"];
863			let mut row = serde_json::Map::new();
864			row.insert(
865				"n".into(),
866				match self.below(30) {
867					0 => Value::Null,
868					n => json!(n % 4),
869				},
870			);
871			if self.below(4) > 0 {
872				row.insert("s".into(), json!(strings[self.below(strings.len() as u64) as usize]));
873			}
874			let mut nested = serde_json::Map::new();
875			nested.insert("a".into(), json!(self.below(3)));
876			if self.below(3) == 0 {
877				nested.insert("b".into(), json!([self.below(2), null]));
878			}
879			if self.below(40) == 0 {
880				nested.insert("c".into(), Value::Null);
881			}
882			row.insert("o".into(), Value::Object(nested));
883			row.insert(
884				"t".into(),
885				match self.below(5) {
886					0 => json!({ "k": self.below(2) }),
887					1 => json!({}),
888					2 => json!([{ "k": null }]),
889					3 => json!(1.5 + self.below(2) as f64),
890					_ => json!("t"),
891				},
892			);
893			if self.below(60) == 0 {
894				row.insert("z".into(), Value::Null);
895			}
896			Value::Object(row)
897		}
898	}
899
900	/// The memo is a shortcut past the value diff, so it must never change a frame: every payload and
901	/// keyframe has to match an encoder diffing without it, through inserts, deletions, reorders,
902	/// shape changes, forced snapshots, and group rolls.
903	#[test]
904	fn memo_matches_the_value_diff() {
905		for (seed, compression) in [
906			(1, Compression::None),
907			(2, Compression::Deflate),
908			(3, Compression::None),
909		] {
910			let mut config = Config::default().with_delta_ratio(2);
911			config.compression = compression;
912			let mut memoized = Encoder::<Rows>::new(config.clone());
913			let mut plain = Encoder::<Rows>::new(config);
914			plain.scratch = RefCell::new(crate::diff::Scratch::default());
915
916			let mut rng = Rng(0x9E37_79B9_7F4A_7C15 ^ seed);
917			let mut rows: Vec<(String, Value)> = (0..40).map(|i| (format!("row-{i:03}"), rng.row())).collect();
918			let mut emitted = 0;
919			for tick in 0..400 {
920				for row in rows.iter_mut() {
921					if rng.below(4) == 0 {
922						row.1 = rng.row();
923					}
924				}
925				if rng.below(3) == 0 {
926					let index = rng.below(rows.len() as u64) as usize;
927					rows.remove(index);
928				}
929				if rng.below(3) == 0 {
930					rows.push((format!("row-{:03}", 40 + rng.below(40)), rng.row()));
931				}
932				rows.sort_by(|a, b| a.0.cmp(&b.0));
933				rows.dedup_by(|a, b| a.0 == b.0);
934				// Now and then, a root that stops ascending.
935				if seed == 3 && rng.below(10) == 0 {
936					let (a, b) = (
937						rng.below(rows.len() as u64) as usize,
938						rng.below(rows.len() as u64) as usize,
939					);
940					rows.swap(a, b);
941				}
942
943				let value = Rows(rows.clone());
944				let want = plain.update(&value).unwrap().map(|frame| {
945					let encoded = (*frame).clone();
946					frame.commit();
947					encoded
948				});
949				let got = memoized.update(&value).unwrap().map(|frame| {
950					let encoded = (*frame).clone();
951					frame.commit();
952					encoded
953				});
954				match (want, got) {
955					(None, None) => {}
956					(Some(want), Some(got)) => {
957						assert_eq!(got.keyframe, want.keyframe, "seed {seed} tick {tick}: keyframe");
958						assert_eq!(got.payload, want.payload, "seed {seed} tick {tick}: payload");
959						emitted += usize::from(!got.keyframe);
960					}
961					(want, got) => panic!("seed {seed} tick {tick}: {want:?} vs {got:?}"),
962				}
963				assert_eq!(memoized.value(), plain.value(), "seed {seed} tick {tick}: baseline");
964			}
965			assert!(emitted > 100, "seed {seed}: only {emitted} deltas exercised the memo");
966		}
967	}
968
969	#[test]
970	fn value_tracks_the_baseline() {
971		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(100));
972		assert_eq!(encoder.value(), None);
973
974		commit(&mut encoder, &json!({ "a": 1, "b": 1 }));
975		commit(&mut encoder, &json!({ "a": 1, "b": 2 }));
976
977		// The delta was folded into the baseline, so it reflects what was actually published.
978		assert_eq!(encoder.value(), Some(&json!({ "a": 1, "b": 2 })));
979	}
980}