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.
201	scratch: RefCell<crate::diff::Scratch>,
202
203	/// The current group's DEFLATE encoder (one window per group), `Some` while compressing.
204	flate: Option<moq_flate::Encoder>,
205
206	/// Bytes of deltas emitted into the current group, excluding the snapshot frame. Compressed
207	/// slice sizes when compressing, raw patch sizes otherwise.
208	delta_bytes: u64,
209
210	/// Reference size the delta budget is measured against: the current group's snapshot frame.
211	/// Its compressed slice size when compressing, raw otherwise.
212	snapshot_len: u64,
213
214	/// Frames emitted into the current group, snapshot included.
215	group_frames: usize,
216
217	/// Whether the next frame has to be a full snapshot, because a frame was lost or the caller cut
218	/// the group. Kept separate from [`last`](Self::last) so a resync doesn't erase the value: that
219	/// field is also what [`Producer::modify`](super::Producer::modify) seeds an edit from, and dropping
220	/// it there would publish a document with every other field missing.
221	resync: bool,
222
223	_marker: PhantomData<fn(T)>,
224}
225
226impl<T> Encoder<T> {
227	/// Create an encoder with a cold baseline, so the first [`update`](Self::update) is a snapshot.
228	pub fn new(config: Config) -> Self {
229		Self {
230			config,
231			last: None,
232			scratch: RefCell::new(crate::diff::Scratch::default()),
233			flate: None,
234			delta_bytes: 0,
235			snapshot_len: 0,
236			group_frames: 0,
237			resync: false,
238			_marker: PhantomData,
239		}
240	}
241
242	/// The last encoded value, or `None` before the first snapshot.
243	///
244	/// This is the baseline the next delta is diffed against, which is what a caller editing the
245	/// value in place needs to start from.
246	///
247	/// With deltas disabled the baseline is held as the encoded bytes, so the first call parses
248	/// them; the result is cached, and callers that never read the value never pay for it.
249	pub fn value(&self) -> Option<&Value> {
250		self.last.as_ref()?.value()
251	}
252
253	/// Force the next [`update`](Self::update) to emit a full snapshot, even for an unchanged value.
254	///
255	/// Call this whenever the caller closes the current group behind the encoder's back (a
256	/// `cut`, a `seek`, a discontinuity). Without it the next value may be encoded as a delta
257	/// against a DEFLATE window and a baseline that the new group doesn't carry.
258	///
259	/// [`value`](Self::value) survives: the snapshot republishes it in full anyway, and it is what a
260	/// caller editing in place starts from.
261	pub fn reset(&mut self) {
262		self.flate = None;
263		self.delta_bytes = 0;
264		self.snapshot_len = 0;
265		self.group_frames = 0;
266		self.resync = true;
267	}
268}
269
270impl<T: Serialize> Encoder<T> {
271	/// Encode a new value, as a snapshot or a delta.
272	///
273	/// Returns `None` when the value is unchanged from the last one encoded, so nothing needs to be
274	/// written. Otherwise the frame comes back as a [`Pending`] the caller writes and then
275	/// [`commit`](Pending::commit)s; dropping it uncommitted resynchronizes the encoder.
276	pub fn update(&mut self, value: &T) -> Result<Option<Pending<'_, T>>> {
277		Ok(self.encode(value)?.map(|encoded| Pending {
278			encoder: self,
279			encoded,
280			committed: false,
281		}))
282	}
283
284	/// Encode a new value into a bare frame, advancing the encoder's state.
285	///
286	/// The state change is what [`Pending`] guards, so this stays private: every caller goes through
287	/// [`update`](Self::update) and has to say whether the frame reached the wire.
288	fn encode(&mut self, value: &T) -> Result<Option<Encoded>> {
289		// A lost frame, or a group the caller cut, leaves the consumer's state unknown. Re-seed with a
290		// full snapshot even when the value is unchanged, since the frame that carried it may never
291		// have landed.
292		if self.resync {
293			return self.snapshot(value).map(Some);
294		}
295
296		// With deltas disabled there is nothing to diff, so the only question is whether the value
297		// changed: compare the encodings rather than parsing a baseline to diff against. The bytes
298		// are handed straight to the snapshot when it did change, so an update still serializes
299		// `T` exactly once.
300		if let Some(Baseline::Encoded { bytes, .. }) = self.last.as_ref() {
301			let bytes = bytes.clone();
302			let next = serde_json::to_vec(value)?;
303			if next.as_slice() == bytes.as_ref() {
304				return Ok(None);
305			}
306			return self.snapshot_encoded(next).map(Some);
307		}
308
309		// The first update has no baseline to diff against, so it seeds the stream with a snapshot.
310		let Some(Baseline::Parsed(last)) = self.last.as_ref() else {
311			return self.snapshot(value).map(Some);
312		};
313
314		// Diff straight off `T`, without building a full `Value` for the new value first.
315		let crate::diff::PatchBytes { patch, forced_snapshot } =
316			crate::diff::bytes(last, value, &self.scratch).map_err(crate::Error::Json)?;
317
318		// An empty object patch with no forced null means the value is unchanged: encode nothing.
319		if !forced_snapshot && patch.is_empty() {
320			return Ok(None);
321		}
322
323		// A forced snapshot (a genuine null, or a non-object root) or an exhausted delta budget starts a
324		// new group; otherwise the change rides as a delta in the open one.
325		if forced_snapshot || !self.delta_allowed() {
326			return self.snapshot(value).map(Some);
327		}
328
329		// Compress into the per-group window only now, for a frame we are committed to emitting.
330		let bytes = Bytes::from(patch);
331
332		// Same cap as a snapshot, on the patch's plaintext: a delta that decompresses past the
333		// consumer's limit makes the whole group unreadable, since there is no keyframe after it to
334		// resynchronize on. Rejecting here leaves the encoder to reset and the group as it was.
335		if self.config.compression.is_deflate() && bytes.len() as u64 > moq_flate::DEFAULT_MAX_FRAME_SIZE {
336			return Err(moq_flate::Error::TooLarge(moq_flate::DEFAULT_MAX_FRAME_SIZE).into());
337		}
338		let payload = match self.flate.as_mut() {
339			Some(flate) => flate.frame(&bytes),
340			None => bytes.clone(),
341		};
342
343		// A delta is only readable while the group still holds the snapshot it applies to.
344		// Admitting a patch that pushes the group past that budget would abort it
345		// (`GroupTooLarge`), leaving a late subscriber with no value. Roll a fresh snapshot
346		// instead, which is cheap next to losing the value.
347		//
348		// Measured on the encoded payload rather than the plaintext: a sync-flushed DEFLATE frame can
349		// come out slightly larger than its input, so the plaintext is not an upper bound. Compressing
350		// first advances the window, but [`Self::snapshot`] opens a fresh one, so an over-budget delta
351		// costs only the wasted compression.
352		if self.snapshot_len + self.delta_bytes + payload.len() as u64 > moq_net::group::MAX_CACHE_BYTES {
353			return self.snapshot(value).map(Some);
354		}
355
356		self.delta_bytes += payload.len() as u64;
357		self.group_frames += 1;
358
359		// Fold the delta into the baseline so the next diff is against the value we just encoded.
360		// Reaching a delta means `delta_allowed`, which means a non-zero ratio, which is what keeps
361		// the baseline parsed.
362		let Some(Baseline::Parsed(last)) = self.last.as_mut() else {
363			unreachable!("a parsed snapshot precedes any delta")
364		};
365		crate::merge::apply_generated_bytes(last, &bytes)?;
366
367		Ok(Some(Encoded {
368			payload,
369			keyframe: false,
370		}))
371	}
372
373	/// Whether the current change may ride as a delta in the open group.
374	///
375	/// The budget gate measures the deltas *already emitted* (excluding the frame about to land)
376	/// against the group's snapshot frame. Both are compressed sizes when compressing and raw
377	/// otherwise, so the comparison is like-for-like. Because the pending frame is excluded, the delta
378	/// that tips the group past `ratio * snapshot` still lands: a group overshoots by at most one delta
379	/// before rolling.
380	fn delta_allowed(&self) -> bool {
381		let ratio = u64::from(self.config.delta_ratio);
382		ratio != 0
383			&& self.group_frames > 0
384			&& self.group_frames < MAX_DELTA_FRAMES
385			&& self.delta_bytes <= ratio * self.snapshot_len
386	}
387
388	/// Encode a full snapshot of `value`, opening a new group and reseeding the baseline.
389	fn snapshot(&mut self, value: &T) -> Result<Encoded> {
390		// Serialize directly from `value` so the snapshot frame preserves the type's own field order,
391		// keeping the wire bytes identical to serializing `T` straight to a frame.
392		let snapshot = serde_json::to_vec(value)?;
393		self.snapshot_encoded(snapshot)
394	}
395
396	/// [`snapshot`](Self::snapshot) for a value that is already serialized, so an update that
397	/// encoded `T` to compare it against a byte baseline does not encode it a second time.
398	fn snapshot_encoded(&mut self, snapshot: Vec<u8>) -> Result<Encoded> {
399		// Every consumer decodes with moq-flate's default output cap, so a value past it would be
400		// unreadable however small it compresses to. Reject it before anything is published, so the
401		// previously published value stands rather than being superseded by one nothing can read.
402		if self.config.compression.is_deflate() && snapshot.len() as u64 > moq_flate::DEFAULT_MAX_FRAME_SIZE {
403			return Err(moq_flate::Error::TooLarge(moq_flate::DEFAULT_MAX_FRAME_SIZE).into());
404		}
405
406		// With deltas possible, read the baseline back out of those same bytes rather than
407		// serializing `value` a second time, so the baseline IS the emitted snapshot by
408		// construction. A `Serialize` impl reading a clock or interior mutable state would otherwise
409		// seed the baseline with a value no consumer ever received, and every later delta would
410		// rebase them onto it. `T` is also only visited once, which is what a caller with an
411		// expensive or effectful `Serialize` pays for.
412		//
413		// That trades a second walk of `T` for a parse of the bytes, so it is not automatically
414		// cheaper than `to_value` (see the `baseline` benchmark); consistency is the reason. With
415		// deltas off there is no diff to rebase and no reason to pay it at all.
416		//
417		// Every fallible step runs before any state changes, so a failure leaves the encoder exactly
418		// as it was rather than half-advanced with no frame to show for it.
419		let snapshot = Bytes::from(snapshot);
420		let last = if self.config.delta_ratio == 0 {
421			// No delta will ever diff against this, so hold the bytes instead. Uncompressed, they are
422			// the same allocation the payload carries, so the baseline costs a refcount.
423			Baseline::Encoded {
424				bytes: snapshot.clone(),
425				parsed: OnceLock::new(),
426			}
427		} else {
428			Baseline::Parsed(serde_json::from_slice(&snapshot)?)
429		};
430
431		// Open a fresh per-group encoder (cold window) and compress the snapshot as frame 0, recording
432		// its wire size as the delta anchor.
433		let (payload, flate) = match self.config.compression {
434			Compression::Deflate => {
435				let mut flate = moq_flate::Encoder::new();
436				let payload = flate.frame(&snapshot);
437				(payload, Some(flate))
438			}
439			Compression::None => (snapshot, None),
440		};
441
442		self.snapshot_len = payload.len() as u64;
443		self.delta_bytes = 0;
444		self.group_frames = 1;
445		self.flate = flate;
446		self.last = Some(last);
447		self.resync = false;
448
449		Ok(Encoded {
450			payload,
451			keyframe: true,
452		})
453	}
454}
455
456#[cfg(test)]
457mod test {
458	use super::*;
459	use serde_json::json;
460
461	#[test]
462	fn duplicate_serialized_keys_are_refused() {
463		use serde::ser::SerializeMap;
464		struct Duplicate {
465			duplicate: bool,
466		}
467		impl Serialize for Duplicate {
468			fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
469				let mut map = serializer.serialize_map(Some(2 + usize::from(self.duplicate)))?;
470				map.serialize_entry("a", &1)?;
471				map.serialize_entry("b", &2)?;
472				if self.duplicate {
473					map.serialize_entry("a", &3)?;
474				}
475				map.end()
476			}
477		}
478		let mut encoder = Encoder::<Duplicate>::new(Config::default());
479		encoder
480			.update(&Duplicate { duplicate: false })
481			.unwrap()
482			.unwrap()
483			.commit();
484		let err = encoder.encode(&Duplicate { duplicate: true }).unwrap_err();
485		assert!(err.to_string().contains("duplicate JSON object key"));
486	}
487
488	/// Encode a sequence of values, committing each frame, and return `(keyframe, payload_len)` per
489	/// emitted frame.
490	fn encode(config: Config, values: &[Value]) -> Vec<(bool, usize)> {
491		let mut encoder = Encoder::<Value>::new(config);
492		let mut out = Vec::new();
493		for value in values {
494			if let Some(frame) = encoder.update(value).unwrap() {
495				out.push((frame.keyframe, frame.payload.len()));
496				frame.commit();
497			}
498		}
499		out
500	}
501
502	/// Encode one value and commit it, returning the frame.
503	fn commit(encoder: &mut Encoder<Value>, value: &Value) -> Option<Encoded> {
504		let frame = encoder.update(value).unwrap()?;
505		let encoded = Encoded {
506			payload: frame.payload.clone(),
507			keyframe: frame.keyframe,
508		};
509		frame.commit();
510		Some(encoded)
511	}
512
513	#[test]
514	fn first_update_is_a_keyframe() {
515		let frames = encode(Config::default(), &[json!({ "a": 1 })]);
516		assert_eq!(frames.len(), 1);
517		assert!(frames[0].0);
518	}
519
520	#[test]
521	fn unchanged_value_encodes_nothing() {
522		let frames = encode(Config::default(), &[json!({ "a": 1 }), json!({ "a": 1 })]);
523		assert_eq!(frames.len(), 1);
524	}
525
526	#[test]
527	fn changes_ride_as_deltas() {
528		let frames = encode(
529			Config::default().with_delta_ratio(100),
530			&[
531				json!({ "a": 1, "b": 1 }),
532				json!({ "a": 1, "b": 2 }),
533				json!({ "a": 1, "b": 3 }),
534			],
535		);
536		assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, false, false]);
537	}
538
539	#[test]
540	fn deltas_off_forces_a_keyframe_per_change() {
541		let frames = encode(
542			Config::default().with_delta_ratio(0),
543			&[json!({ "a": 1 }), json!({ "a": 2 })],
544		);
545		assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
546	}
547
548	/// Deltas off keeps the baseline as bytes rather than a parsed value, so the unchanged check
549	/// runs on the encoding. It still has to suppress a republish, or every stats tick would
550	/// re-emit an identical frame.
551	#[test]
552	fn deltas_off_still_skips_an_unchanged_value() {
553		let frames = encode(
554			Config::default().with_delta_ratio(0),
555			&[json!({ "a": 1 }), json!({ "a": 1 }), json!({ "a": 1 })],
556		);
557		assert_eq!(frames.len(), 1);
558	}
559
560	/// Field order is part of the encoding, so a byte baseline only answers "unchanged" correctly
561	/// because `T` serializes deterministically. Same keys, different values, must still emit.
562	#[test]
563	fn deltas_off_detects_a_change_under_the_same_keys() {
564		let frames = encode(
565			Config::default().with_delta_ratio(0),
566			&[json!({ "a": 1, "b": 2 }), json!({ "a": 1, "b": 3 })],
567		);
568		assert_eq!(frames.len(), 2);
569	}
570
571	/// The byte baseline is parsed on demand, so `value` (and so `Producer::modify`, which seeds an
572	/// edit from it) keeps working with deltas off. Dropping the baseline instead would make
573	/// `modify` start from `T::default()` and publish a document with every other field missing.
574	#[test]
575	fn deltas_off_still_exposes_the_value() {
576		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(0));
577		assert_eq!(encoder.value(), None);
578
579		commit(&mut encoder, &json!({ "a": 1, "b": 2 })).unwrap();
580		assert_eq!(encoder.value(), Some(&json!({ "a": 1, "b": 2 })));
581
582		commit(&mut encoder, &json!({ "a": 1, "b": 3 })).unwrap();
583		assert_eq!(encoder.value(), Some(&json!({ "a": 1, "b": 3 })));
584	}
585
586	/// Compressing shares no allocation between the baseline and the payload, so the byte baseline
587	/// has to hold the plaintext rather than the compressed frame.
588	#[test]
589	fn deltas_off_while_compressing_keeps_the_plaintext_baseline() {
590		let mut config = Config::default().with_delta_ratio(0);
591		config.compression = Compression::Deflate;
592
593		let mut encoder = Encoder::<Value>::new(config);
594		commit(&mut encoder, &json!({ "a": 1 })).unwrap();
595		assert_eq!(encoder.value(), Some(&json!({ "a": 1 })));
596		assert!(commit(&mut encoder, &json!({ "a": 1 })).is_none());
597	}
598
599	/// A value the caller might reasonably expect to be a delta, but that merge patch can't express:
600	/// setting a field to JSON null reads as a key deletion. The encoder has to override the caller
601	/// here, which is why `keyframe` is a return value rather than a parameter.
602	#[test]
603	fn a_null_field_forces_a_keyframe() {
604		let frames = encode(
605			Config::default().with_delta_ratio(100),
606			&[json!({ "a": 1, "b": 1 }), json!({ "a": 1, "b": null })],
607		);
608		assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
609	}
610
611	/// Same story for a root that isn't an object: there is no recursive merge patch for it.
612	#[test]
613	fn a_non_object_root_forces_a_keyframe() {
614		let frames = encode(
615			Config::default().with_delta_ratio(100),
616			&[json!({ "a": 1 }), json!([1, 2, 3])],
617		);
618		assert_eq!(frames.iter().map(|f| f.0).collect::<Vec<_>>(), vec![true, true]);
619	}
620
621	#[test]
622	fn frame_cap_forces_a_keyframe() {
623		let values: Vec<Value> = (0..=MAX_DELTA_FRAMES).map(|n| json!({ "n": n })).collect();
624		let frames = encode(Config::default().with_delta_ratio(1_000_000), &values);
625
626		// The snapshot plus MAX_DELTA_FRAMES - 1 deltas fill the group, then the cap rolls it.
627		assert_eq!(frames.len(), MAX_DELTA_FRAMES + 1);
628		assert_eq!(frames.iter().filter(|f| f.0).count(), 2);
629		assert!(frames[MAX_DELTA_FRAMES].0);
630	}
631
632	/// A caller that cuts the group behind the encoder's back has to say so, or the next value would
633	/// be a delta against a window and a baseline the new group never carried.
634	#[test]
635	fn reset_forces_the_next_update_to_be_a_keyframe() {
636		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(100));
637		assert!(commit(&mut encoder, &json!({ "a": 1 })).unwrap().keyframe);
638		assert!(!commit(&mut encoder, &json!({ "a": 2 })).unwrap().keyframe);
639
640		encoder.reset();
641		assert!(commit(&mut encoder, &json!({ "a": 3 })).unwrap().keyframe);
642	}
643
644	/// A frame the caller never wrote must not leave the encoder emitting deltas against a baseline
645	/// no consumer received. Dropping the [`Pending`] uncommitted is what a failed write looks like,
646	/// and it has to resynchronize on its own: a caller cannot be relied on to remember.
647	#[test]
648	fn an_uncommitted_frame_resynchronizes_the_encoder() {
649		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(100));
650		commit(&mut encoder, &json!({ "a": 1 })).unwrap();
651
652		// The caller wrote this one and said so, so the next value can still ride as a delta.
653		commit(&mut encoder, &json!({ "a": 2 })).unwrap();
654
655		// This one fails to write, so the caller drops it without committing.
656		drop(encoder.update(&json!({ "a": 3 })).unwrap().expect("a delta"));
657
658		// The next value opens a new group with a full snapshot rather than patching a state the
659		// consumer never reached.
660		let recovered = commit(&mut encoder, &json!({ "a": 4 })).expect("a resynchronizing snapshot");
661		assert!(recovered.keyframe);
662		assert_eq!(
663			serde_json::from_slice::<Value>(&recovered.payload).unwrap(),
664			json!({ "a": 4 }),
665			"the snapshot carries the whole value, not a patch"
666		);
667	}
668
669	/// The same recovery when the very first frame is lost: the encoder must not treat the value as
670	/// already published and skip it as unchanged.
671	#[test]
672	fn an_uncommitted_first_frame_is_reencoded() {
673		let mut encoder = Encoder::<Value>::new(Config::default());
674		drop(encoder.update(&json!({ "a": 1 })).unwrap().expect("a snapshot"));
675
676		let retried = commit(&mut encoder, &json!({ "a": 1 })).expect("the same value, re-encoded");
677		assert!(retried.keyframe);
678	}
679
680	/// A reset value is republished even when it matches the last one encoded: the new group has to
681	/// open with a snapshot, so "unchanged" can't mean "write nothing" there.
682	#[test]
683	fn reset_republishes_an_unchanged_value() {
684		let mut encoder = Encoder::<Value>::new(Config::default());
685		commit(&mut encoder, &json!({ "a": 1 })).unwrap();
686
687		encoder.reset();
688		assert!(
689			commit(&mut encoder, &json!({ "a": 1 }))
690				.expect("a fresh snapshot")
691				.keyframe
692		);
693	}
694
695	#[test]
696	fn compressed_deltas_reuse_the_group_window() {
697		let phrase = "Media over QUIC delivers real-time latency at massive scale";
698		let frames = encode(
699			Config {
700				delta_ratio: 100,
701				compression: Compression::Deflate,
702			},
703			&[json!({ "note": phrase }), json!({ "note": phrase, "echo": phrase })],
704		);
705
706		// The raw patch repeats the whole phrase; compressed against the window it's a fraction.
707		let raw = serde_json::to_vec(&json!({ "echo": phrase })).unwrap().len();
708		assert_eq!(frames.len(), 2);
709		assert!(
710			frames[1].1 < raw / 2,
711			"windowed delta {} vs raw patch {raw}",
712			frames[1].1
713		);
714	}
715
716	/// A value whose serialization changes on every call, standing in for a `Serialize` impl backed by
717	/// a clock, an atomic, or interior mutable state.
718	struct Ticking(std::cell::Cell<u32>);
719
720	impl serde::Serialize for Ticking {
721		fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
722			use serde::ser::SerializeMap;
723
724			let n = self.0.get();
725			self.0.set(n + 1);
726
727			let mut map = serializer.serialize_map(Some(1))?;
728			map.serialize_entry("n", &n)?;
729			map.end()
730		}
731	}
732
733	/// The snapshot frame and the baseline must come from a single pass over the value. Serializing
734	/// twice costs a second traversal, and for a value like this one it seeds the baseline with
735	/// something no consumer ever received, so every later delta rebases them onto a phantom state.
736	#[test]
737	fn a_snapshot_serializes_its_value_once() {
738		let value = Ticking(std::cell::Cell::new(0));
739		let mut encoder = Encoder::<Ticking>::new(Config::default());
740		let payload = {
741			let frame = encoder.update(&value).unwrap().expect("a snapshot");
742			let payload = frame.payload.clone();
743			frame.commit();
744			payload
745		};
746
747		assert_eq!(value.0.get(), 1, "the value should be serialized exactly once");
748
749		let emitted: Value = serde_json::from_slice(&payload).unwrap();
750		assert_eq!(emitted, json!({ "n": 0 }));
751		assert_eq!(encoder.value(), Some(&emitted), "the baseline must be what was emitted");
752	}
753
754	#[test]
755	fn value_tracks_the_baseline() {
756		let mut encoder = Encoder::<Value>::new(Config::default().with_delta_ratio(100));
757		assert_eq!(encoder.value(), None);
758
759		commit(&mut encoder, &json!({ "a": 1, "b": 1 }));
760		commit(&mut encoder, &json!({ "a": 1, "b": 2 }));
761
762		// The delta was folded into the baseline, so it reflects what was actually published.
763		assert_eq!(encoder.value(), Some(&json!({ "a": 1, "b": 2 })));
764	}
765}