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}