Skip to main content

moq_json/
lib.rs

1//! JSON publishing over [`moq-net`](moq_net) tracks, in three modes:
2//!
3//! - [`snapshot`]: **lossy**. One JSON value updated over time; a consumer only gets the most
4//!   recent value. Intermediate updates are collapsed and older groups are dropped.
5//! - [`stream`]: **lossless**. An ordered append-log of self-contained records; every record is
6//!   preserved and delivered in order, nothing is ever superseded.
7//! - [`window`]: **bounded**. An ordered run of records appended to the back and dropped from the
8//!   front, which a reader can join at any point.
9//!
10//! Pick [`snapshot`] when consumers care about "what is the value now" (a catalog, a status
11//! document), [`stream`] when they care about every record of an unbounded log, and [`window`] when
12//! the publisher retires old records and a late reader should start from what is still retained.
13//!
14//! Each mode comes in two layers. `Producer`/`Consumer` own a [`moq_net`] track and manage its
15//! groups. `Encoder`/`Decoder` are the same logic without the track: values in, frame payloads out
16//! (and back), with the encoder saying where the group boundaries fall. Reach for the codec layer
17//! when something else already owns the track, such as a `moq_mux::container::Producer` also
18//! managing a timeline and a catalog estimate.
19
20mod diff;
21mod merge;
22pub mod snapshot;
23pub mod stream;
24pub mod window;
25
26pub use crate::diff::{Diff, diff};
27
28/// How a JSON track compresses its frames.
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub enum Compression {
31	/// Uncompressed JSON frames.
32	#[default]
33	None,
34
35	/// Group-scoped raw DEFLATE, sync-flushed at each frame boundary.
36	Deflate,
37}
38
39impl Compression {
40	pub(crate) const fn is_deflate(self) -> bool {
41		matches!(self, Self::Deflate)
42	}
43}
44
45/// Errors produced while publishing or consuming JSON.
46#[derive(thiserror::Error, Debug, Clone)]
47#[non_exhaustive]
48pub enum Error {
49	/// An error from the underlying track.
50	#[error(transparent)]
51	Net(#[from] moq_net::Error),
52
53	/// A value failed to serialize, deserialize, or apply as a merge patch.
54	///
55	/// Stored as a string since [`serde_json::Error`] is not [`Clone`].
56	#[error("json: {0}")]
57	Json(String),
58
59	/// A compressed frame could not be decoded (malformed, truncated, or oversized).
60	#[error(transparent)]
61	Flate(#[from] moq_flate::Error),
62
63	/// A merge patch arrived with no snapshot to apply it to.
64	///
65	/// Every group opens with a full snapshot, so this means frames reached
66	/// [`snapshot::Decoder`] out of order, or a group's first frame was routed as a delta.
67	#[error("delta before snapshot")]
68	MissingSnapshot,
69
70	/// A compressed [`stream`] frame was encoded but never written, so the shared DEFLATE window is
71	/// ahead of what the consumer holds and nothing later in this group can be decoded.
72	///
73	/// Unlike [`snapshot`], a stream has no keyframe to resynchronize on, so the encoder refuses to
74	/// continue rather than emit frames that cannot be read. Recover by rolling a new group and
75	/// calling [`stream::Encoder::reset`].
76	#[error("compression desynchronized: a frame was encoded but never written")]
77	Desync,
78
79	/// A [`stream`] track carried a second group, which a lossless log cannot do.
80	///
81	/// A stream is a single group by construction: a publisher that cannot write a record ends the
82	/// track rather than rolling. A second group therefore means the records that would have
83	/// completed the first one are gone, so the read fails instead of presenting the remainder as
84	/// a continuous log.
85	#[error("stream rolled to a second group")]
86	Rolled,
87}
88
89impl From<serde_json::Error> for Error {
90	fn from(err: serde_json::Error) -> Self {
91		Error::Json(err.to_string())
92	}
93}
94
95/// A [`Result`](std::result::Result) using this crate's [`Error`].
96pub type Result<T> = std::result::Result<T, Error>;