Skip to main content

moq_net/model/
subscription.rs

1use std::{
2	ops::{Bound, RangeBounds, RangeFull, RangeTo, RangeToInclusive},
3	task::Poll,
4	time::Duration,
5};
6
7/// Subscriber-side preferences for receiving a track.
8///
9/// Each subscriber holds its own [`Subscription`]; the publisher observes an
10/// aggregate across all live subscribers via [`crate::track::Producer::subscription`].
11/// A subscriber can change its preferences after the fact with
12/// [`crate::track::Subscriber::update`].
13#[derive(Clone, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct Subscription {
16	/// Delivery priority. Higher values preempt lower ones when bandwidth is constrained.
17	pub priority: u8,
18	/// How old a group may get before this subscriber gives up on it.
19	///
20	/// [`Duration::ZERO`] (the default) skips immediately: group 8 arriving means group 7
21	/// is abandoned. A larger budget tolerates that much reordering before giving up.
22	/// This never *adds* delay, since the bound is only reached once newer data is
23	/// already that far ahead.
24	///
25	/// This is the `Subscriber Max Age` on the wire, and it is stored here verbatim so
26	/// what was asked for stays readable. Encoded as milliseconds in a QUIC varint, so
27	/// a duration of `2^62` milliseconds or more cannot be put on the wire. Clamped to
28	/// the publisher's [`Info::max_age`](crate::track::Info::max_age), since waiting for
29	/// a group longer than it is kept around cannot produce it.
30	///
31	/// # Where it is enforced
32	///
33	/// At both ends, and neither alone is enough. The publisher skips a group that has
34	/// aged out instead of putting it on the wire, which bounds a backlog before it
35	/// costs bandwidth. But what reaches the publisher is the aggregate across every
36	/// subscriber, resolved in favor of the most tolerant one, so that gate is only ever
37	/// as tight as the most patient viewer. The subscriber applies the same budget again
38	/// as it reads, where its own is the only one in play.
39	///
40	/// This bounds a *subscription*.
41	/// [`track::Consumer::fetch_group`](crate::track::Consumer::fetch_group) is exempt:
42	/// it names one old group explicitly, so there is no live edge to be late against.
43	///
44	/// # How age is measured
45	///
46	/// In presentation time only. A group is measured by its *reach*, where its immediate
47	/// successor begins, against the newest frame of the latest group: it cannot present
48	/// past its successor, so once everything it could still hold falls outside the budget
49	/// it is provably useless. The candidate needs no timestamp of its own, so an empty or
50	/// stalled group is bounded by its stamped successor the same way. Wall-clock
51	/// reclamation of idle content is the cache's own policy, not this budget's.
52	///
53	/// Protocols whose wire can't carry a timestamp (pre-Lite05 moq-lite, moq-transport
54	/// without the Timestamp property) have their frames stamped on receipt, which makes
55	/// the measure burst-blind on the receiving side: thirty seconds of backlog delivered
56	/// in three reads as three. The publisher's copy is stamped as it produces, so the
57	/// gate there still holds; it is just the coarser of the two.
58	pub max_age: Duration,
59	/// The lowest [`Position`] the publisher may deliver, or `None` for no floor.
60	///
61	/// A floor, not a request: only [`Self::max_age`] asks for data, and the floor bounds
62	/// how far back it may reach. `None` and a floor of group 0 mean the same thing, since
63	/// nothing sits below group 0. Delivery starts at the oldest group at or above the
64	/// floor that the budget still considers fresh, so a floor above the live edge simply
65	/// waits there (a resumed subscription naming where it left off).
66	///
67	/// Aggregated across every live subscriber (the loosest floor wins, and any subscriber
68	/// without one clears it), so it says what the publisher sends, not what any one
69	/// subscriber sees. [`crate::track::Subscriber::set_groups`] is the local read cursor;
70	/// setting one does not imply the other. See [Local cursor vs wire
71	/// preference](crate::track::Subscriber#local-cursor-vs-wire-preference).
72	pub start: Option<Position>,
73	/// First [`Position`] the publisher should *not* deliver, or `None` for no end.
74	///
75	/// Exclusive, like the end of a [`std::ops::Range`], which is what lets one field
76	/// carry both "through the end of group 5" ([`Position::after_group(5)`](Position::after_group))
77	/// and "up to frame 2 of group 5" ([`Position::after(5, 2)`](Position::after)). An
78	/// inclusive end cannot express the first without a sentinel frame, and the ordering
79	/// falls out for free: group 6's head sorts above any frame of group 5, so a
80	/// whole-group subscriber correctly absorbs a frame-capped one in the aggregate.
81	///
82	/// The wire agrees: `Group End` and `Frame End` are both encoded as `absolute + 1`.
83	///
84	/// A request, aggregated across every live subscriber (any unbounded subscriber makes
85	/// the aggregate unbounded). [`crate::track::Subscriber::set_groups`] is the local read
86	/// cursor; [`Position::group_end`] translates this field into its bound. Setting one
87	/// does not imply the other.
88	pub end: Option<Position>,
89}
90
91impl Default for Subscription {
92	fn default() -> Self {
93		Self {
94			priority: 0,
95			max_age: Duration::ZERO,
96			start: None,
97			end: None,
98		}
99	}
100}
101
102impl Subscription {
103	/// Set the delivery priority, returning `self` for chaining.
104	pub fn with_priority(mut self, priority: u8) -> Self {
105		self.priority = priority;
106		self
107	}
108
109	/// Set how old a group may get before it is skipped, returning `self` for chaining.
110	pub fn with_max_age(mut self, max_age: Duration) -> Self {
111		self.max_age = max_age;
112		self
113	}
114
115	/// Floor delivery at `start`, or leave it unfloored when `None`. Returns `self` for
116	/// chaining.
117	///
118	/// A floor bounds how far back [`Self::max_age`] may reach; it does not request data
119	/// on its own. [`Position::group`] is the whole-group form.
120	pub fn with_start(mut self, start: impl Into<Option<Position>>) -> Self {
121		self.start = start.into();
122		self
123	}
124
125	/// Stop delivery at `end`, or leave the subscription unbounded when `None`. Returns
126	/// `self` for chaining.
127	///
128	/// Exclusive, matching [`Self::end`], so pass the position *after* the last one you
129	/// want. [`Position::after`] and [`Position::after_group`] name that conversion so no
130	/// call site has to write the `+ 1` itself.
131	pub fn with_end(mut self, end: impl Into<Option<Position>>) -> Self {
132		self.end = end.into();
133		self
134	}
135
136	/// Request the whole groups in `groups`, replacing both [`Self::start`] and
137	/// [`Self::end`]. Returns `self` for chaining.
138	///
139	/// Any range of group sequences works: `2..=5`, `2..6`, `..6`, `2..`, or `..` to
140	/// clear both bounds. An inclusive end past the last group is unbounded, as
141	/// [`Position::after_group`] spells it.
142	pub fn with_groups(mut self, groups: impl RangeBounds<u64>) -> Self {
143		let unbounded_start = matches!(groups.start_bound(), Bound::Unbounded);
144		let (start, end) = sequence_bounds(groups);
145		self.start = (!unbounded_start).then(|| Position::group(start));
146		self.end = end.map(Position::group);
147		self
148	}
149
150	// Fold this subscription into the running aggregate: Ready with the merged
151	// result when it demands more than `combined`, Pending when it's a subset
152	// (so callers can skip a redundant broadcast of the same aggregate).
153	pub(super) fn poll_combined(&self, combined: &Option<Subscription>) -> Poll<Subscription> {
154		let Some(combined) = combined else {
155			return Poll::Ready(self.clone());
156		};
157
158		let merged = Subscription {
159			priority: self.priority.max(combined.priority),
160			// Sequence-first prioritization is enabled only when every subscriber wants it.
161			max_age: self.max_age.max(combined.max_age),
162			// Bounds fold as whole positions. Two subscribers starting in the same group
163			// are separated only by their frame, so folding group and frame independently
164			// would invent a bound neither asked for.
165			start: min_floored(self.start, combined.start),
166			end: max_unbounded(self.end, combined.end),
167		};
168
169		if &merged != combined {
170			return Poll::Ready(merged);
171		}
172
173		Poll::Pending
174	}
175}
176
177/// A frame-precise point in a track: a group sequence and a frame index within it.
178///
179/// Ordered lexicographically, so comparing positions is the same as comparing groups
180/// and only falling back to frames within one. This is the model's counterpart of the
181/// wire's (`Group`, `Frame`) pairs on SUBSCRIBE and FETCH.
182///
183/// Pairing the two is the point: a frame index counts from the start of a group, so it
184/// means nothing on its own. Carrying them together makes "frame 5 of nothing"
185/// unrepresentable rather than merely undefined.
186#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
187pub struct Position {
188	/// The group sequence.
189	pub group: u64,
190	/// The frame index within the group, numbered from 0 in write order.
191	pub frame: u64,
192}
193
194impl Position {
195	/// The first frame of `group`.
196	///
197	/// As an exclusive end this means "everything before `group`"; as a start it means
198	/// "`group` from the beginning".
199	pub fn group(group: u64) -> Self {
200		Self { group, frame: 0 }
201	}
202
203	/// The position just past `frame` of `group`: the exclusive end that includes it.
204	///
205	/// `None` when there is no such position, i.e. the very last frame of the very last
206	/// group. That is past everything, which [`Subscription::end`] spells `None` too, so
207	/// the two meanings line up and `with_end` can take this directly.
208	pub fn after(group: u64, frame: u64) -> Option<Self> {
209		match frame.checked_add(1) {
210			Some(frame) => Some(Self { group, frame }),
211			// Past the last frame of a group is the head of the next one.
212			None => Self::after_group(group),
213		}
214	}
215
216	/// The position just past every frame of `group`: the exclusive end that includes
217	/// the group whole.
218	///
219	/// `None` past the last group, for the reason given on [`Self::after`].
220	pub fn after_group(group: u64) -> Option<Self> {
221		Some(Self::group(group.checked_add(1)?))
222	}
223
224	/// The bound this exclusive end puts on a group cursor, for
225	/// [`crate::track::Subscriber::set_groups`].
226	///
227	/// A head-of-group end excludes its group. A mid-group end includes it, so a frame
228	/// cap can apply within that group.
229	pub fn group_end(self) -> Bound<u64> {
230		if self.frame == 0 {
231			Bound::Excluded(self.group)
232		} else {
233			Bound::Included(self.group)
234		}
235	}
236}
237
238/// Where a read cursor stops: a group sequence or frame index it will not deliver.
239///
240/// Built from a range so the call site says whether its bound is delivered: `..5` stops
241/// before 5, `..=5` reads through it, and `..` removes the cap. A [`Bound`] converts
242/// too, for callers holding one (a decoded wire field, or [`Position::group_end`]).
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub(crate) struct Cap(Option<u64>);
245
246impl Cap {
247	/// The first index withheld, or `None` for no cap. Cursors store this form and
248	/// compare with [`before_end`]. An inclusive bound at `u64::MAX` has nothing above
249	/// it, so it is no cap at all.
250	pub(crate) fn exclusive(self) -> Option<u64> {
251		self.0
252	}
253}
254
255impl From<Bound<u64>> for Cap {
256	fn from(bound: Bound<u64>) -> Self {
257		Self(match bound {
258			Bound::Included(index) => index.checked_add(1),
259			Bound::Excluded(index) => Some(index),
260			Bound::Unbounded => None,
261		})
262	}
263}
264
265impl From<RangeTo<u64>> for Cap {
266	fn from(range: RangeTo<u64>) -> Self {
267		Self(Some(range.end))
268	}
269}
270
271impl From<RangeToInclusive<u64>> for Cap {
272	fn from(range: RangeToInclusive<u64>) -> Self {
273		Bound::Included(range.end).into()
274	}
275}
276
277impl From<RangeFull> for Cap {
278	fn from(_: RangeFull) -> Self {
279		Self(None)
280	}
281}
282
283// Normalize discrete ranges once, including the empty range above the last index.
284pub(super) fn sequence_bounds(range: impl RangeBounds<u64>) -> (u64, Option<u64>) {
285	let start = match range.start_bound() {
286		Bound::Included(&start) => start,
287		Bound::Excluded(&start) => match start.checked_add(1) {
288			Some(start) => start,
289			None => return (u64::MAX, Some(u64::MAX)),
290		},
291		Bound::Unbounded => 0,
292	};
293	(start, Cap::from(range.end_bound().cloned()).exclusive())
294}
295
296/// Whether `sequence` is strictly below an exclusive cap. `None` is unbounded.
297pub(super) fn before_end(sequence: u64, end: Option<u64>) -> bool {
298	end.is_none_or(|end| sequence < end)
299}
300
301// Combining two optional bounds comes in two families, and they disagree on what `None`
302// means. `_some` treats it as the neutral element (the other side wins), for intersecting
303// two ranges that each restrict independently. `_floored` / `_unbounded` treat it as
304// absorbing (the result is `None` too), for aggregating across subscribers, where one
305// subscriber asking for everything makes the aggregate everything. Picking the wrong
306// family silently narrows or widens what the publisher sends, so the suffix, not the
307// `min`/`max`, is the part to read.
308
309/// The lower of two optional bounds, `None` neutral. Pairs with [`max_some`].
310pub(super) fn min_some<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
311	match (a, b) {
312		(Some(a), Some(b)) => Some(a.min(b)),
313		(Some(a), None) | (None, Some(a)) => Some(a),
314		(None, None) => None,
315	}
316}
317
318/// The higher of two optional bounds, `None` neutral. Pairs with [`min_some`].
319pub(super) fn max_some<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
320	match (a, b) {
321		(Some(a), Some(b)) => Some(a.max(b)),
322		(Some(a), None) | (None, Some(a)) => Some(a),
323		(None, None) => None,
324	}
325}
326
327/// The lower of two optional floors, `None` absorbing (no floor). The mirror of
328/// [`max_unbounded`]: both bounds only ever *restrict*, so a subscriber without one keeps
329/// the aggregate unrestricted.
330pub(super) fn min_floored<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
331	match (a, b) {
332		(Some(a), Some(b)) => Some(a.min(b)),
333		(None, _) | (_, None) => None,
334	}
335}
336
337/// The higher of two optional bounds, `None` absorbing (unbounded).
338pub(super) fn max_unbounded<T: Ord>(a: Option<T>, b: Option<T>) -> Option<T> {
339	match (a, b) {
340		(Some(a), Some(b)) => Some(a.max(b)),
341		(None, _) | (_, None) => None,
342	}
343}
344
345#[cfg(test)]
346mod tests {
347	use super::*;
348
349	fn combine(subscriptions: &[Subscription]) -> Option<Subscription> {
350		let mut combined = None;
351		for sub in subscriptions {
352			if let Poll::Ready(merged) = sub.poll_combined(&combined) {
353				combined = Some(merged);
354			}
355		}
356		combined
357	}
358
359	/// The exclusive representation runs out at both extremes, and `Option` says so
360	/// rather than saturating into a bound that contradicts the request.
361	/// A group range spells both positions at once, in whichever form the caller has.
362	#[test]
363	fn group_ranges_build_whole_group_positions() {
364		let sub = Subscription::default().with_groups(2..=5);
365		assert_eq!(sub.start, Some(Position::group(2)));
366		assert_eq!(sub.end, Some(Position::group(6)));
367
368		let sub = Subscription::default().with_groups(2..6);
369		assert_eq!(sub.end, Some(Position::group(6)));
370
371		let sub = Subscription::default().with_groups(..6);
372		assert_eq!(sub.start, None);
373		assert_eq!(sub.end, Some(Position::group(6)));
374
375		let sub = Subscription::default().with_groups(2..);
376		assert_eq!(sub.start, Some(Position::group(2)));
377		assert_eq!(sub.end, None);
378
379		// Through the last group is unbounded, as `after_group` spells it.
380		let sub = Subscription::default().with_groups(..=u64::MAX);
381		assert_eq!(sub.end, None);
382
383		let sub = Subscription::default().with_groups(2..=5).with_groups(..);
384		assert_eq!((sub.start, sub.end), (None, None));
385	}
386
387	#[test]
388	fn positions_are_total_at_the_extremes() {
389		// Past the last frame of a group is the head of the next one, not a wider frame
390		// in the same group.
391		assert_eq!(Position::after(5, u64::MAX), Some(Position::group(6)));
392
393		// Past everything has no position. `Subscription::end` spells that `None` too,
394		// so the meanings coincide and the group is included rather than dropped.
395		assert_eq!(Position::after_group(u64::MAX), None);
396		assert_eq!(Position::after(u64::MAX, u64::MAX), None);
397		assert_eq!(
398			Subscription::default().with_end(Position::after_group(u64::MAX)).end,
399			None
400		);
401
402		// A head-of-group end excludes that group; a mid-group end includes it.
403		assert_eq!(Position::group(0).group_end(), Bound::Excluded(0));
404		assert_eq!(Position::after_group(5).unwrap().group_end(), Bound::Excluded(6));
405		assert_eq!(Position::after(5, 2).unwrap().group_end(), Bound::Included(5));
406
407		// A cursor cap is the first index it withholds; an inclusive bound at the last
408		// index withholds nothing.
409		assert_eq!(Cap::from(..0).exclusive(), Some(0));
410		assert_eq!(Cap::from(..=5).exclusive(), Some(6));
411		assert_eq!(Cap::from(..=u64::MAX).exclusive(), None);
412		assert_eq!(Cap::from(..).exclusive(), None);
413		assert_eq!(Cap::from(Bound::Included(5)), Cap::from(..6));
414		assert_eq!(Cap::from(Bound::Unbounded), Cap::from(..));
415	}
416
417	#[test]
418	fn combined_group_start_keeps_the_loosest_floor() {
419		// A floor only restricts, so the lowest one wins across floored subscribers.
420		let catchup = Subscription::default().with_start(Position::group(10));
421		let older_catchup = Subscription::default().with_start(Position::group(5));
422		let combined = combine(&[catchup.clone(), older_catchup]).unwrap();
423		assert_eq!(combined.start, Some(Position::group(5)));
424
425		// A subscriber with no floor at all clears the aggregate: its budget may reach
426		// below any floor the others set.
427		let unfloored = Subscription::default();
428		let combined = combine(&[catchup, unfloored]).unwrap();
429		assert_eq!(combined.start, None);
430	}
431
432	#[test]
433	fn combined_group_end_keeps_live_subscription_unbounded() {
434		// No end at all is unbounded, which absorbs any explicit one.
435		let live = Subscription::default();
436		let bounded = Subscription::default().with_end(Position::after_group(10));
437
438		let combined = combine(&[live, bounded]).unwrap();
439
440		assert_eq!(combined.end, None);
441	}
442
443	#[test]
444	fn combined_start_folds_the_whole_position() {
445		let early_frame = Subscription::default().with_start(Position { group: 5, frame: 2 });
446		let late_frame = Subscription::default().with_start(Position { group: 5, frame: 9 });
447
448		// Same group: the earlier frame wins.
449		let combined = combine(&[late_frame.clone(), early_frame.clone()]).unwrap();
450		assert_eq!(combined.start, Some(Position { group: 5, frame: 2 }));
451
452		// An earlier group wins outright, carrying its own frame rather than the
453		// smallest frame across the two.
454		let earlier_group = Subscription::default().with_start(Position { group: 4, frame: 7 });
455		let combined = combine(&[early_frame, earlier_group]).unwrap();
456		assert_eq!(combined.start, Some(Position { group: 4, frame: 7 }));
457	}
458
459	#[test]
460	fn combined_end_folds_the_whole_position() {
461		let short = Subscription::default().with_end(Position::after(5, 2));
462		let long = Subscription::default().with_end(Position::after(5, 9));
463
464		// Same group: the later frame wins. Ends are exclusive, so an inclusive frame 9
465		// is stored as 10.
466		let combined = combine(&[short.clone(), long.clone()]).unwrap();
467		assert_eq!(combined.end, Some(Position { group: 5, frame: 10 }));
468
469		// The whole group is the head of the next one, which outsorts every frame of
470		// this one, so it absorbs any capped end without a sentinel.
471		let whole = Subscription::default().with_end(Position::after_group(5));
472		let combined = combine(&[long, whole]).unwrap();
473		assert_eq!(combined.end, Some(Position::group(6)));
474
475		// A later group wins outright, carrying its own frame.
476		let later_group = Subscription::default().with_end(Position::after(6, 1));
477		let combined = combine(&[short, later_group]).unwrap();
478		assert_eq!(combined.end, Some(Position { group: 6, frame: 2 }));
479	}
480
481	#[test]
482	fn combined_group_end_uses_latest_bounded_end() {
483		let early = Subscription::default().with_end(Position::after_group(10));
484		let late = Subscription::default().with_end(Position::after_group(20));
485
486		let combined = combine(&[early, late]).unwrap();
487
488		assert_eq!(combined.end, Some(Position::group(21)));
489	}
490}