Skip to main content

moq_json/window/
producer.rs

1//! Publishing a window over a track: an [`Encoder`] plus the track it writes to.
2
3use std::marker::PhantomData;
4use std::sync::{Arc, Mutex};
5
6use serde::Serialize;
7use serde_json::Value;
8
9use super::{Encoded, Encoder, ProducerConfig};
10use crate::Result;
11
12/// Publishes a sliding window of JSON records over a track.
13///
14/// An [`Encoder`] that owns its track: it writes each encoded frame and rolls a group whenever the
15/// encoder emits a header. When something else already owns the track, use the [`Encoder`] directly.
16///
17/// Cheaply clonable: clones share one underlying track and window, like other MoQ producers.
18pub struct Producer<T> {
19	inner: Arc<Mutex<Inner<T>>>,
20	_marker: PhantomData<fn(T)>,
21}
22
23impl<T> Clone for Producer<T> {
24	fn clone(&self) -> Self {
25		Self {
26			inner: self.inner.clone(),
27			_marker: PhantomData,
28		}
29	}
30}
31
32impl<T> Producer<T> {
33	/// Create a producer that publishes to the given track.
34	pub fn new(track: moq_net::track::Producer, config: ProducerConfig) -> Self {
35		Self {
36			inner: Arc::new(Mutex::new(Inner {
37				track: Track {
38					inner: track,
39					group: None,
40				},
41				encoder: Encoder::new(config),
42				finished: false,
43			})),
44			_marker: PhantomData,
45		}
46	}
47
48	/// Create a subscriber for the underlying track.
49	pub fn consume(&self) -> moq_net::track::Subscriber {
50		self.inner.lock().unwrap().track.inner.subscribe(None)
51	}
52
53	/// The retained checkpoint suffix, oldest first.
54	///
55	/// This is the complete window unless [`ProducerConfig::checkpoint_records`] is set.
56	pub fn window(&self) -> Vec<Value> {
57		self.inner.lock().unwrap().encoder.window()
58	}
59
60	/// Absolute index of the oldest retained record, and of the next to be pushed.
61	pub fn range(&self) -> std::ops::Range<u64> {
62		self.inner.lock().unwrap().encoder.range()
63	}
64
65	/// Drop `count` records from the front of the window.
66	///
67	/// A no-op when the window is already empty, and clamped to what it holds, so a caller can trim
68	/// unconditionally.
69	pub fn pop(&mut self, count: u64) -> Result<()> {
70		self.inner.lock().unwrap().pop(count)
71	}
72
73	/// Finish the track, closing any open group.
74	///
75	/// Borrows rather than consumes, matching snapshot and stream, so the handle stays
76	/// usable afterwards (reads, a second finish). Writes after this fail with
77	/// [`moq_net::Error::Closed`].
78	pub fn finish(&mut self) -> Result<()> {
79		self.inner.lock().unwrap().finish()
80	}
81}
82
83impl<T: Serialize> Producer<T> {
84	/// Append one record to the back of the window.
85	pub fn push(&mut self, value: &T) -> Result<()> {
86		self.inner.lock().unwrap().push(value)
87	}
88}
89
90/// Shared publishing state behind [`Producer`]'s `Arc<Mutex>`.
91///
92/// The track and the encoder are separate fields so a [`Pending`](super::Pending) frame (which
93/// borrows the encoder) and the write that consumes it (which borrows the track) don't contend for
94/// one `&mut self`.
95struct Inner<T> {
96	track: Track,
97	encoder: Encoder<T>,
98	finished: bool,
99}
100
101impl<T> Inner<T> {
102	fn pop(&mut self, count: u64) -> Result<()> {
103		self.ensure_open()?;
104		let Inner { track, encoder, .. } = self;
105
106		let Some(frame) = encoder.pop(count)? else {
107			return Ok(());
108		};
109
110		// A failed write drops the frame uncommitted. The pop is discarded, and the next edit opens a
111		// new group because the attempted frame advanced the group-local compression state.
112		track.write(&frame)?;
113		frame.commit();
114
115		Ok(())
116	}
117
118	fn finish(&mut self) -> Result<()> {
119		if self.finished {
120			return Ok(());
121		}
122		self.finished = true;
123		self.track.finish()
124	}
125
126	fn ensure_open(&self) -> Result<()> {
127		if self.finished {
128			return Err(moq_net::Error::Closed.into());
129		}
130		Ok(())
131	}
132}
133
134impl<T: Serialize> Inner<T> {
135	fn push(&mut self, value: &T) -> Result<()> {
136		self.ensure_open()?;
137		let Inner { track, encoder, .. } = self;
138
139		let frame = encoder.push(value)?;
140		track.write(&frame)?;
141		frame.commit();
142
143		Ok(())
144	}
145}
146
147/// The track half of [`Inner`]: where an encoded frame goes and how groups are rolled.
148struct Track {
149	inner: moq_net::track::Producer,
150
151	/// The group an op would be appended to, open after a header.
152	group: Option<moq_net::group::Producer>,
153}
154
155impl Track {
156	/// Write one encoded frame, rolling a group when it is a header.
157	fn write(&mut self, encoded: &Encoded) -> Result<()> {
158		match encoded.keyframe {
159			true => self.write_header(encoded.payload.clone()),
160			false => self.write_op(encoded.payload.clone()),
161		}
162	}
163
164	/// Close the open group and write the header as the first frame of a new one.
165	fn write_header(&mut self, payload: bytes::Bytes) -> Result<()> {
166		// The previous group is complete; no more frames will be appended to it.
167		if let Some(group) = self.group.take() {
168			group.finish()?;
169		}
170
171		let mut group = self.inner.append_group()?;
172		if let Err(err) = group.write_frame(moq_net::Timestamp::now(), payload) {
173			// `append_group` already published this group, and a rejected frame (too large) doesn't
174			// close the track. Dropping the handle does NOT close the group, so leaving it would strand
175			// any subscriber that advanced into it with nothing to read and no end.
176			let _ = group.finish();
177			return Err(err.into());
178		}
179
180		self.group = Some(group);
181		Ok(())
182	}
183
184	/// Append an op to the group the last header opened.
185	fn write_op(&mut self, payload: bytes::Bytes) -> Result<()> {
186		self.group
187			.as_mut()
188			.expect("the encoder only emits an op after a header opened a group")
189			.write_frame(moq_net::Timestamp::now(), payload)?;
190		Ok(())
191	}
192
193	fn finish(&mut self) -> Result<()> {
194		if let Some(group) = self.group.take() {
195			group.finish()?;
196		}
197		self.inner.finish()?;
198		Ok(())
199	}
200}