Skip to main content

moq_net/model/
track.rs

1//! A track is a collection of semi-reliable and semi-ordered streams, split into a [Producer] and [Subscriber] handle.
2//!
3//! A [Producer] creates streams with a sequence number and priority.
4//! The sequence number is used to determine the order of streams, while the priority is used to determine which stream to transmit first.
5//! This may seem counter-intuitive, but is designed for live streaming where the newest streams may be higher priority.
6//! A cloned [Producer] can be used to create streams in parallel, but will error if a duplicate sequence number is used.
7//!
8//! A [Subscriber] may not receive all streams in order or at all.
9//! These streams are meant to be transmitted over congested networks and the key to MoQ Transport is to not block on them.
10//! Streams will be cached for a potentially limited duration added to the unreliable nature.
11//! A [Consumer] is a cheap, cloneable handle; subscribing it multiple times fans the same
12//! cached streams out to each independent [Subscriber].
13//!
14//! The track is closed with [Error] when all writers or readers are dropped.
15
16use crate::{Error, Result, Timescale, Timestamp, coding};
17use crate::{broadcast, cache, frame, group, stats};
18
19use super::{Datagram, Requests};
20
21pub use super::subscription::Subscription;
22
23use std::{
24	collections::{HashSet, VecDeque},
25	sync::Arc,
26	task::{Poll, ready},
27	time::Duration,
28};
29
30/// Default [`Info::latency_max`] age when the publisher doesn't set one.
31pub const DEFAULT_LATENCY_MAX: Duration = Duration::from_secs(5);
32
33/// How long a datagram stays in the per-track buffer before it is dropped.
34///
35/// Datagrams are a best-effort send buffer, not a replay cache (unlike groups): only the last
36/// few tens of milliseconds are kept, so a consumer that stalls loses stale datagrams instead of
37/// replaying them. Sized like a typical send buffer for real-time audio/video.
38const MAX_DATAGRAM_AGE: Duration = Duration::from_millis(50);
39
40/// Publisher-side properties of a track.
41///
42/// These are fixed by the publisher when the track is created and don't change
43/// while the track is alive. A subscriber learns them via
44/// [`broadcast::Consumer::track`](broadcast::Consumer::track),
45/// which returns the publisher's [`Info`] once the subscription is accepted.
46///
47/// Not `Copy`: it carries an internal handle to its parent broadcast.
48#[derive(Clone, Debug)]
49#[non_exhaustive]
50pub struct Info {
51	/// Units per second for per-frame timestamps on this track.
52	///
53	/// Every track is timed; this defaults to [`Timescale::MILLI`]. On Lite05+ it is
54	/// reported in TRACK_INFO and the publisher zigzag-delta encodes per-frame
55	/// timestamps at this scale on the wire. Protocols whose wire can't carry it
56	/// (pre-Lite05 moq-lite, IETF moq-transport) fall back to local monotonic milliseconds.
57	pub timescale: Timescale,
58	/// The maximum age of a non-latest group before the publisher evicts it (the
59	/// newest group is always retained). A subscriber's
60	/// [`Subscription::latency_max`] window is clamped to this, since a group can't be
61	/// waited for longer than it's kept around. Reported in TRACK_INFO so
62	/// relays re-serve with the same window. Defaults to [`DEFAULT_LATENCY_MAX`].
63	///
64	/// This is the `Publisher Max Latency` on the wire, the publisher-side half of
65	/// the same budget [`Subscription::latency_max`] sets for a subscriber.
66	pub latency_max: Duration,
67	/// The publisher's priority for this track, used only to break ties between
68	/// subscriptions of equal subscriber priority. Reported in TRACK_INFO (Lite05+).
69	pub priority: u8,
70	/// Whether groups are prioritized in sequence order. Groups may always arrive
71	/// out-of-order (or not at all) over the network. Used only to break ties,
72	/// reported in TRACK_INFO (Lite05+), and defaults to `false` (newest-first).
73	pub ordered: bool,
74
75	// The broadcast this track belongs to, bound when the track is created under one
76	// (`create_track` / `reserve_track` / `Request::accept`); until then it's a
77	// standalone broadcast with an unbounded pool. Not on the wire, and not a knob:
78	// every bind path overwrites whatever is here. It's the parent link a group walks
79	// to reach the shared cache pool (`track.broadcast.origin.pool`), which stays
80	// crate-private.
81	pub(crate) broadcast: Arc<broadcast::Info>,
82}
83
84/// The shared parent for a not-yet-bound [`Info`]: a standalone broadcast with an
85/// unbounded pool. Cheap to hand out (one `Arc` clone) and replaced the moment the
86/// track is bound to a real broadcast.
87fn default_broadcast() -> Arc<broadcast::Info> {
88	static DEFAULT: std::sync::LazyLock<Arc<broadcast::Info>> =
89		std::sync::LazyLock::new(|| Arc::new(broadcast::Info::default()));
90	DEFAULT.clone()
91}
92
93impl Default for Info {
94	fn default() -> Self {
95		Self {
96			timescale: Timescale::default(),
97			latency_max: DEFAULT_LATENCY_MAX,
98			priority: 0,
99			ordered: false,
100			broadcast: default_broadcast(),
101		}
102	}
103}
104
105impl Info {
106	/// Set the per-frame timestamp scale, returning `self` for chaining.
107	///
108	/// Defaults to [`Timescale::MILLI`]. On Lite05+ this scale is reported in TRACK_INFO
109	/// and used to encode per-frame timestamps on the wire.
110	pub fn with_timescale(mut self, timescale: Timescale) -> Self {
111		self.timescale = timescale;
112		self
113	}
114
115	/// Set the maximum age of a non-latest group before eviction, returning `self` for chaining.
116	pub fn with_latency_max(mut self, latency_max: Duration) -> Self {
117		self.latency_max = latency_max;
118		self
119	}
120
121	/// Set the publisher's tie-break priority, returning `self` for chaining.
122	pub fn with_priority(mut self, priority: u8) -> Self {
123		self.priority = priority;
124		self
125	}
126
127	/// Set whether groups are prioritized in sequence order, returning `self` for
128	/// chaining. Groups may always arrive out-of-order (or not at all) over the
129	/// network. Defaults to `false`.
130	pub fn with_ordered(mut self, ordered: bool) -> Self {
131		self.ordered = ordered;
132		self
133	}
134}
135
136#[derive(Default)]
137struct TrackState {
138	// The info for the track; always Some for Subscriber/Producer. Inherited (cloned)
139	// by each group it creates, which reaches the cache pool through its `broadcast`.
140	info: Option<Info>,
141
142	// The broadcast this track belongs to, the source for stamping `Info::broadcast`
143	// on groups (and on a not-yet-accepted track's default info). Its
144	// `origin.pool` is the shared cache pool every group registers with.
145	broadcast: Arc<broadcast::Info>,
146
147	// The pool registration of the current max_sequence group, pinned so the
148	// latest group is immune to pool eviction. `None` when the pool is detached
149	// or no group exists yet.
150	latest_entry: Option<Arc<cache::Entry>>,
151
152	// Groups in arrival order. `None` entries are tombstones for evicted groups.
153	groups: VecDeque<Option<(group::Producer, web_async::time::Instant)>>,
154
155	// Datagrams in arrival order paired with their arrival time, a best-effort send buffer
156	// evicted by age (see `MAX_DATAGRAM_AGE`). Shares the group `max_sequence` namespace but
157	// is otherwise independent.
158	datagrams: VecDeque<(Datagram, web_async::time::Instant)>,
159
160	// Number of datagrams dropped off the front (aged out), mapping a subscriber's absolute
161	// cursor to an index into `datagrams` (mirrors `offset` for groups).
162	datagram_offset: usize,
163
164	// Sequences currently occupying a cache slot, used to reject a duplicate
165	// `create_group`/`write_group` with `Error::Duplicate`. Entries are removed on
166	// expiry/eviction so a pool-evicted sequence can be re-fetched into its slot.
167	duplicates: HashSet<u64>,
168
169	// We've popped the front of this VecDeque this many times, used to map sequence -> index.
170	offset: usize,
171
172	// The highest sequence number successfully appended to the track.
173	max_sequence: Option<u64>,
174
175	// The sequence number at which the track was finalized.
176	final_sequence: Option<u64>,
177
178	// The error that caused the track to be aborted, if any.
179	abort: Option<Error>,
180
181	// Active subscriptions, in their own [`kio::Shared`] so a read-only `Consumer`
182	// registers under that lock instead of writing back into the track state.
183	// Kept here (rather than threaded through every handle) so any holder reaches it.
184	subscriptions: kio::Shared<Subscriptions>,
185
186	// The reverse fetch queue (see [`FetchState`]), same reasoning: cache-miss
187	// `fetch_group` calls enqueue here and a `Dynamic` drains.
188	fetch: kio::Shared<FetchState>,
189}
190
191/// The registered subscriptions, aggregated by the producer.
192type Subscriptions = Vec<kio::Consumer<Subscription>>;
193
194/// Reverse state for [`Consumer::fetch_group`], beside the track state in its own
195/// [`kio::Shared`]: consumers enqueue (coalescing per sequence, so a relay opens one
196/// upstream FETCH per group) and [`Dynamic`] handlers drain under one lock, without
197/// write access to the track itself.
198type FetchState = Requests<u64, PendingFetch>;
199
200/// One fetch attempt for a sequence, shared by every [`Fetching`] that joined it.
201struct PendingFetch {
202	// The most demanding delivery priority across the joined fetches.
203	priority: u8,
204
205	// Result channel back to the joined fetches. Written only on rejection; a
206	// successful accept resolves them through the track cache instead. Dropping
207	// every producer without writing (a vanished handler) closes the channel,
208	// which a [`Fetching`] reads as [`Error::NotFound`].
209	result: kio::Producer<FetchOutcome>,
210}
211
212/// The result of a fetch attempt. Stays empty on success (the group lands in the
213/// track cache); a handler writes `rejected` to fail every joined fetch.
214#[derive(Default)]
215struct FetchOutcome {
216	rejected: Option<Error>,
217}
218
219impl TrackState {
220	fn poll_info(&self) -> Poll<Result<Info>> {
221		if let Some(info) = &self.info {
222			Poll::Ready(Ok(info.clone()))
223		} else {
224			Poll::Pending
225		}
226	}
227
228	/// Find the next non-tombstoned group at or after `index` in arrival order.
229	///
230	/// Returns the group and its absolute index so the consumer can advance past it.
231	fn poll_recv_group(&self, index: usize, min_sequence: u64) -> Poll<Result<Option<(group::Consumer, usize)>>> {
232		let start = index.saturating_sub(self.offset);
233		for (i, slot) in self.groups.iter().enumerate().skip(start) {
234			if let Some((group, _)) = slot
235				&& group.sequence >= min_sequence
236				&& !group.is_aborted()
237			{
238				return Poll::Ready(Ok(Some((group.consume(), self.offset + i))));
239			}
240		}
241
242		// TODO once we have drop notifications, check if index == final_sequence.
243		if self.is_complete() {
244			Poll::Ready(Ok(None))
245		} else if let Some(err) = &self.abort {
246			Poll::Ready(Err(err.clone()))
247		} else {
248			Poll::Pending
249		}
250	}
251
252	/// Find the next datagram at or after the subscriber's absolute `index`.
253	///
254	/// Returns the datagram and its absolute index so the consumer can advance past it. A
255	/// consumer whose `index` has fallen behind `datagram_offset` (older datagrams dropped)
256	/// resumes at the oldest still-buffered datagram, skipping the lost ones.
257	fn poll_recv_datagram(&self, index: usize) -> Poll<Result<Option<(Datagram, usize)>>> {
258		let start = index.saturating_sub(self.datagram_offset);
259		if let Some((datagram, _)) = self.datagrams.get(start) {
260			return Poll::Ready(Ok(Some((datagram.clone(), self.datagram_offset + start))));
261		}
262
263		// Nothing buffered at the cursor: the track ending terminates the datagram stream too.
264		if self.is_complete() {
265			Poll::Ready(Ok(None))
266		} else if let Some(err) = &self.abort {
267			Poll::Ready(Err(err.clone()))
268		} else {
269			Poll::Pending
270		}
271	}
272
273	/// Push a datagram onto the buffer, dropping any that have aged past [`MAX_DATAGRAM_AGE`].
274	fn push_datagram(&mut self, datagram: Datagram) {
275		let now = web_async::time::Instant::now();
276		self.datagrams.push_back((datagram, now));
277		while let Some((_, at)) = self.datagrams.front() {
278			if now.duration_since(*at) <= MAX_DATAGRAM_AGE {
279				break;
280			}
281			self.datagrams.pop_front();
282			self.datagram_offset += 1;
283		}
284	}
285
286	/// Scan groups at or after `index` in arrival order, looking for the first with sequence
287	/// `>= next_sequence` that has a fully-buffered next frame. Returns the frame plus the
288	/// winning slot's absolute index and sequence so the consumer can advance past it.
289	fn poll_read_frame(
290		&self,
291		index: usize,
292		next_sequence: u64,
293		waiter: &kio::Waiter,
294	) -> Poll<Result<Option<(frame::Frame, usize, u64)>>> {
295		let start = index.saturating_sub(self.offset);
296		let mut pending_seen = false;
297		for (i, slot) in self.groups.iter().enumerate().skip(start) {
298			let Some((group, _)) = slot else { continue };
299			if group.sequence < next_sequence {
300				continue;
301			}
302
303			let mut consumer = group.consume();
304			match consumer.poll_read_frame(waiter) {
305				Poll::Ready(Ok(Some(frame))) => {
306					return Poll::Ready(Ok(Some((frame, self.offset + i, group.sequence))));
307				}
308				Poll::Ready(Ok(None)) => continue,
309				// A single group failing (aborted upstream, or evicted from the
310				// cache) doesn't poison the track; skip it like a gap.
311				Poll::Ready(Err(_)) => continue,
312				Poll::Pending => {
313					pending_seen = true;
314					continue;
315				}
316			}
317		}
318
319		// A pending group can still produce a frame even after finish(). Finish only
320		// blocks new groups at/above final_sequence, not frames on existing groups.
321		if pending_seen {
322			Poll::Pending
323		} else if self.is_complete() {
324			Poll::Ready(Ok(None))
325		} else if let Some(err) = &self.abort {
326			Poll::Ready(Err(err.clone()))
327		} else {
328			Poll::Pending
329		}
330	}
331
332	/// Find the smallest-sequence cached group satisfying
333	/// `next_sequence <= seq <= end_sequence (if set)`. Used by
334	/// [`Subscriber::next_group`] so the range can be widened (or unset)
335	/// after the fact and previously-skipped cached groups become available
336	/// without scanning past them in arrival order.
337	///
338	/// Returns `Poll::Pending` when no in-range group is currently cached but
339	/// future groups could still arrive in range; returns `Ok(None)` only when
340	/// the track is finalized and no further in-range group is possible.
341	fn poll_next_in_range(
342		&self,
343		next_sequence: u64,
344		end_sequence: Option<u64>,
345	) -> Poll<Result<Option<group::Consumer>>> {
346		// If the end cap is already below where we'd resume, no group can
347		// ever satisfy this call until the cap rises. Pending (not None) so
348		// the consumer is parked rather than told the stream is over.
349		if let Some(end) = end_sequence
350			&& end < next_sequence
351		{
352			if let Some(err) = &self.abort {
353				return Poll::Ready(Err(err.clone()));
354			}
355			return Poll::Pending;
356		}
357
358		let mut best: Option<&group::Producer> = None;
359		for (group, _) in self.groups.iter().flatten() {
360			if group.sequence < next_sequence {
361				continue;
362			}
363			if let Some(end) = end_sequence
364				&& group.sequence > end
365			{
366				continue;
367			}
368			if group.is_aborted() {
369				continue;
370			}
371			if best.is_none_or(|b| group.sequence < b.sequence) {
372				best = Some(group);
373			}
374		}
375
376		if let Some(group) = best {
377			return Poll::Ready(Ok(Some(group.consume())));
378		}
379
380		// No in-range group is cached. Decide whether more could ever arrive.
381		if let Some(err) = &self.abort {
382			return Poll::Ready(Err(err.clone()));
383		}
384		// `final_sequence` is one past the last possible sequence. If our
385		// floor is already at/past it, nothing else can land in range.
386		if let Some(fin) = self.final_sequence
387			&& next_sequence >= fin
388		{
389			return Poll::Ready(Ok(None));
390		}
391		Poll::Pending
392	}
393
394	/// Find a cached group by sequence, skipping tombstones and groups evicted from
395	/// the cache pool (a fetch treats those as a miss and re-fetches). Synchronous,
396	/// never blocks.
397	fn cached_group(&self, sequence: u64) -> Option<group::Consumer> {
398		self.groups
399			.iter()
400			.flatten()
401			.find(|(group, _)| group.sequence == sequence && !group.is_aborted())
402			.map(|(group, _)| group.consume())
403	}
404
405	/// The publisher's latency window, or `None` while the info is unknown (an
406	/// unaccepted [`Request`]). Bounds the aggregate subscription; see [`clamp_combined`].
407	fn latency_bound(&self) -> Option<Duration> {
408		self.info.as_ref().map(|info| info.latency_max)
409	}
410
411	/// Resolve a one-shot fetch from the track side: the cached group, or an [`Error`]
412	/// once it can never be served. A missing group is a failure ([`Error::NotFound`]), not an
413	/// end-of-stream. The handler side (a rejection, or no [`Dynamic`] at all) lives
414	/// in [`FetchState`]; [`Fetching`] polls both.
415	fn poll_fetch_cached(&self, sequence: u64) -> Poll<Result<group::Consumer>> {
416		if let Some(group) = self.cached_group(sequence) {
417			return Poll::Ready(Ok(group));
418		}
419
420		if let Some(err) = &self.abort {
421			return Poll::Ready(Err(err.clone()));
422		}
423
424		// Past the final sequence: the group can never exist.
425		if self.final_sequence.is_some_and(|fin| sequence >= fin) {
426			return Poll::Ready(Err(Error::NotFound));
427		}
428
429		Poll::Pending
430	}
431
432	/// Evict groups older than `max_age`, never evicting the max_sequence group.
433	///
434	/// Groups are in arrival order, so we can stop early when we hit a non-expired,
435	/// non-max_sequence group (everything after it arrived even later).
436	/// When max_sequence is at the front, we skip past it and tombstone expired groups
437	/// behind it.
438	///
439	/// Also reaps slots whose group the cache pool already evicted (walked before
440	/// the early exit); ones behind fresh groups stay as soft tombstones that every
441	/// read path skips, and are replaced in place if the sequence is re-fetched.
442	fn evict_expired(&mut self, now: web_async::time::Instant, max_age: Duration) {
443		for slot in self.groups.iter_mut() {
444			let Some((group, created_at)) = slot else { continue };
445
446			// Evicted by the pool: the frames are already gone, reclaim the slot
447			// and the sequence so a later fetch can re-insert it.
448			if group.is_aborted() {
449				self.duplicates.remove(&group.sequence);
450				*slot = None;
451				continue;
452			}
453
454			if Some(group.sequence) == self.max_sequence {
455				continue;
456			}
457
458			if now.duration_since(*created_at) <= max_age {
459				break;
460			}
461
462			self.duplicates.remove(&group.sequence);
463			// Take the group out of the cache and abort it, so any consumer still reading
464			// surfaces `Error::Old` instead of blocking forever on a frame that will never
465			// arrive. Without this a reader parked on an aged-out group hangs indefinitely,
466			// since the group is neither finished nor aborted.
467			if let Some((group, _)) = slot.take() {
468				let _ = group.abort(Error::Old);
469			}
470		}
471
472		// Trim leading tombstones to advance the offset.
473		while let Some(None) = self.groups.front() {
474			self.groups.pop_front();
475			self.offset += 1;
476		}
477	}
478
479	/// Pin `group` as the latest (immune to pool eviction) if it holds the track's
480	/// max_sequence, releasing the previous pin. Call after updating `max_sequence`.
481	fn pin_latest(&mut self, group: &group::Producer) {
482		if Some(group.sequence) != self.max_sequence {
483			return;
484		}
485		if let Some(prev) = self.latest_entry.take() {
486			prev.set_pinned(false);
487		}
488		if let Some(entry) = group.cache_entry() {
489			entry.set_pinned(true);
490			self.latest_entry = Some(entry);
491		}
492	}
493
494	/// Record the exclusive final sequence, rejecting a re-finish or a boundary that
495	/// would orphan already-produced groups.
496	fn set_final(&mut self, final_sequence: u64) -> Result<()> {
497		if self.final_sequence.is_some() {
498			return Err(Error::Closed);
499		}
500		if let Some(max) = self.max_sequence
501			&& final_sequence <= max
502		{
503			return Err(Error::ProtocolViolation);
504		}
505		self.final_sequence = Some(final_sequence);
506		Ok(())
507	}
508
509	/// Whether the track has reached its end: the final boundary is set and the live
510	/// edge has caught up to it, so no further group can arrive. A future boundary
511	/// (declared via [`Producer::finish_at`] ahead of the live edge) stays incomplete
512	/// until the remaining groups are produced. Drives the end-of-stream signal from
513	/// the read methods (`recv_group` / `next_group` / `read_frame` return `None`).
514	fn is_complete(&self) -> bool {
515		self.final_sequence
516			.is_some_and(|fin| self.max_sequence.map_or(0, |max| max.saturating_add(1)) >= fin)
517	}
518
519	fn poll_finished(&self) -> Poll<Result<u64>> {
520		if let Some(fin) = self.final_sequence {
521			Poll::Ready(Ok(fin))
522		} else if let Some(err) = &self.abort {
523			Poll::Ready(Err(err.clone()))
524		} else {
525			Poll::Pending
526		}
527	}
528
529	fn modify(producer: &kio::Producer<Self>) -> Result<kio::Mut<'_, Self>> {
530		producer.write().map_err(|r| r.abort.clone().unwrap_or(Error::Dropped))
531	}
532
533	/// Replace the slot of a duplicate `sequence` whose group was evicted from the
534	/// cache pool, returning the fresh producer. `Err(Duplicate)` when the cached
535	/// group is still live; `None` when no slot holds the sequence.
536	fn replace_evicted(
537		&mut self,
538		sequence: u64,
539		track: Info,
540		now: web_async::time::Instant,
541	) -> Option<Result<group::Producer>> {
542		let slot = self
543			.groups
544			.iter_mut()
545			.find(|slot| matches!(slot, Some((group, _)) if group.sequence == sequence))?;
546		let (existing, _) = slot.as_ref().unwrap();
547		if !existing.is_aborted() {
548			return Some(Err(Error::Duplicate));
549		}
550		let group = group::Producer::new(group::Info { sequence }, track);
551		*slot = Some((group.clone(), now));
552		// The replaced group can hold max_sequence when the publisher aborted the
553		// latest group itself; re-pin so the live edge stays eviction-immune.
554		self.pin_latest(&group);
555		Some(Ok(group))
556	}
557
558	/// Insert a group fetched for a [`GroupRequest`], setting the track's [`Info`]
559	/// if it isn't accepted yet. The group's timescale comes from that info, so a
560	/// fetch can serve an as-yet-unaccepted track (e.g. a relay with no live
561	/// subscription). The group lands in the cache so a waiting
562	/// [`Fetching`] resolves via [`Self::poll_fetch`].
563	fn insert_group_request(&mut self, sequence: u64, info: Option<Info>) -> Result<group::Producer> {
564		if let Some(err) = &self.abort {
565			return Err(err.clone());
566		}
567		if let Some(fin) = self.final_sequence
568			&& sequence >= fin
569		{
570			return Err(Error::Closed);
571		}
572
573		// Adopt the supplied info only if the track hasn't been accepted yet, binding
574		// it to this track's broadcast so its groups reach the shared pool.
575		let now = web_async::time::Instant::now();
576		let broadcast = self.broadcast.clone();
577		let info = self
578			.info
579			.get_or_insert_with(|| {
580				let mut info = info.unwrap_or_default();
581				info.broadcast = broadcast;
582				info
583			})
584			.clone();
585
586		if !self.duplicates.insert(sequence) {
587			// A pool-evicted group can be re-fetched into its old slot.
588			return self
589				.replace_evicted(sequence, info, now)
590				.unwrap_or(Err(Error::Duplicate));
591		}
592
593		let latency_max = info.latency_max;
594		let group = group::Producer::new(group::Info { sequence }, info);
595		self.max_sequence = Some(self.max_sequence.unwrap_or(0).max(sequence));
596		self.groups.push_back(Some((group.clone(), now)));
597		self.pin_latest(&group);
598		self.evict_expired(now, latency_max);
599		Ok(group)
600	}
601}
602
603/// A producer for a track, used to create new groups.
604#[derive(Clone)]
605pub struct Producer {
606	name: Arc<str>,
607	// The parent broadcast's info, inherited from [`broadcast::Producer::create_track`].
608	// Top link of the ownership chain; carried for identity and future inheritance.
609	broadcast: Arc<broadcast::Info>,
610	state: kio::Producer<TrackState>,
611	prev_subscription: Option<Subscription>,
612	// Ingress stats scope, inherited from a tagged [`broadcast::Producer`]. Bumped as
613	// one subscription on tag and closed when the last producer clone drops. Empty
614	// (no-op) for an untagged broadcast.
615	stats: stats::Scope,
616}
617
618impl Producer {
619	/// Build a producer for the given track metadata.
620	///
621	/// Crate-private: tracks are born from their broadcast via
622	/// [`broadcast::Producer::create_track`] (or served on demand through a
623	/// [`Request`]), which threads the broadcast's `Arc<broadcast::Info>` down. The
624	/// track binds it onto its [`Info`] so every group reaches the shared cache pool
625	/// by walking `track.broadcast.origin.pool`.
626	pub(crate) fn new(
627		broadcast: Arc<broadcast::Info>,
628		name: impl Into<Arc<str>>,
629		info: impl Into<Option<Info>>,
630	) -> Self {
631		let mut info = info.into().unwrap_or_default();
632		info.broadcast = broadcast.clone();
633		Self {
634			name: name.into(),
635			state: kio::Producer::new(TrackState {
636				info: Some(info),
637				broadcast: broadcast.clone(),
638				..Default::default()
639			}),
640			broadcast,
641			prev_subscription: None,
642			stats: stats::Scope::default(),
643		}
644	}
645
646	/// Attach the parent broadcast's ingress stats scope, counting this track as one
647	/// ingress subscription (closed when the last producer clone drops). Called by a
648	/// tagged [`broadcast::Producer`] when it creates the track.
649	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
650		scope.open_subscription();
651		self.stats = scope;
652		self
653	}
654
655	/// The track's name, unique within its broadcast.
656	pub fn name(&self) -> &str {
657		&self.name
658	}
659
660	/// The parent broadcast this track belongs to.
661	pub fn broadcast(&self) -> &broadcast::Info {
662		&self.broadcast
663	}
664
665	/// Create a new group with the given sequence number.
666	pub fn create_group(&mut self, group: group::Info) -> Result<group::Producer> {
667		let mut state = self.modify()?;
668		if let Some(fin) = state.final_sequence
669			&& group.sequence >= fin
670		{
671			return Err(Error::Closed);
672		}
673		let info = state.info.as_ref().unwrap();
674		let track = info.clone();
675		let latency_max = info.latency_max;
676		let now = web_async::time::Instant::now();
677
678		if !state.duplicates.insert(group.sequence) {
679			// A pool-evicted group can be re-created into its old slot.
680			return state
681				.replace_evicted(group.sequence, track, now)
682				.unwrap_or(Err(Error::Duplicate));
683		}
684
685		let group = group::Producer::new(group, track).with_meter(self.stats.meter());
686		state.max_sequence = Some(state.max_sequence.unwrap_or(0).max(group.sequence));
687		state.groups.push_back(Some((group.clone(), now)));
688		state.pin_latest(&group);
689		state.evict_expired(now, latency_max);
690
691		Ok(group)
692	}
693
694	/// Create a new group with the next sequence number.
695	pub fn append_group(&mut self) -> Result<group::Producer> {
696		let mut state = self.modify()?;
697		let sequence = match state.max_sequence {
698			Some(s) => s.checked_add(1).ok_or(coding::BoundsExceeded)?,
699			None => 0,
700		};
701		if let Some(fin) = state.final_sequence
702			&& sequence >= fin
703		{
704			return Err(Error::Closed);
705		}
706
707		let info = state.info.as_ref().unwrap();
708		let track = info.clone();
709		let latency_max = info.latency_max;
710
711		let group = group::Producer::new(group::Info { sequence }, track).with_meter(self.stats.meter());
712
713		let now = web_async::time::Instant::now();
714		state.duplicates.insert(sequence);
715		state.max_sequence = Some(sequence);
716		state.groups.push_back(Some((group.clone(), now)));
717		state.pin_latest(&group);
718		state.evict_expired(now, latency_max);
719
720		Ok(group)
721	}
722
723	/// Append a datagram with the next sequence number, returning the assigned sequence.
724	///
725	/// A datagram is delivered best-effort over a single QUIC datagram, parallel to the
726	/// track's groups but drawing from the same sequence namespace (so interleaving with
727	/// [`Self::append_group`] never reuses a number). There is no group fallback: each
728	/// session drops (with a debug log) any datagram whose encoded body exceeds the
729	/// transport's datagram size, and sessions that can't carry datagrams at all (IETF
730	/// moq-transport, moq-lite before 05, or stream-only transports like WebSocket) never
731	/// deliver them. Keep payloads well under the 1200-byte minimum path MTU. An origin
732	/// publisher uses this; a relay preserving upstream numbering uses
733	/// [`Self::write_datagram`].
734	pub fn append_datagram<B: crate::IntoBytes>(&mut self, timestamp: Timestamp, payload: B) -> Result<u64> {
735		let payload = payload.into_bytes();
736		if payload.len() > super::datagram::MAX_DATAGRAM_PAYLOAD {
737			return Err(Error::FrameTooLarge);
738		}
739		// Resolved before the state guard borrows `self`.
740		let meter = self.stats.meter();
741		let mut state = self.modify()?;
742		// Normalize into the track's timescale, like frames (see `group::Producer::create_frame`).
743		let timescale = state.info.as_ref().unwrap().timescale;
744		let timestamp = timestamp.convert(timescale).map_err(|_| Error::TimestampMismatch)?;
745		let sequence = match state.max_sequence {
746			Some(s) => s.checked_add(1).ok_or(coding::BoundsExceeded)?,
747			None => 0,
748		};
749		if let Some(fin) = state.final_sequence
750			&& sequence >= fin
751		{
752			return Err(Error::Closed);
753		}
754		state.max_sequence = Some(sequence);
755		meter.datagram(payload.len() as u64);
756		state.push_datagram(Datagram {
757			sequence,
758			timestamp,
759			payload,
760		});
761		Ok(sequence)
762	}
763
764	/// Write a datagram with an explicit sequence number.
765	///
766	/// Preserves the supplied sequence (bumping the shared `max_sequence` if needed), so a
767	/// relay can forward a datagram without renumbering it. Most origin publishers want
768	/// [`Self::append_datagram`] instead.
769	pub fn write_datagram(&mut self, mut datagram: Datagram) -> Result<()> {
770		if datagram.payload.len() > super::datagram::MAX_DATAGRAM_PAYLOAD {
771			return Err(Error::FrameTooLarge);
772		}
773		// Resolved before the state guard borrows `self`.
774		let meter = self.stats.meter();
775		let mut state = self.modify()?;
776		// Normalize into the track's timescale, like frames (see `group::Producer::create_frame`).
777		let timescale = state.info.as_ref().unwrap().timescale;
778		datagram.timestamp = datagram
779			.timestamp
780			.convert(timescale)
781			.map_err(|_| Error::TimestampMismatch)?;
782		if let Some(fin) = state.final_sequence
783			&& datagram.sequence >= fin
784		{
785			return Err(Error::Closed);
786		}
787		state.max_sequence = Some(state.max_sequence.unwrap_or(0).max(datagram.sequence));
788		meter.datagram(datagram.payload.len() as u64);
789		state.push_datagram(datagram);
790		Ok(())
791	}
792
793	/// Create a group with a single frame, at the given presentation timestamp.
794	///
795	/// The timestamp is converted into the track's timescale. For data without
796	/// a presentation time, pass [`Timestamp::now`] explicitly.
797	pub fn write_frame<B: crate::IntoBytes>(&mut self, timestamp: Timestamp, frame: B) -> Result<()> {
798		let mut group = self.append_group()?;
799		group.write_frame(timestamp, frame)?;
800		group.finish()?;
801		Ok(())
802	}
803
804	/// Mark the track as finished after the last appended group.
805	///
806	/// Sets the final sequence to one past the current max_sequence.
807	/// No new groups at or above this sequence can be appended.
808	/// NOTE: Old groups with lower sequence numbers can still arrive.
809	pub fn finish(&mut self) -> Result<()> {
810		let mut state = self.modify()?;
811		let final_sequence = match state.max_sequence {
812			Some(max) => max.checked_add(1).ok_or(coding::BoundsExceeded)?,
813			None => 0,
814		};
815		state.set_final(final_sequence)
816	}
817
818	/// Declare the track's exclusive final sequence, possibly ahead of the live edge.
819	///
820	/// `final_sequence` is the first sequence that will never be produced, so a track
821	/// whose last group is 89 finishes at `90`. Passing a boundary beyond the current
822	/// max_sequence records a known ending before the remaining groups arrive (e.g.
823	/// learning a track ends at group 89 while only 87 has been received). The boundary
824	/// must be strictly greater than the highest produced group, otherwise it would
825	/// orphan groups that already exist ([`Error::ProtocolViolation`]).
826	///
827	/// Groups below `final_sequence` may still be created afterwards; groups at or above
828	/// it are rejected. Consumers only see end-of-stream once the live edge reaches the
829	/// boundary. Use [`Self::finish`] to finish exactly at the live edge.
830	pub fn finish_at(&mut self, final_sequence: u64) -> Result<()> {
831		self.modify()?.set_final(final_sequence)
832	}
833
834	/// The exclusive final sequence, once [`Self::finish`] or [`Self::finish_at`] declared one.
835	///
836	/// `None` while the track is still open ended. Both methods reject a second boundary, so
837	/// callers that may have already declared one check here first.
838	pub fn final_sequence(&self) -> Option<u64> {
839		self.state.read().final_sequence
840	}
841
842	/// Abort the track with the given error.
843	///
844	/// Consumes the handle, since nothing can be written to an aborted track. Drops the
845	/// cached groups so a stale [`Consumer`] can't pin them (and their frame buffers) in
846	/// memory forever. Consumers that haven't drained yet surface the abort error instead
847	/// of the leftover cache. Child groups are independent: a consumer that already pulled
848	/// a [`group::Consumer`] keeps its own handle and can finish reading it.
849	///
850	/// [`finish`](Self::finish) is deliberately not terminal: it declares the final
851	/// sequence, and lower-numbered groups may still be written afterwards.
852	pub fn abort(self, err: Error) -> Result<()> {
853		let mut guard = self.modify()?;
854		guard.abort = Some(err);
855		guard.groups.clear();
856		guard.datagrams.clear();
857		guard.duplicates.clear();
858		guard.latest_entry = None;
859		guard.close();
860		Ok(())
861	}
862
863	/// Block until there are no active consumers.
864	pub async fn unused(&self) -> Result<()> {
865		self.state.unused().await.map_err(|_| self.abort_reason())
866	}
867
868	/// Block until there is at least one active consumer.
869	pub async fn used(&self) -> Result<()> {
870		self.state.used().await.map_err(|_| self.abort_reason())
871	}
872
873	/// Block until the track is closed or aborted, returning the cause.
874	pub async fn closed(&self) -> Error {
875		kio::wait(|waiter| self.poll_closed(waiter)).await
876	}
877
878	/// Poll until the track is closed or aborted; ready with the cause.
879	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Error> {
880		self.state.poll_closed(waiter).map(|()| self.abort_reason())
881	}
882
883	/// The recorded abort reason, or [`Error::Dropped`] if the track closed without one.
884	fn abort_reason(&self) -> Error {
885		self.state.read().abort.clone().unwrap_or(Error::Dropped)
886	}
887
888	/// Return true if the track has been closed.
889	pub fn is_closed(&self) -> bool {
890		self.state.read().is_closed()
891	}
892
893	/// Return the latest sequence number successfully appended to the track.
894	pub fn latest(&self) -> Option<u64> {
895		self.state.read().max_sequence
896	}
897
898	/// Return true if this is the same track.
899	pub fn is_clone(&self, other: &Self) -> bool {
900		self.state.same_channel(&other.state)
901	}
902
903	/// Create a weak reference that doesn't prevent auto-close.
904	pub(crate) fn weak(&self) -> TrackWeak {
905		TrackWeak {
906			name: self.name.clone(),
907			state: self.state.weak(),
908		}
909	}
910
911	/// Create a [`Demand`]: a cloneable, watch-only handle to this track's
912	/// subscriber demand.
913	///
914	/// Lets a publisher gate work (e.g. on-demand capture) on whether anyone is
915	/// subscribed, without the ability to publish frames or close the track. The
916	/// handle is weak, so holding one neither keeps the track alive nor pins its
917	/// cached groups.
918	pub fn demand(&self) -> Demand {
919		Demand {
920			name: self.name.clone(),
921			state: self.state.weak(),
922		}
923	}
924
925	/// Get a consumer handle for this in-process track.
926	///
927	/// Unlike a wire subscription, the info is already known, so a subscription
928	/// opened from this handle resolves immediately.
929	pub fn consume(&self) -> Consumer {
930		Consumer::plain(self.name.clone(), self.state.consume())
931	}
932
933	/// Subscribing to this in-process track, resolving synchronously.
934	///
935	/// The info is fixed at creation, so there's nothing to wait for (no
936	/// SUBSCRIBE_OK round trip). Pass `None` for [`Subscription::default`].
937	pub fn subscribe(&self, subscription: impl Into<Option<Subscription>>) -> Subscriber {
938		let preferences = subscription.into().unwrap_or_default();
939
940		// Info is fixed at creation and survives a close/abort, so read it without
941		// requiring a live producer state. If the track already ended, the returned
942		// subscriber surfaces the close/abort on its first read; the preferences are
943		// simply never registered (nothing aggregates them anymore).
944		let info = self
945			.state
946			.read()
947			.info
948			.as_ref()
949			.expect("producer always has info")
950			.clone();
951		let subscription = kio::Producer::new(preferences);
952		register_subscription(self.state.read(), &subscription);
953
954		Subscriber {
955			name: self.name.clone(),
956			info,
957			inner: SubscriberKind::Plain(PlainSubscriber {
958				state: self.state.consume(),
959				subscription,
960				index: 0,
961				datagram_index: 0,
962				min_sequence: 0,
963				next_sequence: 0,
964				end_sequence: None,
965			}),
966			// A producer-side (in-process) subscribe is not egress: stay untagged.
967			stats: stats::Scope::default(),
968			_stats_sub: stats::Subscription::default(),
969		}
970	}
971
972	/// Block until the aggregate subscription changes, then return the new value.
973	///
974	/// Yields the most demanding request across all live subscribers, or `None`
975	/// once the last one drops. Used by relays to forward downstream demand
976	/// upstream (e.g. SUBSCRIBE_UPDATE).
977	pub async fn subscription_changed(&mut self) -> Result<Option<Subscription>> {
978		kio::wait(|waiter| self.poll_subscription_changed(waiter)).await
979	}
980
981	/// A non-blocking snapshot of the current aggregate subscription, or `None`
982	/// when there are no live subscribers. Unlike [`Self::subscription`], this
983	/// doesn't wait for a change or advance the change cursor.
984	///
985	/// The aggregate's [`Subscription::latency_max`] is clamped to this track's
986	/// [`Info::latency_max`]: no subscriber can wait for a late group longer than the
987	/// publisher keeps it.
988	pub fn subscription(&self) -> Option<Subscription> {
989		let state = self.state.read();
990		let (subs, bound) = (state.subscriptions.clone(), state.latency_bound());
991		drop(state);
992		snapshot_subscription(&subs, bound)
993	}
994
995	/// Poll counterpart to [`subscription_changed`](Self::subscription_changed): the
996	/// aggregate subscription whenever it changes, or `None` once nobody is subscribed.
997	/// Errors once the track is aborted.
998	pub fn poll_subscription_changed(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Subscription>>> {
999		// Surface an abort as the stream ending. `poll_closed` parks on the closed
1000		// waiters, so per-group churn on the track state never wakes this poll.
1001		if self.state.poll_closed(waiter).is_ready() {
1002			let abort = self.state.read().abort.clone();
1003			return Poll::Ready(Err(abort.unwrap_or(Error::Dropped)));
1004		}
1005
1006		// Read the bound before locking `subs`, so the aggregation never nests the two locks.
1007		let state = self.state.read();
1008		let (subs, bound) = (state.subscriptions.clone(), state.latency_bound());
1009		drop(state);
1010
1011		let prev = &self.prev_subscription;
1012		let mut combined = None;
1013		let mut guard = match subs.poll(waiter, |subs| {
1014			let next = combined_subscription(subs, bound, waiter);
1015			if &next == prev {
1016				Poll::Pending
1017			} else {
1018				combined = next;
1019				Poll::Ready(())
1020			}
1021		}) {
1022			Poll::Ready(guard) => guard,
1023			Poll::Pending => return Poll::Pending,
1024		};
1025		// The aggregate changed: prune any closed subscribers now that we hold the lock.
1026		guard.retain(|sub| !sub.is_closed());
1027		drop(guard);
1028		self.prev_subscription = combined.clone();
1029		Poll::Ready(Ok(combined))
1030	}
1031
1032	/// Poll for the producer becoming unused (every consumer dropped).
1033	pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<()> {
1034		self.state.poll_unused(waiter).map(|_| ())
1035	}
1036
1037	/// Create a [`Dynamic`] handle that serves on-demand fetches of uncached
1038	/// (old) groups. Most producers never need this; a relay creates one to fetch
1039	/// past groups from upstream.
1040	pub fn dynamic(&self) -> Dynamic {
1041		Dynamic::new(self.name.clone(), self.state.clone())
1042	}
1043
1044	fn modify(&self) -> Result<kio::Mut<'_, TrackState>> {
1045		TrackState::modify(&self.state)
1046	}
1047}
1048
1049/// Pop the next queued group fetch off the fetch queue and wrap it in a
1050/// [`GroupRequest`] bound to a fresh producer handle. Shared by every
1051/// [`Dynamic`] handle on the track.
1052fn poll_requested_group(
1053	state: &kio::Producer<TrackState>,
1054	fetch: &kio::Shared<FetchState>,
1055	waiter: &kio::Waiter,
1056) -> Poll<Result<GroupRequest>> {
1057	// Prefer serving a queued fetch, even if the track has since aborted.
1058	if let Poll::Ready(mut guard) = fetch.poll(waiter, |fetch| {
1059		if fetch.has_queued() {
1060			Poll::Ready(())
1061		} else {
1062			Poll::Pending
1063		}
1064	}) {
1065		let sequence = guard.pop().expect("predicate guaranteed a request");
1066		// The popped attempt stays pending, so a fetch in the window between hand-off
1067		// and accept joins it instead of queueing a duplicate.
1068		// `GroupRequest::{accept, reject, drop}` removes the entry.
1069		let pending = guard.get(&sequence).expect("popped key must be pending");
1070		let priority = pending.priority;
1071		let result = pending.result.clone();
1072		drop(guard);
1073		return Poll::Ready(Ok(GroupRequest {
1074			state: state.clone(),
1075			fetch: fetch.clone(),
1076			sequence,
1077			priority,
1078			result,
1079			done: false,
1080		}));
1081	}
1082
1083	// No fetch queued: surface a track abort so the handler loop can exit.
1084	match state.poll_ref(waiter, |state| match &state.abort {
1085		Some(err) => Poll::Ready(err.clone()),
1086		None => Poll::Pending,
1087	}) {
1088		Poll::Ready(Ok(err)) => Poll::Ready(Err(err)),
1089		Poll::Ready(Err(closed)) => Poll::Ready(Err(closed.abort.clone().unwrap_or(Error::Dropped))),
1090		Poll::Pending => Poll::Pending,
1091	}
1092}
1093
1094/// Serves on-demand fetches of uncached (old) groups for a track, the group-level
1095/// analogue of [`broadcast::Dynamic`].
1096///
1097/// Most tracks never serve old content, so this capability lives on a dedicated
1098/// handle rather than [`Producer`]: a relay creates one (via
1099/// [`Producer::dynamic`] or [`Request::dynamic`]) to pull past groups
1100/// from upstream. While at least one is alive the track will block a cache-miss
1101/// [`Consumer::fetch_group`] waiting to be served; with none, an accepted track's
1102/// miss fails fast with [`Error::NotFound`].
1103pub struct Dynamic {
1104	name: Arc<str>,
1105	// Kept to insert served groups into the cache and observe track abort.
1106	state: kio::Producer<TrackState>,
1107	// The fetch queue this handle drains; its `dynamic` count gates `fetch_group`.
1108	fetch: kio::Shared<FetchState>,
1109}
1110
1111impl Dynamic {
1112	fn new(name: Arc<str>, state: kio::Producer<TrackState>) -> Self {
1113		let fetch = state.read().fetch.clone();
1114		fetch.lock().add_handler();
1115		Self { name, state, fetch }
1116	}
1117
1118	/// The track's name, unique within its broadcast.
1119	pub fn name(&self) -> &str {
1120		&self.name
1121	}
1122
1123	/// Block until a consumer fetches a group that isn't cached, returning a
1124	/// [`GroupRequest`] to serve via [`GroupRequest::accept`].
1125	///
1126	/// A relay issues a wire FETCH first; an origin already has the group cached, so
1127	/// the fetch resolves without ever reaching here. Errors once the track is aborted.
1128	pub async fn requested_group(&self) -> Result<GroupRequest> {
1129		kio::wait(|waiter| self.poll_requested_group(waiter)).await
1130	}
1131
1132	/// Poll counterpart to [`requested_group`](Self::requested_group).
1133	pub fn poll_requested_group(&self, waiter: &kio::Waiter) -> Poll<Result<GroupRequest>> {
1134		poll_requested_group(&self.state, &self.fetch, waiter)
1135	}
1136
1137	/// Poll for the track becoming unused (every consumer dropped).
1138	pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<()> {
1139		self.state.poll_unused(waiter).map(|_| ())
1140	}
1141}
1142
1143impl Clone for Dynamic {
1144	fn clone(&self) -> Self {
1145		// Count each live handle (mirrors `broadcast::Dynamic`).
1146		self.fetch.lock().add_handler();
1147		Self {
1148			name: self.name.clone(),
1149			state: self.state.clone(),
1150			fetch: self.fetch.clone(),
1151		}
1152	}
1153}
1154
1155impl Drop for Dynamic {
1156	fn drop(&mut self) {
1157		// Unlike `broadcast::Dynamic`, dropping the last handle doesn't abort the track:
1158		// a live `Producer` may still be serving the subscription. It just stops fetch
1159		// serving. Queued attempts no handler will ever pop are dropped, closing their
1160		// result channels so every joined `Fetching` resolves NotFound; an attempt
1161		// already handed to a handler stays, resolved by its `GroupRequest` instead.
1162		let mut fetch = self.fetch.lock();
1163		if fetch.remove_handler() {
1164			fetch.drain_queued();
1165		}
1166	}
1167}
1168
1169impl Drop for Producer {
1170	fn drop(&mut self) {
1171		// The last producer going away without finishing is an abrupt teardown:
1172		// release the cached groups so a stale consumer can't pin them (and their
1173		// frame buffers) forever, the same as an explicit abort. A cleanly
1174		// finished track keeps its cache so consumers can still drain it.
1175		if !self.state.is_last() {
1176			return;
1177		}
1178		// The last ingress producer closing the track ends its subscription.
1179		self.stats.close_subscription();
1180		if let Ok(mut state) = self.state.write()
1181			&& state.final_sequence.is_none()
1182		{
1183			// Dropped without finish() or abort(), so consumers will see
1184			// Error::Dropped instead of a clean end. Deliberate ends go through
1185			// finish()/abort().
1186			tracing::warn!(
1187				track = %self.name(),
1188				"track::Producer dropped without finish() or abort()"
1189			);
1190			state.groups.clear();
1191			state.datagrams.clear();
1192			state.duplicates.clear();
1193			state.latest_entry = None;
1194		}
1195	}
1196}
1197
1198/// Aggregate every live subscriber's preferences into the most demanding request.
1199///
1200/// Read-only: iterates the subscriptions immutably and registers `waiter` on each, so a
1201/// preference update (or a subscriber dropping) wakes the caller's poll. Callers decide
1202/// readiness from the returned value, then prune closed subscribers through the `Mut`.
1203fn combined_subscription(subs: &Subscriptions, bound: Option<Duration>, waiter: &kio::Waiter) -> Option<Subscription> {
1204	let mut combined = None;
1205	for sub in subs.iter() {
1206		// A closed consumer means the subscriber dropped: it holds no live demand.
1207		// `Consumer::poll` evaluates the closure before the closed flag, so it would
1208		// still replay the final value into the aggregate; skip it explicitly so a
1209		// departed subscriber can't keep the aggregate pinned to its last request.
1210		if sub.is_closed() {
1211			continue;
1212		}
1213		if let Poll::Ready(Ok(sub)) = sub.poll(waiter, |sub| sub.poll_combined(&combined)) {
1214			combined = Some(sub);
1215		}
1216	}
1217	clamp_combined(combined, bound)
1218}
1219
1220/// A non-blocking aggregate of the current subscriptions, without arming any waiter.
1221fn snapshot_subscription(subs: &kio::Shared<Subscriptions>, bound: Option<Duration>) -> Option<Subscription> {
1222	let mut combined: Option<Subscription> = None;
1223	for sub in subs.read().iter() {
1224		// Skip dropped subscribers, matching `combined_subscription`.
1225		if sub.is_closed() {
1226			continue;
1227		}
1228		if let Poll::Ready(merged) = sub.read().poll_combined(&combined) {
1229			combined = Some(merged);
1230		}
1231	}
1232	clamp_combined(combined, bound)
1233}
1234
1235/// Clamp the aggregate's latency budget to the publisher's window: nobody can wait for a
1236/// late group longer than the publisher keeps it around.
1237///
1238/// The single clamp point. Subscribers hold their preferences verbatim, so what they asked
1239/// for stays readable, and clamping the aggregate is equivalent to clamping each subscriber
1240/// first (`min` distributes over the `max` that combines them). `bound` is `None` on a track
1241/// whose info isn't known yet (an unaccepted [`Request`]), which imposes no window.
1242fn clamp_combined(combined: Option<Subscription>, bound: Option<Duration>) -> Option<Subscription> {
1243	let mut combined = combined?;
1244	if let Some(bound) = bound {
1245		combined.latency_max = combined.latency_max.min(bound);
1246	}
1247	Some(combined)
1248}
1249
1250/// Register a subscription if the track is live: clone the shared list out of the
1251/// state, release the track lock, then push under the list's own lock. A closed
1252/// track skips the push; nothing aggregates the preferences anymore.
1253fn register_subscription(state: kio::Ref<'_, TrackState>, subscription: &kio::Producer<Subscription>) {
1254	if state.is_closed() {
1255		return;
1256	}
1257	let subs = state.subscriptions.clone();
1258	drop(state);
1259	subs.lock().push(subscription.consume());
1260}
1261
1262/// A weak reference to a track that doesn't prevent auto-close.
1263#[derive(Clone)]
1264pub(crate) struct TrackWeak {
1265	name: Arc<str>,
1266	state: kio::ProducerWeak<TrackState>,
1267}
1268
1269impl TrackWeak {
1270	pub fn consume(&self) -> Consumer {
1271		Consumer::plain(self.name.clone(), self.state.consume())
1272	}
1273
1274	/// The shared name handle, for use as a broadcast lookup key (clone is a
1275	/// refcount bump, and the same `Arc` is shared with the track's handles).
1276	pub(crate) fn name(&self) -> &Arc<str> {
1277		&self.name
1278	}
1279
1280	/// Whether anyone is consuming the track right now. A closed track doesn't
1281	/// count even if consumers linger to drain its cache: no new work is owed.
1282	pub(crate) fn is_used(&self) -> bool {
1283		!self.state.is_closed() && self.state.is_used()
1284	}
1285
1286	/// Park `waiter` for the next consumer appearing; a no-op once one exists.
1287	/// Feeds [`crate::broadcast::Demand`], which recomputes on wake.
1288	pub(crate) fn poll_used(&self, waiter: &kio::Waiter) {
1289		let _ = self.state.poll_used(waiter);
1290	}
1291
1292	/// Park `waiter` for the last consumer (or the track) going away; a no-op
1293	/// once none remain. Feeds [`crate::broadcast::Demand`].
1294	pub(crate) fn poll_unused(&self, waiter: &kio::Waiter) {
1295		let _ = self.state.poll_unused(waiter);
1296	}
1297}
1298
1299impl super::WeakEntry for TrackWeak {
1300	fn is_closed(&self) -> bool {
1301		self.state.is_closed()
1302	}
1303
1304	fn same_channel(&self, other: &Self) -> bool {
1305		self.state.same_channel(&other.state)
1306	}
1307}
1308
1309/// A cloneable, watch-only handle to a track's subscriber demand.
1310///
1311/// Obtained from [`Producer::demand`]. A publisher uses it to react to
1312/// whether anyone is subscribed (on-demand capture / encoding) without being able
1313/// to publish frames or close the track. It's a weak handle, so it neither keeps
1314/// the track alive nor pins its cached groups; once the owning [`Producer`]
1315/// goes away, [`used`](Self::used) / [`unused`](Self::unused) report the track's
1316/// closure.
1317#[derive(Clone)]
1318pub struct Demand {
1319	name: Arc<str>,
1320	state: kio::ProducerWeak<TrackState>,
1321}
1322
1323impl Demand {
1324	/// The track name this handle is bound to.
1325	pub fn name(&self) -> &str {
1326		&self.name
1327	}
1328
1329	/// Block until there is at least one active consumer.
1330	pub async fn used(&self) -> Result<()> {
1331		self.state.used().await.map_err(|_| self.abort_reason())
1332	}
1333
1334	/// Block until there are no active consumers.
1335	pub async fn unused(&self) -> Result<()> {
1336		self.state.unused().await.map_err(|_| self.abort_reason())
1337	}
1338
1339	/// Block until the track is closed or aborted, returning the cause.
1340	pub async fn closed(&self) -> Error {
1341		self.state.closed().await;
1342		self.abort_reason()
1343	}
1344
1345	/// The recorded abort reason, or [`Error::Dropped`] if the track closed without one.
1346	fn abort_reason(&self) -> Error {
1347		self.state.read().abort.clone().unwrap_or(Error::Dropped)
1348	}
1349}
1350
1351/// A handle to a single track within a broadcast.
1352///
1353/// Obtained from [`broadcast::Consumer::track`]. Holding it sends nothing
1354/// to the publisher; it just names a track you can [`subscribe`](Self::subscribe)
1355/// to (a live, ongoing stream of groups) later. The same handle can be subscribed
1356/// to multiple times, and clones are cheap.
1357///
1358/// A track reached through a route-fed broadcast is *spliced*: it is backed by one
1359/// or more per-session tracks joined at group boundaries, and this handle reads
1360/// across them transparently.
1361#[derive(Clone)]
1362pub struct Consumer {
1363	name: Arc<str>,
1364	inner: ConsumerKind,
1365	// Egress stats scope, set by a tagged [`broadcast::Consumer`] via
1366	// [`Self::with_stats`]. Empty (no-op) for an untagged track.
1367	stats: stats::Scope,
1368}
1369
1370#[derive(Clone)]
1371enum ConsumerKind {
1372	Plain(kio::Consumer<TrackState>),
1373	Spliced(super::resume::Consumer),
1374}
1375
1376impl Consumer {
1377	fn plain(name: Arc<str>, state: kio::Consumer<TrackState>) -> Self {
1378		Self {
1379			name,
1380			inner: ConsumerKind::Plain(state),
1381			stats: stats::Scope::default(),
1382		}
1383	}
1384
1385	/// A consumer over a spliced logical track (a route-fed broadcast's track).
1386	pub(crate) fn spliced(name: Arc<str>, resume: super::resume::Consumer) -> Self {
1387		Self {
1388			name,
1389			inner: ConsumerKind::Spliced(resume),
1390			stats: stats::Scope::default(),
1391		}
1392	}
1393
1394	/// Attach an egress stats scope, inherited by the subscriptions, fetches, and
1395	/// groups derived from this handle. Called by a tagged [`broadcast::Consumer`].
1396	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
1397		self.stats = scope;
1398		self
1399	}
1400
1401	/// The track name this handle is bound to.
1402	pub fn name(&self) -> &str {
1403		&self.name
1404	}
1405
1406	/// Open a live subscription.
1407	///
1408	/// Registers the subscription on the track and returns a [`kio::Pending`] that resolves to the
1409	/// [`Subscriber`] once the track info is available, or the track's abort error (or
1410	/// [`Error::Dropped`]) if it is already closed.
1411	pub fn subscribe(&self, subscription: impl Into<Option<Subscription>>) -> kio::Pending<Subscribing> {
1412		let subscription = kio::Producer::new(subscription.into().unwrap_or_default());
1413
1414		let inner = match &self.inner {
1415			ConsumerKind::Plain(state) => {
1416				// Register the subscription if the track is live. If it is already closed, the
1417				// returned future resolves to the abort error via `Subscribing::poll_ok`.
1418				register_subscription(state.read(), &subscription);
1419				SubscribingKind::Plain(state.clone())
1420			}
1421			// A spliced subscription registers per segment once the subscriber polls.
1422			ConsumerKind::Spliced(resume) => SubscribingKind::Spliced(resume.clone()),
1423		};
1424
1425		kio::Pending::new(Subscribing {
1426			name: self.name.clone(),
1427			inner,
1428			subscription,
1429			stats: self.stats.clone(),
1430		})
1431	}
1432
1433	// Peek at a cached group by sequence without blocking, or `None` if it isn't in the
1434	// cache. A test hook for asserting cache state; the library reads
1435	// `TrackState::cached_group` directly, and callers want `fetch_group`.
1436	#[cfg(test)]
1437	pub(crate) fn peek_group(&self, sequence: u64) -> Option<group::Consumer> {
1438		match &self.inner {
1439			ConsumerKind::Plain(state) => state.read().cached_group(sequence),
1440			// Spliced tracks have no cache of their own; peek the newest segment
1441			// via `fetch_group` instead.
1442			ConsumerKind::Spliced(_) => None,
1443		}
1444	}
1445
1446	/// Fetching a single past group, without holding a live subscription.
1447	///
1448	/// Returns a [`kio::Pending`] that resolves to the [`group::Consumer`]:
1449	/// immediately if the group is cached, otherwise once a [`Dynamic`] serves
1450	/// the request (a wire FETCH for a relay). `options` accepts `None`, a [`group::Fetch`],
1451	/// or `group::Fetch::default()`.
1452	///
1453	/// The returned future resolves to [`Error::NotFound`] when the group can never be served
1454	/// (past the final sequence, or no [`Dynamic`] on the track), or the track's abort error
1455	/// if it's already closed. Concurrent fetches for the same sequence coalesce onto one
1456	/// handler request.
1457	pub fn fetch_group(&self, sequence: u64, options: impl Into<Option<group::Fetch>>) -> kio::Pending<Fetching> {
1458		let options = options.into().unwrap_or_default();
1459
1460		// One fetch per calling context, counted here (coalesced upstream work is
1461		// still one request served). Independent of `subscriptions` and the viewer
1462		// refcount.
1463		self.stats.fetch();
1464
1465		let state = match &self.inner {
1466			ConsumerKind::Plain(state) => state,
1467			// Spliced: routed to the newest segment's (plain) track, waiting for a
1468			// segment to exist if no route has served the track yet.
1469			ConsumerKind::Spliced(resume) => {
1470				return kio::Pending::new(Fetching {
1471					inner: FetchingKind::Spliced(resume.fetch_group(sequence, options)),
1472					stats: self.stats.clone(),
1473				});
1474			}
1475		};
1476
1477		let mut result = None;
1478
1479		// Queue a request only when the group isn't already resolvable from the track
1480		// (cached, aborted, or past-final all resolve through `Fetching::poll` without
1481		// a queue entry).
1482		let (fetch, unresolved) = {
1483			let state = state.read();
1484			(state.fetch.clone(), state.poll_fetch_cached(sequence).is_pending())
1485		};
1486
1487		if unresolved {
1488			let mut fetch = fetch.lock();
1489			if let Some(pending) = fetch.join(&sequence) {
1490				// Join the in-flight attempt for this sequence (queued or already being
1491				// served): share its result channel, raising its priority if ours is higher.
1492				pending.priority = pending.priority.max(options.priority);
1493				result = Some(pending.result.consume());
1494			} else {
1495				// Queue a new attempt. The handler gate is atomic with a handler
1496				// dropping (no fetch stranded on a queue nobody drains); with no
1497				// handler, `Fetching::poll` fails fast instead.
1498				let producer = kio::Producer::<FetchOutcome>::default();
1499				let consumer = producer.consume();
1500				let attempt = PendingFetch {
1501					priority: options.priority,
1502					result: producer,
1503				};
1504				if fetch.insert(sequence, attempt).is_ok() {
1505					result = Some(consumer);
1506				}
1507			}
1508		}
1509
1510		kio::Pending::new(Fetching {
1511			inner: FetchingKind::Plain {
1512				state: state.clone(),
1513				fetch,
1514				sequence,
1515				result,
1516			},
1517			stats: self.stats.clone(),
1518		})
1519	}
1520
1521	/// Resolve the track's [`Info`] without subscribing.
1522	///
1523	/// A [`Consumer`] is a lazy handle, so the info may not be known yet: this waits
1524	/// for the producer to [`Request::accept`] the track (a wire TRACK_INFO round-trip
1525	/// for a relay), and errors with the track's abort error if it closes first.
1526	/// [`Subscriber::info`] is the already-resolved counterpart.
1527	pub fn info(&self) -> kio::Pending<Querying> {
1528		kio::Pending::new(Querying {
1529			inner: match &self.inner {
1530				ConsumerKind::Plain(state) => QueryingKind::Plain(state.clone()),
1531				ConsumerKind::Spliced(resume) => QueryingKind::Spliced(resume.clone()),
1532			},
1533		})
1534	}
1535
1536	/// Return the latest group sequence in the track, or `None` before any group.
1537	pub fn latest(&self) -> Option<u64> {
1538		match &self.inner {
1539			ConsumerKind::Plain(state) => state.read().max_sequence,
1540			ConsumerKind::Spliced(resume) => resume.latest(),
1541		}
1542	}
1543
1544	/// Poll for the track reaching a terminal state: `Ok(())` once it is complete
1545	/// (the final group was produced), `Err` once it closed or aborted before
1546	/// completing. The origin's dispatcher uses this to tell a track that truly
1547	/// ended from one whose serving route died mid-stream.
1548	pub(crate) fn poll_complete(&self, waiter: &kio::Waiter) -> Poll<Result<()>> {
1549		let ConsumerKind::Plain(state) = &self.inner else {
1550			// Spliced tracks are compositions; the dispatcher never monitors one.
1551			return Poll::Pending;
1552		};
1553		match ready!(state.poll(waiter, |state| {
1554			if state.is_complete() {
1555				Poll::Ready(())
1556			} else {
1557				Poll::Pending
1558			}
1559		})) {
1560			Ok(_) => Poll::Ready(Ok(())),
1561			// Closed before completing. Read through the returned guard: it holds
1562			// the lock, so re-locking the channel here would deadlock.
1563			Err(closed) => Poll::Ready(Err(closed.abort.clone().unwrap_or(Error::Dropped))),
1564		}
1565	}
1566}
1567
1568/// The pollable state of a [`Consumer::subscribe`]; awaited via the
1569/// [`kio::Pending`] wrapper, whose `DerefMut` exposes [`Self::update`].
1570pub struct Subscribing {
1571	name: Arc<str>,
1572	inner: SubscribingKind,
1573	subscription: kio::Producer<Subscription>,
1574	stats: stats::Scope,
1575}
1576
1577enum SubscribingKind {
1578	Plain(kio::Consumer<TrackState>),
1579	Spliced(super::resume::Consumer),
1580}
1581
1582impl Subscribing {
1583	/// Poll until the peer confirms the subscription, yielding the [`Subscriber`].
1584	/// Errors if the track is aborted or not found.
1585	pub fn poll_ok(&self, waiter: &kio::Waiter) -> Poll<Result<Subscriber>> {
1586		match &self.inner {
1587			SubscribingKind::Plain(state) => {
1588				// Wait until the track info is available
1589				let info = ready!(state.poll(waiter, |state| state.poll_info()))
1590					.map_err(|e| e.abort.clone().unwrap_or(Error::Dropped))??;
1591
1592				Poll::Ready(Ok(Subscriber {
1593					name: self.name.clone(),
1594					info,
1595					inner: SubscriberKind::Plain(PlainSubscriber {
1596						state: state.clone(),
1597						subscription: self.subscription.clone(),
1598						index: 0,
1599						datagram_index: 0,
1600						min_sequence: 0,
1601						next_sequence: 0,
1602						end_sequence: None,
1603					}),
1604					stats: self.stats.clone(),
1605					_stats_sub: self.stats.subscribe(),
1606				}))
1607			}
1608			SubscribingKind::Spliced(resume) => {
1609				// Resolved from the first segment's track. The publisher's latency
1610				// window is applied to each per-session aggregate, not here.
1611				let info = ready!(resume.poll_info(waiter))?;
1612
1613				Poll::Ready(Ok(Subscriber {
1614					name: self.name.clone(),
1615					info,
1616					inner: SubscriberKind::Spliced(Box::new(resume.subscribe_shared(self.subscription.clone()))),
1617					stats: self.stats.clone(),
1618					_stats_sub: self.stats.subscribe(),
1619				}))
1620			}
1621		}
1622	}
1623
1624	/// Change the subscription preferences before (or after) it resolves.
1625	///
1626	/// Returns [`Error::Closed`] if the track already ended; the update is
1627	/// meaningless at that point and can usually be ignored.
1628	pub fn update(&mut self, subscription: Subscription) -> Result<()> {
1629		let mut state = self.subscription.write().map_err(|_| Error::Closed)?;
1630		*state = subscription;
1631		Ok(())
1632	}
1633}
1634
1635impl kio::Pollable for Subscribing {
1636	type Output = Result<Subscriber>;
1637
1638	fn poll(&self, waiter: &kio::Waiter) -> Poll<Self::Output> {
1639		self.poll_ok(waiter)
1640	}
1641}
1642
1643/// The pollable state of a [`Consumer::info`]; awaited via the
1644/// [`kio::Pending`] wrapper.
1645pub struct Querying {
1646	inner: QueryingKind,
1647}
1648
1649enum QueryingKind {
1650	Plain(kio::Consumer<TrackState>),
1651	Spliced(super::resume::Consumer),
1652}
1653
1654impl Querying {
1655	/// Poll until the track's [`Info`] is known, without subscribing to its groups.
1656	pub fn poll_ok(&self, waiter: &kio::Waiter) -> Poll<Result<Info>> {
1657		match &self.inner {
1658			QueryingKind::Plain(state) => {
1659				// Wait until the track info is available
1660				let info = ready!(state.poll(waiter, |state| state.poll_info()))
1661					.map_err(|e| e.abort.clone().unwrap_or(Error::Dropped))??;
1662				Poll::Ready(Ok(info))
1663			}
1664			QueryingKind::Spliced(resume) => resume.poll_info(waiter),
1665		}
1666	}
1667}
1668
1669impl kio::Pollable for Querying {
1670	type Output = Result<Info>;
1671
1672	fn poll(&self, waiter: &kio::Waiter) -> Poll<Self::Output> {
1673		self.poll_ok(waiter)
1674	}
1675}
1676
1677/// A consumer's request for a single past group, handed to a handler via
1678/// [`Dynamic::requested_group`].
1679///
1680/// The handler fulfills it by calling [`Self::accept`], which inserts the group
1681/// into the track cache (resolving every [`Consumer::fetch_group`] that joined the
1682/// attempt) and returns a [`group::Producer`] to fill. A relay typically opens a wire
1683/// FETCH, reads FETCH_OK, then accepts. The request carries its own producer handle,
1684/// so it works the same whether or not the track has been accepted yet.
1685pub struct GroupRequest {
1686	state: kio::Producer<TrackState>,
1687	// To remove this attempt from the fetch state once it resolves.
1688	fetch: kio::Shared<FetchState>,
1689	sequence: u64,
1690	priority: u8,
1691	// Rejections route back to every joined `Fetching`.
1692	result: kio::Producer<FetchOutcome>,
1693	done: bool,
1694}
1695
1696impl GroupRequest {
1697	/// The group sequence the consumer wants.
1698	pub fn sequence(&self) -> u64 {
1699		self.sequence
1700	}
1701
1702	/// The delivery priority the consumer requested for this group.
1703	pub fn priority(&self) -> u8 {
1704		self.priority
1705	}
1706
1707	/// Insert the fetched group into the track cache, resolving the waiting
1708	/// [`Consumer::fetch_group`], and return a [`group::Producer`] to fill.
1709	///
1710	/// The group's timescale comes from the track's [`Info`]. `info` sets that
1711	/// info if the track hasn't been accepted yet (a fetch with no live subscription),
1712	/// and is ignored once accepted. Returns [`Error::Duplicate`] if the group is
1713	/// already present, or the track's abort error if it closed while pending.
1714	pub fn accept(mut self, info: impl Into<Option<Info>>) -> Result<group::Producer> {
1715		self.done = true;
1716		// Cache the group before removing the attempt: the joined fetches resolve
1717		// through the cache, and removal closes their result channel (which alone
1718		// would read as NotFound).
1719		let res = TrackState::modify(&self.state)
1720			.and_then(|mut state| state.insert_group_request(self.sequence, info.into()));
1721		self.remove();
1722		res
1723	}
1724
1725	/// Reject the fetch, resolving every joined [`Consumer::fetch_group`] with `err`.
1726	pub fn reject(mut self, err: Error) {
1727		self.done = true;
1728		// Remove before writing, so a fetch arriving now starts a fresh attempt
1729		// instead of joining a rejected one.
1730		self.remove();
1731		if let Ok(mut outcome) = self.result.write() {
1732			outcome.rejected = Some(err);
1733		}
1734	}
1735
1736	/// Remove this attempt from the fetch state, unless a newer attempt for the same
1737	/// sequence has already replaced it.
1738	fn remove(&self) {
1739		self.fetch
1740			.lock()
1741			.remove_if(&self.sequence, |pending| pending.result.same_channel(&self.result));
1742	}
1743}
1744
1745impl Drop for GroupRequest {
1746	fn drop(&mut self) {
1747		if self.done {
1748			return;
1749		}
1750		self.remove();
1751		if let Ok(mut outcome) = self.result.write() {
1752			outcome.rejected = Some(Error::Dropped);
1753		}
1754	}
1755}
1756
1757/// The pollable state of a [`Consumer::fetch_group`].
1758///
1759/// Awaited via the [`kio::Pending`] wrapper; resolves to the
1760/// [`group::Consumer`] once the group lands in the track's cache (already present,
1761/// or produced after a wire FETCH), or [`Error::NotFound`] if it can never exist.
1762pub struct Fetching {
1763	inner: FetchingKind,
1764	// Egress stats scope, so the resolved group carries a payload meter (and counts
1765	// as one delivered group). Empty (no-op) for an untagged track.
1766	stats: stats::Scope,
1767}
1768
1769enum FetchingKind {
1770	Plain {
1771		state: kio::Consumer<TrackState>,
1772		fetch: kio::Shared<FetchState>,
1773		sequence: u64,
1774		// The joined attempt's result channel; `None` when no handler existed to queue on.
1775		result: Option<kio::Consumer<FetchOutcome>>,
1776	},
1777	/// A spliced track's fetch: waits for a segment, then fetches from it.
1778	Spliced(kio::Pending<super::resume::Fetching>),
1779}
1780
1781impl kio::Pollable for Fetching {
1782	type Output = Result<group::Consumer>;
1783
1784	fn poll(&self, waiter: &kio::Waiter) -> Poll<Self::Output> {
1785		let (state, fetch, sequence, result) = match &self.inner {
1786			FetchingKind::Plain {
1787				state,
1788				fetch,
1789				sequence,
1790				result,
1791			} => (state, fetch, *sequence, result.as_ref()),
1792			FetchingKind::Spliced(spliced) => {
1793				// A fetched group is metered here (once), at the tagged handle: the
1794				// spliced source track it comes from is the origin's own, untagged.
1795				return kio::Pollable::poll(&**spliced, waiter)
1796					.map(|res| res.map(|group| group.with_meter(self.stats.meter())));
1797			}
1798		};
1799
1800		// Track side: the cached group, the abort error, or past-final. The outer
1801		// error is the channel closing without any of those.
1802		match state.poll(waiter, |state| state.poll_fetch_cached(sequence)) {
1803			Poll::Ready(Ok(res)) => return Poll::Ready(res.map(|group| group.with_meter(self.stats.meter()))),
1804			Poll::Ready(Err(closed)) => {
1805				return Poll::Ready(Err(closed.abort.clone().unwrap_or(Error::Dropped)));
1806			}
1807			Poll::Pending => {}
1808		}
1809
1810		// Handler side.
1811		let Some(result) = result else {
1812			// Never queued: no handler existed when the fetch was made. Fail fast while
1813			// that's still true; a handler that appeared since may yet fill the cache.
1814			return match fetch.poll(waiter, |fetch| match fetch.has_handlers() {
1815				false => Poll::Ready(()),
1816				true => Poll::Pending,
1817			}) {
1818				Poll::Ready(_guard) => Poll::Ready(Err(Error::NotFound)),
1819				Poll::Pending => Poll::Pending,
1820			};
1821		};
1822
1823		// A written rejection fails every joined fetch. The channel closing without
1824		// one means the attempt was dropped unserved (its handlers went away).
1825		match result.poll(waiter, |outcome| match &outcome.rejected {
1826			Some(err) => Poll::Ready(err.clone()),
1827			None => Poll::Pending,
1828		}) {
1829			Poll::Ready(Ok(err)) => Poll::Ready(Err(err)),
1830			Poll::Ready(Err(_closed)) => Poll::Ready(Err(Error::NotFound)),
1831			Poll::Pending => Poll::Pending,
1832		}
1833	}
1834}
1835
1836/// A live subscription to a track, used to read its groups.
1837///
1838/// Created via [`Consumer::subscribe`](Consumer::subscribe), or
1839/// directly from a [`Producer`] for an in-process track. Carries this
1840/// subscriber's [`Subscription`] preferences, which feed the producer's aggregate.
1841///
1842/// # Local cursor vs wire preference
1843///
1844/// Group bounds exist at two levels, and setting one does not imply the other:
1845///
1846/// - [`Self::start_at`] / [`Self::end_at`] move **this subscriber's read cursor**. They
1847///   filter exactly what this handle returns and are invisible to the publisher.
1848/// - [`Subscription::group_start`] / [`Subscription::group_end`], set via [`Self::update`],
1849///   are a **request to the publisher**. They're aggregated across every live subscriber
1850///   (earliest start, widest end), so they say what the publisher should send, not what
1851///   this subscriber sees.
1852///
1853/// They stay separate because their scopes differ: a subscriber can't filter by the
1854/// aggregate, since another subscriber can widen it, and the publisher can't honor a
1855/// cursor it's never told about. So setting only the cursor still transfers the skipped
1856/// groups, and setting only the preference still returns groups another subscriber asked
1857/// for. Set both to skip them *and* avoid the transfer.
1858pub struct Subscriber {
1859	name: Arc<str>,
1860	info: Info,
1861	inner: SubscriberKind,
1862	// Egress stats scope, used to meter the groups this subscriber reads. Empty
1863	// (no-op) for an untagged track.
1864	stats: stats::Scope,
1865	// The subscription guard: bumps `subscriptions` (and the egress viewer refcount)
1866	// while held, closing them on drop. Empty (no-op) for an untagged track.
1867	_stats_sub: stats::Subscription,
1868}
1869
1870enum SubscriberKind {
1871	Plain(PlainSubscriber),
1872	// Boxed: the spliced cursor set dwarfs the plain cursor.
1873	Spliced(Box<super::resume::Subscriber>),
1874}
1875
1876/// The cursor state for a subscription over a single (per-session) track.
1877struct PlainSubscriber {
1878	state: kio::Consumer<TrackState>,
1879
1880	subscription: kio::Producer<Subscription>,
1881	/// Arrival-order cursor used by `recv_group`.
1882	index: usize,
1883	/// Arrival-order cursor used by `recv_datagram`, independent of groups.
1884	datagram_index: usize,
1885	/// Minimum sequence to return from any `recv` method. Set by `start_at`.
1886	min_sequence: u64,
1887	/// One past the highest sequence returned by `next_group`.
1888	/// Used only by that method to skip late arrivals; does not affect `recv_group`.
1889	next_sequence: u64,
1890	/// Inclusive upper sequence bound for `next_group`. `None` means no cap. Set by
1891	/// `end_at`; can be raised, lowered, or unset at any time. Groups beyond the
1892	/// cap stay in the producer's cache and become eligible again when the cap
1893	/// rises (or is removed).
1894	end_sequence: Option<u64>,
1895}
1896
1897impl PlainSubscriber {
1898	// A helper to automatically apply Dropped if the state is closed without an error.
1899	fn poll<F, R>(&self, waiter: &kio::Waiter, f: F) -> Poll<Result<R>>
1900	where
1901		F: Fn(&kio::Ref<'_, TrackState>) -> Poll<Result<R>>,
1902	{
1903		Poll::Ready(match ready!(self.state.poll(waiter, f)) {
1904			Ok(res) => res,
1905			// We try to clone abort just in case the function forgot to check for terminal state.
1906			Err(state) => Err(state.abort.clone().unwrap_or(Error::Dropped)),
1907		})
1908	}
1909
1910	fn poll_recv_group(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<group::Consumer>>> {
1911		let Some((consumer, found_index)) =
1912			ready!(self.poll(waiter, |state| state.poll_recv_group(self.index, self.min_sequence))?)
1913		else {
1914			return Poll::Ready(Ok(None));
1915		};
1916
1917		self.index = found_index + 1;
1918		Poll::Ready(Ok(Some(consumer)))
1919	}
1920
1921	fn poll_recv_datagram(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Datagram>>> {
1922		let Some((datagram, found_index)) =
1923			ready!(self.poll(waiter, |state| state.poll_recv_datagram(self.datagram_index))?)
1924		else {
1925			return Poll::Ready(Ok(None));
1926		};
1927
1928		self.datagram_index = found_index + 1;
1929		self.next_sequence = self.next_sequence.max(datagram.sequence.saturating_add(1));
1930		Poll::Ready(Ok(Some(datagram)))
1931	}
1932
1933	fn poll_next_group(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<group::Consumer>>> {
1934		let floor = self.next_sequence.max(self.min_sequence);
1935		let Some(group) = ready!(self.poll(waiter, |state| state.poll_next_in_range(floor, self.end_sequence))?) else {
1936			return Poll::Ready(Ok(None));
1937		};
1938		self.next_sequence = group.sequence.saturating_add(1);
1939		Poll::Ready(Ok(Some(group)))
1940	}
1941
1942	fn poll_read_frame(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<frame::Frame>>> {
1943		let lower = self.min_sequence.max(self.next_sequence);
1944		let Some((frame, found_index, sequence)) =
1945			ready!(self.poll(waiter, |state| { state.poll_read_frame(self.index, lower, waiter) })?)
1946		else {
1947			return Poll::Ready(Ok(None));
1948		};
1949
1950		self.index = found_index + 1;
1951		self.next_sequence = sequence.saturating_add(1);
1952		Poll::Ready(Ok(Some(frame)))
1953	}
1954}
1955
1956/// A cloneable handle to a subscriber's delivery preferences.
1957///
1958/// This updates the same subscription as the owning [`Subscriber`] without
1959/// borrowing its read cursor, so callers can change delivery priority, group
1960/// ordering priority, or group bounds while another task is waiting for groups.
1961#[derive(Clone)]
1962pub struct SubscriberControl {
1963	subscription: kio::Producer<Subscription>,
1964}
1965
1966impl SubscriberControl {
1967	/// This subscriber's current preferences.
1968	pub fn subscription(&self) -> Subscription {
1969		self.subscription.read().clone()
1970	}
1971
1972	/// Replace this subscriber's preferences, updating the producer's aggregate.
1973	///
1974	/// Returns [`Error::Closed`] if the track already ended; the update is
1975	/// meaningless at that point and can usually be ignored.
1976	pub fn update(&self, subscription: Subscription) -> Result<()> {
1977		let mut state = self.subscription.write().map_err(|_| Error::Closed)?;
1978		*state = subscription;
1979		Ok(())
1980	}
1981}
1982
1983impl Subscriber {
1984	/// The track's [`Info`], resolved when the subscription was established.
1985	///
1986	/// Free, unlike [`Consumer::info`]: subscribing already waited for the info
1987	/// (SUBSCRIBE_OK on the wire), so a subscriber always has it.
1988	pub fn info(&self) -> &Info {
1989		&self.info
1990	}
1991
1992	/// The track's name, unique within its broadcast.
1993	pub fn name(&self) -> &str {
1994		&self.name
1995	}
1996
1997	/// Create a handle for updating this subscriber's delivery preferences.
1998	pub fn control(&self) -> SubscriberControl {
1999		SubscriberControl {
2000			subscription: match &self.inner {
2001				SubscriberKind::Plain(plain) => plain.subscription.clone(),
2002				SubscriberKind::Spliced(spliced) => spliced.prefs(),
2003			},
2004		}
2005	}
2006
2007	/// Poll for the next group in arrival order, without blocking.
2008	///
2009	/// Returns every group exactly once in the order it landed on the wire, which may be
2010	/// out of sequence due to network reordering or loss. Use [`Self::poll_next_group`] if
2011	/// you only want groups whose sequence number is higher than any previously returned.
2012	///
2013	/// Returns `Poll::Ready(Ok(Some(group)))` when a group is available,
2014	/// `Poll::Ready(Ok(None))` when the track is finished,
2015	/// `Poll::Ready(Err(e))` when the track has been aborted, or
2016	/// `Poll::Pending` when no group is available yet.
2017	pub fn poll_recv_group(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<group::Consumer>>> {
2018		let meter = self.stats.meter();
2019		let res = match &mut self.inner {
2020			SubscriberKind::Plain(plain) => plain.poll_recv_group(waiter),
2021			SubscriberKind::Spliced(spliced) => spliced.poll_recv_group(waiter),
2022		};
2023		res.map(|res| res.map(|group| group.map(|group| group.with_meter(meter))))
2024	}
2025
2026	/// Receive the next group in arrival order.
2027	///
2028	/// Every group is returned exactly once, in the order it landed on the wire, which may
2029	/// be out of sequence due to network reordering or loss. Use [`Self::next_group`] if you
2030	/// only want groups whose sequence number is higher than any previously returned.
2031	pub async fn recv_group(&mut self) -> Result<Option<group::Consumer>> {
2032		kio::wait(|waiter| self.poll_recv_group(waiter)).await
2033	}
2034
2035	/// Poll for the next datagram in arrival order, without blocking.
2036	///
2037	/// Datagrams are a separate best-effort channel from groups (see
2038	/// [`Producer::append_datagram`]); they share only the sequence namespace. A consumer
2039	/// that falls too far behind silently loses the oldest datagrams.
2040	/// Returning a datagram advances [`Self::poll_next_group`] past that sequence.
2041	///
2042	/// Returns `Poll::Ready(Ok(Some(datagram)))` when one is available,
2043	/// `Poll::Ready(Ok(None))` when the track is finished, `Poll::Ready(Err(e))` when the track
2044	/// is aborted, or `Poll::Pending` when none is buffered yet.
2045	pub fn poll_recv_datagram(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<Datagram>>> {
2046		let meter = self.stats.meter();
2047		let res = match &mut self.inner {
2048			SubscriberKind::Plain(plain) => plain.poll_recv_datagram(waiter),
2049			SubscriberKind::Spliced(spliced) => spliced.poll_recv_datagram(waiter),
2050		};
2051		// Unlike a group (metered lazily as its frames are read), a datagram is
2052		// delivered whole here, so count it as the single-frame group it stands in for.
2053		if let Poll::Ready(Ok(Some(datagram))) = &res {
2054			meter.datagram(datagram.payload.len() as u64);
2055		}
2056		res
2057	}
2058
2059	/// Receive the next datagram in arrival order.
2060	///
2061	/// A best-effort channel parallel to [`Self::recv_group`]; the two share only the sequence
2062	/// namespace. To receive both concurrently from one subscriber, poll [`Self::poll_next_group`]
2063	/// (or [`Self::poll_recv_group`]) and [`Self::poll_recv_datagram`] together in a single `poll`
2064	/// closure (sequential `&mut` borrows), rather than awaiting the two `recv` futures at once.
2065	pub async fn recv_datagram(&mut self) -> Result<Option<Datagram>> {
2066		kio::wait(|waiter| self.poll_recv_datagram(waiter)).await
2067	}
2068
2069	/// Poll for the next group with a higher sequence number than any previously returned.
2070	///
2071	/// Late arrivals (sequence at or below the last returned) are silently skipped, so this
2072	/// produces a monotonically increasing sequence at the cost of dropping out-of-order
2073	/// groups. Use [`Self::poll_recv_group`] to see every group in arrival order instead.
2074	///
2075	/// Honors the cap set by [`Self::end_at`]: groups with sequence past the cap are left
2076	/// in the producer's cache and become eligible again if the cap is raised or removed.
2077	pub fn poll_next_group(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<group::Consumer>>> {
2078		let meter = self.stats.meter();
2079		let res = match &mut self.inner {
2080			SubscriberKind::Plain(plain) => plain.poll_next_group(waiter),
2081			SubscriberKind::Spliced(spliced) => spliced.poll_next_group(waiter),
2082		};
2083		res.map(|res| res.map(|group| group.map(|group| group.with_meter(meter))))
2084	}
2085
2086	/// Return the next group with a higher sequence number than any previously returned.
2087	///
2088	/// Late arrivals (sequence at or below the last returned) are silently skipped, so this
2089	/// produces a monotonically increasing sequence at the cost of dropping out-of-order
2090	/// groups. Use [`Self::recv_group`] to see every group in arrival order instead.
2091	pub async fn next_group(&mut self) -> Result<Option<group::Consumer>> {
2092		kio::wait(|waiter| self.poll_next_group(waiter)).await
2093	}
2094
2095	/// A helper that calls [`Self::poll_next_group`] and returns its first frame
2096	/// (timestamp and payload), skipping the rest of the group. Intended for
2097	/// single-frame groups (see [`Producer::write_frame`]).
2098	pub fn poll_read_frame(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<frame::Frame>>> {
2099		let meter = self.stats.meter();
2100		let res = match &mut self.inner {
2101			SubscriberKind::Plain(plain) => plain.poll_read_frame(waiter),
2102			SubscriberKind::Spliced(spliced) => spliced.poll_read_frame(waiter),
2103		};
2104		// This helper collapses a group to its first frame: count the group, the one
2105		// frame, and the bytes actually read.
2106		if let Poll::Ready(Ok(Some(frame))) = &res {
2107			meter.group();
2108			meter.frames(1);
2109			meter.bytes(frame.payload.len() as u64);
2110		}
2111		res
2112	}
2113
2114	/// Read a single full frame (timestamp and payload) from the next group in
2115	/// sequence order.
2116	///
2117	/// See [`Self::poll_read_frame`] for semantics.
2118	pub async fn read_frame(&mut self) -> Result<Option<frame::Frame>> {
2119		kio::wait(|waiter| self.poll_read_frame(waiter)).await
2120	}
2121
2122	/// Whether `other` was cloned from this subscriber (shares the same underlying state).
2123	pub fn is_clone(&self, other: &Self) -> bool {
2124		match (&self.inner, &other.inner) {
2125			(SubscriberKind::Plain(a), SubscriberKind::Plain(b)) => a.state.same_channel(&b.state),
2126			(SubscriberKind::Spliced(a), SubscriberKind::Spliced(b)) => a.is_clone(b),
2127			_ => false,
2128		}
2129	}
2130
2131	/// Poll for the track's declared final sequence, without blocking.
2132	pub fn poll_finished(&mut self, waiter: &kio::Waiter) -> Poll<Result<u64>> {
2133		match &mut self.inner {
2134			SubscriberKind::Plain(plain) => plain.poll(waiter, |state| state.poll_finished()),
2135			SubscriberKind::Spliced(spliced) => spliced.poll_finished(waiter),
2136		}
2137	}
2138
2139	/// Block until the track declares its end, returning the exclusive final sequence
2140	/// (also the total group count), or the cause on an abort.
2141	///
2142	/// Resolves as soon as the boundary is known, which may be ahead of the live edge
2143	/// when the producer finished via [`Producer::finish_at`]. This reports the declared
2144	/// end, not that every group has arrived: drive [`Self::recv_group`] /
2145	/// [`Self::next_group`] until they yield `None` to observe the track fully drained.
2146	pub async fn finished(&mut self) -> Result<u64> {
2147		kio::wait(|waiter| self.poll_finished(waiter)).await
2148	}
2149
2150	/// Start this subscriber's read cursor at the given sequence.
2151	///
2152	/// A local filter, not a request: it doesn't tell the publisher anything, so the
2153	/// skipped groups are still delivered and simply not returned. To ask the publisher
2154	/// to start there instead, set [`Subscription::group_start`] via [`Self::update`].
2155	/// See [Local cursor vs wire preference](Self#local-cursor-vs-wire-preference).
2156	pub fn start_at(&mut self, sequence: u64) {
2157		match &mut self.inner {
2158			SubscriberKind::Plain(plain) => plain.min_sequence = sequence,
2159			SubscriberKind::Spliced(spliced) => spliced.start_at(sequence),
2160		}
2161	}
2162
2163	/// Cap this subscriber's read cursor at the given sequence (inclusive), or remove the
2164	/// cap entirely.
2165	///
2166	/// Accepts a bare `u64` (cap), `Some(u64)`, or `None` (uncap).
2167	///
2168	/// A local filter, not a request; [`Subscription::group_end`] is the wire-level
2169	/// counterpart. See [Local cursor vs wire preference](Self#local-cursor-vs-wire-preference).
2170	///
2171	/// Affects [`Self::next_group`] only: groups beyond the cap stay in the producer's
2172	/// cache rather than being skipped past, so a later call to [`Self::end_at`] with a
2173	/// higher value (or `None`) makes them available again. Lowering the cap below the
2174	/// consumer's current cursor parks the consumer until the cap is raised.
2175	pub fn end_at(&mut self, sequence: impl Into<Option<u64>>) {
2176		match &mut self.inner {
2177			SubscriberKind::Plain(plain) => plain.end_sequence = sequence.into(),
2178			SubscriberKind::Spliced(spliced) => spliced.end_at(sequence),
2179		}
2180	}
2181
2182	/// This subscriber's current preferences.
2183	pub fn subscription(&self) -> Subscription {
2184		self.control().subscription()
2185	}
2186
2187	/// Replace this subscriber's delivery preferences.
2188	///
2189	/// Stored verbatim; the publisher's latency window is applied to the aggregate, not
2190	/// here (see [`Producer::subscription`]). Returns [`Error::Closed`] if the track
2191	/// already ended; the update is meaningless at that point and can usually be ignored.
2192	pub fn update(&mut self, subscription: Subscription) -> Result<()> {
2193		match &mut self.inner {
2194			SubscriberKind::Plain(plain) => {
2195				let mut state = plain.subscription.write().map_err(|_| Error::Closed)?;
2196				*state = subscription;
2197			}
2198			SubscriberKind::Spliced(spliced) => spliced.update(subscription),
2199		}
2200		Ok(())
2201	}
2202
2203	/// Return the latest sequence number in the track.
2204	pub fn latest(&self) -> Option<u64> {
2205		match &self.inner {
2206			SubscriberKind::Plain(plain) => plain.state.read().max_sequence,
2207			SubscriberKind::Spliced(spliced) => spliced.latest(),
2208		}
2209	}
2210}
2211
2212/// A subscriber asked for a track this broadcast doesn't have yet.
2213///
2214/// Yielded by [`broadcast::Dynamic::requested_track`](crate::broadcast::Dynamic::requested_track),
2215/// or created up front with [`broadcast::Producer::reserve_track`](crate::broadcast::Producer::reserve_track).
2216/// Subscribers block until the request is
2217/// resolved: call [`accept`](Self::accept) to serve it with a [`Producer`], or
2218/// [`reject`](Self::reject) to fail them. Dropping it without either rejects with
2219/// [`Error::Dropped`].
2220///
2221/// Concurrent requests for one name are coalesced, so exactly one of these exists per
2222/// name at a time.
2223pub struct Request {
2224	name: Arc<str>,
2225	// The parent broadcast's info, threaded into the [`Producer`] on accept.
2226	broadcast: Arc<broadcast::Info>,
2227	state: kio::Producer<TrackState>,
2228
2229	// The previous subscription that was combined, used to detect changes.
2230	prev_subscription: Option<Subscription>,
2231
2232	// A requested track is served on demand, so it counts as fetch-capable from
2233	// birth: a consumer's cache-miss `fetch_group` waits to be served instead of
2234	// racing the producer (e.g. a relay) into creating its own handler. Released
2235	// when the request is accepted or dropped; by then the relay holds its own.
2236	_dynamic: Dynamic,
2237
2238	// Ingress stats scope, threaded into the accepted [`Producer`]. Empty (no-op)
2239	// unless this request was reserved on a tagged broadcast.
2240	stats: stats::Scope,
2241}
2242
2243impl Request {
2244	pub(crate) fn new(broadcast: Arc<broadcast::Info>, name: impl Into<Arc<str>>) -> Self {
2245		let name = name.into();
2246		let state = kio::Producer::new(TrackState {
2247			broadcast: broadcast.clone(),
2248			..Default::default()
2249		});
2250		let dynamic = Dynamic::new(name.clone(), state.clone());
2251		Self {
2252			name,
2253			broadcast,
2254			state,
2255			prev_subscription: None,
2256			_dynamic: dynamic,
2257			stats: stats::Scope::default(),
2258		}
2259	}
2260
2261	/// Attach an ingress stats scope, applied to the [`Producer`] on accept. Set by
2262	/// a tagged [`broadcast::Producer::reserve_track`].
2263	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
2264		self.stats = scope;
2265		self
2266	}
2267
2268	/// The requested track name.
2269	pub fn name(&self) -> &str {
2270		&self.name
2271	}
2272
2273	/// A [`Consumer`] for the eventual track, usable before the request is accepted.
2274	pub fn consume(&self) -> Consumer {
2275		Consumer::plain(self.name.clone(), self.state.consume())
2276	}
2277
2278	/// Create a [`Dynamic`] handle that serves on-demand fetches of uncached
2279	/// groups, before [`Self::accept`] is even called. A relay creates one to fetch
2280	/// past groups from upstream while (or instead of) serving a live subscription.
2281	pub fn dynamic(&self) -> Dynamic {
2282		Dynamic::new(self.name.clone(), self.state.clone())
2283	}
2284
2285	/// Poll for the request becoming unused (every consumer dropped), so a relay can
2286	/// stop serving and drop the request.
2287	pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<()> {
2288		self.state.poll_unused(waiter).map(|_| ())
2289	}
2290
2291	/// Serve the request with the given track, resolving every waiting subscriber.
2292	///
2293	/// The name is taken from [`Self::name`]; `info` supplies the remaining knobs
2294	/// (`None` for the defaults). If the track was already aborted, the returned
2295	/// [`Producer`] is inert: writes fail with the abort error, as if it had been
2296	/// aborted immediately after accepting.
2297	pub fn accept(self, info: impl Into<Option<Info>>) -> Producer {
2298		let mut info = info.into().unwrap_or_default();
2299		info.broadcast = self.broadcast.clone();
2300		// A closed state means the track was aborted under us. Mirror `reject` and
2301		// tolerate it: the Producer we hand back simply can't write.
2302		if let Ok(mut state) = self.state.write() {
2303			state.info = Some(info);
2304		}
2305		// Accepting the request creates the track producer: count it as one ingress
2306		// subscription (closed on the last producer drop). No-op when untagged.
2307		self.stats.open_subscription();
2308		Producer {
2309			name: self.name,
2310			broadcast: self.broadcast,
2311			state: self.state,
2312			prev_subscription: None,
2313			stats: self.stats,
2314		}
2315	}
2316
2317	/// Reject the request, waking all waiting subscribers with `err`.
2318	pub fn reject(self, err: Error) {
2319		if let Ok(mut state) = self.state.write() {
2320			state.abort = Some(err);
2321		}
2322	}
2323
2324	/// The delivery preferences aggregated across everyone waiting on this request,
2325	/// or `None` if nobody is waiting. Useful for sizing the track before accepting.
2326	pub fn subscription(&self) -> Option<Subscription> {
2327		let state = self.state.read();
2328		let (subs, bound) = (state.subscriptions.clone(), state.latency_bound());
2329		drop(state);
2330		snapshot_subscription(&subs, bound)
2331	}
2332
2333	/// Block until the aggregate [`subscription`](Self::subscription) changes,
2334	/// yielding `None` once nobody is waiting.
2335	pub async fn subscription_changed(&mut self) -> Option<Subscription> {
2336		kio::wait(|waiter| self.poll_subscription_changed(waiter)).await
2337	}
2338
2339	/// Poll counterpart to [`subscription_changed`](Self::subscription_changed).
2340	pub fn poll_subscription_changed(&mut self, waiter: &kio::Waiter) -> Poll<Option<Subscription>> {
2341		let state = self.state.read();
2342		let (subs, bound) = (state.subscriptions.clone(), state.latency_bound());
2343		drop(state);
2344
2345		let prev = &self.prev_subscription;
2346		let mut combined = None;
2347		let mut guard = ready!(subs.poll(waiter, |subs| {
2348			let next = combined_subscription(subs, bound, waiter);
2349			if &next == prev {
2350				Poll::Pending
2351			} else {
2352				combined = next;
2353				Poll::Ready(())
2354			}
2355		}));
2356		// The aggregate changed: prune any closed subscribers now that we hold the lock.
2357		guard.retain(|sub| !sub.is_closed());
2358		drop(guard);
2359		self.prev_subscription = combined.clone();
2360		Poll::Ready(combined)
2361	}
2362
2363	pub(super) fn weak(&self) -> TrackWeak {
2364		TrackWeak {
2365			name: self.name.clone(),
2366			state: self.state.weak(),
2367		}
2368	}
2369}
2370
2371#[cfg(test)]
2372use futures::FutureExt;
2373
2374#[cfg(test)]
2375#[allow(missing_docs)] // test-only assertion helpers
2376impl Subscriber {
2377	pub fn assert_group(&mut self) -> group::Consumer {
2378		self.recv_group()
2379			.now_or_never()
2380			.expect("group would have blocked")
2381			.expect("would have errored")
2382			.expect("track was closed")
2383	}
2384
2385	pub fn assert_no_group(&mut self) {
2386		assert!(
2387			self.recv_group().now_or_never().is_none(),
2388			"recv_group would not have blocked"
2389		);
2390	}
2391
2392	pub fn assert_not_closed(&mut self) {
2393		assert!(self.finished().now_or_never().is_none(), "should not be closed");
2394	}
2395
2396	pub fn assert_closed(&mut self) {
2397		assert!(self.finished().now_or_never().is_some(), "should be closed");
2398	}
2399
2400	// TODO assert specific errors after implementing PartialEq
2401	pub fn assert_error(&mut self) {
2402		assert!(
2403			self.finished().now_or_never().expect("should not block").is_err(),
2404			"should be error"
2405		);
2406	}
2407
2408	pub fn assert_is_clone(&self, other: &Self) {
2409		assert!(self.is_clone(other), "should be clone");
2410	}
2411
2412	pub fn assert_not_clone(&self, other: &Self) {
2413		assert!(!self.is_clone(other), "should not be clone");
2414	}
2415}
2416
2417#[cfg(test)]
2418mod test {
2419	use super::*;
2420
2421	/// Mint a track for tests with a default parent broadcast, since tracks are
2422	/// normally born from a [`broadcast::Producer`].
2423	fn track_producer(name: impl Into<Arc<str>>, info: impl Into<Option<Info>>) -> Producer {
2424		Producer::new(Arc::new(broadcast::Info::default()), name, info)
2425	}
2426
2427	/// Helper: count non-tombstoned groups in state.
2428	fn live_groups(state: &TrackState) -> usize {
2429		state.groups.iter().flatten().count()
2430	}
2431
2432	/// Helper: get the sequence number of the first live group.
2433	fn first_live_sequence(state: &TrackState) -> u64 {
2434		state.groups.iter().flatten().next().unwrap().0.sequence
2435	}
2436
2437	/// Helper: non-blocking datagram receive that must be ready with a datagram.
2438	fn recv_datagram(dg: &mut Subscriber) -> Datagram {
2439		dg.recv_datagram()
2440			.now_or_never()
2441			.expect("datagram would have blocked")
2442			.expect("would have errored")
2443			.expect("track was closed")
2444	}
2445
2446	#[tokio::test]
2447	async fn append_datagram_shares_group_sequence() {
2448		let mut producer = track_producer("test", None);
2449		let ts = Timestamp::from_millis(10).unwrap();
2450
2451		// Interleave groups and datagrams: they draw from one monotonic counter.
2452		assert_eq!(producer.append_group().unwrap().sequence, 0);
2453		assert_eq!(producer.append_datagram(ts, &b"a"[..]).unwrap(), 1);
2454		assert_eq!(producer.append_group().unwrap().sequence, 2);
2455		assert_eq!(producer.append_datagram(ts, &b"b"[..]).unwrap(), 3);
2456		assert_eq!(producer.latest(), Some(3));
2457	}
2458
2459	#[tokio::test]
2460	async fn append_datagram_roundtrip() {
2461		let mut producer = track_producer("test", None);
2462		let mut dg = producer.subscribe(None);
2463
2464		let ts = Timestamp::from_millis(42).unwrap();
2465		let seq = producer.append_datagram(ts, &b"hello"[..]).unwrap();
2466
2467		let got = recv_datagram(&mut dg);
2468		assert_eq!(got.sequence, seq);
2469		assert_eq!(got.timestamp, ts);
2470		assert_eq!(&got.payload[..], b"hello");
2471	}
2472
2473	#[tokio::test]
2474	async fn write_datagram_preserves_sequence() {
2475		let mut producer = track_producer("test", None);
2476		let mut dg = producer.subscribe(None);
2477
2478		let ts = Timestamp::from_millis(5).unwrap();
2479		// A relay forwarding an upstream datagram keeps its sequence number.
2480		producer
2481			.write_datagram(Datagram {
2482				sequence: 100,
2483				timestamp: ts,
2484				payload: bytes::Bytes::from_static(b"x"),
2485			})
2486			.unwrap();
2487
2488		assert_eq!(recv_datagram(&mut dg).sequence, 100);
2489		// max_sequence advanced, so the next appended group/datagram continues past it.
2490		assert_eq!(producer.append_group().unwrap().sequence, 101);
2491	}
2492
2493	#[tokio::test]
2494	async fn recv_datagram_advances_ordered_group_cursor() {
2495		let mut producer = track_producer("test", None);
2496		let mut subscriber = producer.subscribe(None);
2497		let ts = Timestamp::from_millis(5).unwrap();
2498
2499		producer
2500			.write_datagram(Datagram {
2501				sequence: 5,
2502				timestamp: ts,
2503				payload: bytes::Bytes::from_static(b"x"),
2504			})
2505			.unwrap();
2506		assert_eq!(recv_datagram(&mut subscriber).sequence, 5);
2507
2508		producer.create_group(group::Info { sequence: 3 }).unwrap();
2509		producer.create_group(group::Info { sequence: 6 }).unwrap();
2510
2511		let group = subscriber
2512			.next_group()
2513			.now_or_never()
2514			.expect("group would have blocked")
2515			.expect("would have errored")
2516			.expect("track was closed");
2517		assert_eq!(group.sequence, 6);
2518	}
2519
2520	#[tokio::test]
2521	async fn datagram_normalized_to_track_timescale() {
2522		let info = Info::default().with_timescale(Timescale::MICRO);
2523		let mut producer = track_producer("test", info);
2524		let mut dg = producer.subscribe(None);
2525
2526		// Supplied at millis; stored/emitted at the track's micro timescale.
2527		producer
2528			.append_datagram(Timestamp::from_millis(2).unwrap(), &b"z"[..])
2529			.unwrap();
2530		let got = recv_datagram(&mut dg);
2531		assert_eq!(got.timestamp.scale(), Timescale::MICRO);
2532		assert_eq!(got.timestamp.value(), 2_000);
2533	}
2534
2535	#[tokio::test]
2536	async fn datagram_rejects_oversized() {
2537		let mut producer = track_producer("test", None);
2538		let big = bytes::Bytes::from(vec![0u8; crate::model::datagram::MAX_DATAGRAM_PAYLOAD + 1]);
2539		let ts = Timestamp::from_millis(0).unwrap();
2540		assert!(matches!(
2541			producer.append_datagram(ts, big.clone()),
2542			Err(Error::FrameTooLarge)
2543		));
2544		assert!(matches!(
2545			producer.write_datagram(Datagram {
2546				sequence: 0,
2547				timestamp: ts,
2548				payload: big,
2549			}),
2550			Err(Error::FrameTooLarge)
2551		));
2552	}
2553
2554	#[tokio::test]
2555	async fn datagram_fanout_to_subscribers() {
2556		let mut producer = track_producer("test", None);
2557		// Two independent subscribers, each with its own datagram cursor.
2558		let mut a = producer.subscribe(None);
2559		let mut b = producer.subscribe(None);
2560		let ts = Timestamp::from_millis(1).unwrap();
2561
2562		producer.append_datagram(ts, &b"first"[..]).unwrap();
2563		producer.append_datagram(ts, &b"second"[..]).unwrap();
2564
2565		// Both receive every datagram in order, independently.
2566		assert_eq!(&recv_datagram(&mut a).payload[..], b"first");
2567		assert_eq!(&recv_datagram(&mut a).payload[..], b"second");
2568		assert_eq!(&recv_datagram(&mut b).payload[..], b"first");
2569		assert_eq!(&recv_datagram(&mut b).payload[..], b"second");
2570	}
2571
2572	#[tokio::test]
2573	async fn datagram_evicts_stale() {
2574		tokio::time::pause();
2575
2576		let mut producer = track_producer("test", None);
2577		let mut dg = producer.subscribe(None);
2578		let ts = Timestamp::from_millis(0).unwrap();
2579
2580		producer.append_datagram(ts, &b"old"[..]).unwrap(); // sequence 0
2581
2582		// Age past the send-buffer window, then push a fresh datagram: the stale one is evicted.
2583		tokio::time::advance(MAX_DATAGRAM_AGE + Duration::from_millis(10)).await;
2584		producer.append_datagram(ts, &b"new"[..]).unwrap(); // sequence 1
2585
2586		// A lagging consumer resumes at the oldest still-buffered datagram (the fresh one).
2587		let got = recv_datagram(&mut dg);
2588		assert_eq!(got.sequence, 1);
2589		assert_eq!(&got.payload[..], b"new");
2590	}
2591
2592	#[tokio::test]
2593	async fn datagram_recv_pends_until_written() {
2594		let mut producer = track_producer("test", None);
2595		let mut dg = producer.subscribe(None);
2596
2597		assert!(
2598			dg.recv_datagram().now_or_never().is_none(),
2599			"should block with no datagrams"
2600		);
2601
2602		producer
2603			.append_datagram(Timestamp::from_millis(0).unwrap(), &b"go"[..])
2604			.unwrap();
2605		assert_eq!(&recv_datagram(&mut dg).payload[..], b"go");
2606	}
2607
2608	/// Exercises the full producer -> publisher-encode -> subscriber-decode -> producer seam
2609	/// (everything but the QUIC datagram send/recv), catching any field-order mismatch between
2610	/// the wire codec and the model.
2611	#[tokio::test]
2612	async fn datagram_wire_roundtrip_between_tracks() {
2613		use crate::coding::{Decode, Encode};
2614		use crate::lite;
2615
2616		let version = lite::Version::Lite05;
2617
2618		// Origin publishes a datagram; the publisher reads it and encodes the wire body.
2619		let mut origin = track_producer("test", None);
2620		let mut origin_dg = origin.subscribe(None);
2621		let ts = Timestamp::from_millis(7).unwrap();
2622		let seq = origin.append_datagram(ts, &b"payload"[..]).unwrap();
2623
2624		let d = recv_datagram(&mut origin_dg);
2625		let body = lite::Datagram {
2626			subscribe: 5,
2627			sequence: d.sequence,
2628			timestamp: d.timestamp.value(),
2629			payload: d.payload.clone(),
2630		}
2631		.encode_bytes(version)
2632		.unwrap();
2633
2634		// Subscriber decodes the body and writes it downstream, preserving the sequence.
2635		let mut slice = &body[..];
2636		let wire = lite::Datagram::decode(&mut slice, version).unwrap();
2637		let mut downstream = track_producer("test", None);
2638		let mut downstream_dg = downstream.subscribe(None);
2639		downstream
2640			.write_datagram(Datagram {
2641				sequence: wire.sequence,
2642				timestamp: Timestamp::new(wire.timestamp, Timescale::MILLI).unwrap(),
2643				payload: wire.payload,
2644			})
2645			.unwrap();
2646
2647		let got = recv_datagram(&mut downstream_dg);
2648		assert_eq!(got.sequence, seq);
2649		assert_eq!(got.timestamp, ts);
2650		assert_eq!(&got.payload[..], b"payload");
2651	}
2652
2653	#[tokio::test]
2654	async fn evict_expired_groups() {
2655		tokio::time::pause();
2656
2657		let mut producer = track_producer("test", None);
2658
2659		// Create 3 groups at time 0.
2660		producer.append_group().unwrap(); // seq 0
2661		producer.append_group().unwrap(); // seq 1
2662		producer.append_group().unwrap(); // seq 2
2663
2664		{
2665			let state = producer.state.read();
2666			assert_eq!(live_groups(&state), 3);
2667			assert_eq!(state.offset, 0);
2668		}
2669
2670		// Advance time past the eviction threshold.
2671		tokio::time::advance(DEFAULT_LATENCY_MAX + Duration::from_secs(1)).await;
2672
2673		// Append a new group to trigger eviction.
2674		producer.append_group().unwrap(); // seq 3
2675
2676		// Groups 0, 1, 2 are expired but seq 3 (max_sequence) is kept.
2677		// Leading tombstones are trimmed, so only seq 3 remains.
2678		{
2679			let state = producer.state.read();
2680			assert_eq!(live_groups(&state), 1);
2681			assert_eq!(first_live_sequence(&state), 3);
2682			assert_eq!(state.offset, 3);
2683			assert!(!state.duplicates.contains(&0));
2684			assert!(!state.duplicates.contains(&1));
2685			assert!(!state.duplicates.contains(&2));
2686			assert!(state.duplicates.contains(&3));
2687		}
2688	}
2689
2690	#[tokio::test]
2691	async fn evict_keeps_max_sequence() {
2692		tokio::time::pause();
2693
2694		let mut producer = track_producer("test", None);
2695		producer.append_group().unwrap(); // seq 0
2696
2697		// Advance time past threshold.
2698		tokio::time::advance(DEFAULT_LATENCY_MAX + Duration::from_secs(1)).await;
2699
2700		// Append another group; seq 0 is expired and evicted.
2701		producer.append_group().unwrap(); // seq 1
2702
2703		{
2704			let state = producer.state.read();
2705			assert_eq!(live_groups(&state), 1);
2706			assert_eq!(first_live_sequence(&state), 1);
2707			assert_eq!(state.offset, 1);
2708		}
2709	}
2710
2711	#[tokio::test]
2712	async fn no_eviction_when_fresh() {
2713		tokio::time::pause();
2714
2715		let mut producer = track_producer("test", None);
2716		producer.append_group().unwrap(); // seq 0
2717		producer.append_group().unwrap(); // seq 1
2718		producer.append_group().unwrap(); // seq 2
2719
2720		{
2721			let state = producer.state.read();
2722			assert_eq!(live_groups(&state), 3);
2723			assert_eq!(state.offset, 0);
2724		}
2725	}
2726
2727	#[tokio::test]
2728	async fn consumer_skips_evicted_groups() {
2729		tokio::time::pause();
2730
2731		let mut producer = track_producer("test", None);
2732		producer.append_group().unwrap(); // seq 0
2733
2734		let mut consumer = producer.subscribe(None);
2735
2736		tokio::time::advance(DEFAULT_LATENCY_MAX + Duration::from_secs(1)).await;
2737		producer.append_group().unwrap(); // seq 1
2738
2739		// Group 0 was evicted. Consumer should get group 1.
2740		let group = consumer.assert_group();
2741		assert_eq!(group.sequence, 1);
2742	}
2743
2744	#[tokio::test]
2745	async fn cache_age_controls_eviction() {
2746		tokio::time::pause();
2747
2748		// A shorter cache evicts sooner than the default.
2749		let mut producer = track_producer("test", Info::default().with_latency_max(Duration::from_secs(1)));
2750		producer.append_group().unwrap(); // seq 0
2751
2752		// Past the custom budget but well within DEFAULT_LATENCY_MAX.
2753		tokio::time::advance(Duration::from_secs(2)).await;
2754		producer.append_group().unwrap(); // seq 1
2755
2756		// Seq 0 is gone because the publisher only keeps groups for 1s.
2757		let state = producer.state.read();
2758		assert_eq!(live_groups(&state), 1);
2759		assert_eq!(first_live_sequence(&state), 1);
2760	}
2761
2762	#[test]
2763	fn latency_max_clamped_to_cache() {
2764		let producer = track_producer("test", Info::default().with_latency_max(Duration::from_secs(2)));
2765
2766		// A latency budget beyond the cache is capped in the aggregate; a group can't be
2767		// waited for longer than the publisher keeps it. The subscriber's own preference
2768		// is stored verbatim, so what it asked for stays readable.
2769		let mut subscriber = producer.subscribe(Subscription::default().with_latency_max(Duration::from_secs(10)));
2770		assert_eq!(subscriber.subscription().latency_max, Duration::from_secs(10));
2771		assert_eq!(producer.subscription().unwrap().latency_max, Duration::from_secs(2));
2772
2773		// A budget within the cache is left alone, and ZERO (skip immediately) stays ZERO.
2774		subscriber
2775			.update(Subscription::default().with_latency_max(Duration::from_millis(500)))
2776			.unwrap();
2777		assert_eq!(producer.subscription().unwrap().latency_max, Duration::from_millis(500));
2778
2779		subscriber
2780			.update(Subscription::default().with_latency_max(Duration::ZERO))
2781			.unwrap();
2782		assert_eq!(producer.subscription().unwrap().latency_max, Duration::ZERO);
2783	}
2784
2785	#[test]
2786	fn latency_max_clamped_via_every_update_path() {
2787		let producer = track_producer("test", Info::default().with_latency_max(Duration::from_secs(2)));
2788		let over = Subscription::default().with_latency_max(Duration::from_secs(10));
2789
2790		// The clamp lives in the aggregation, so it applies no matter which entry point
2791		// wrote the raw preference. Previously only `Subscriber::update` clamped.
2792		let mut subscriber = producer.subscribe(over.clone());
2793		assert_eq!(producer.subscription().unwrap().latency_max, Duration::from_secs(2));
2794
2795		subscriber.control().update(over.clone()).unwrap();
2796		assert_eq!(producer.subscription().unwrap().latency_max, Duration::from_secs(2));
2797
2798		subscriber.update(over).unwrap();
2799		assert_eq!(producer.subscription().unwrap().latency_max, Duration::from_secs(2));
2800	}
2801
2802	#[test]
2803	fn latency_max_aggregate_clamps_the_max_across_subscribers() {
2804		let producer = track_producer("test", Info::default().with_latency_max(Duration::from_secs(2)));
2805
2806		// The aggregate takes the max, then clamps once. Equivalent to clamping each
2807		// subscriber first, since `min` distributes over `max`.
2808		let _a = producer.subscribe(Subscription::default().with_latency_max(Duration::from_millis(500)));
2809		let _b = producer.subscribe(Subscription::default().with_latency_max(Duration::from_secs(10)));
2810
2811		assert_eq!(producer.subscription().unwrap().latency_max, Duration::from_secs(2));
2812	}
2813
2814	#[test]
2815	fn subscriber_control_updates_while_read_future_is_pending() {
2816		let producer = track_producer("test", None);
2817		let mut subscriber = producer.subscribe(None);
2818		let control = subscriber.control();
2819
2820		let mut recv = Box::pin(subscriber.recv_group());
2821		assert!(recv.as_mut().now_or_never().is_none());
2822
2823		control
2824			.update(Subscription::default().with_priority(7).with_ordered(false))
2825			.unwrap();
2826
2827		let aggregate = producer.subscription().expect("expected an active subscription");
2828		assert_eq!(aggregate.priority, 7);
2829		assert!(!aggregate.ordered);
2830	}
2831
2832	#[test]
2833	fn dropped_subscriber_leaves_no_ghost_in_aggregate() {
2834		// Regression (#2351): a departed subscriber must not keep contributing its
2835		// last subscription to the aggregate. When it did, a relay's linger loop
2836		// never observed the track going idle, and an identical viewer reconnecting
2837		// within the linger window was reset when the stale timer fired.
2838		let mut producer = track_producer("test", None);
2839		let a = producer.subscribe(Subscription::default().with_priority(5));
2840
2841		// Prime the change cursor: the aggregate currently has one subscriber.
2842		let waiter = kio::Waiter::noop();
2843		assert!(
2844			matches!(producer.poll_subscription_changed(&waiter), Poll::Ready(Ok(Some(_)))),
2845			"one live subscriber should aggregate to Some",
2846		);
2847
2848		// The only subscriber leaves.
2849		drop(a);
2850
2851		// The aggregate must report the drop to None, not the ghost's last value.
2852		assert!(
2853			matches!(producer.poll_subscription_changed(&waiter), Poll::Ready(Ok(None))),
2854			"a dropped subscriber must not linger in the aggregate",
2855		);
2856
2857		// And the snapshot used by the linger loop must agree.
2858		assert!(
2859			producer.subscription().is_none(),
2860			"snapshot must exclude a dropped subscriber",
2861		);
2862	}
2863
2864	#[tokio::test]
2865	async fn out_of_order_max_sequence_at_front() {
2866		tokio::time::pause();
2867
2868		let mut producer = track_producer("test", None);
2869
2870		// Arrive out of order: seq 5 first, then 3, then 4.
2871		producer.create_group(group::Info { sequence: 5 }).unwrap();
2872		producer.create_group(group::Info { sequence: 3 }).unwrap();
2873		producer.create_group(group::Info { sequence: 4 }).unwrap();
2874
2875		// max_sequence = 5, which is at the front of the VecDeque.
2876		{
2877			let state = producer.state.read();
2878			assert_eq!(state.max_sequence, Some(5));
2879		}
2880
2881		// Expire all three groups.
2882		tokio::time::advance(DEFAULT_LATENCY_MAX + Duration::from_secs(1)).await;
2883
2884		// Append seq 6 (becomes new max_sequence).
2885		producer.append_group().unwrap(); // seq 6
2886
2887		// Seq 3, 4, 5 are all expired. Seq 5 was the old max_sequence but now 6 is.
2888		// All old groups are evicted.
2889		{
2890			let state = producer.state.read();
2891			assert_eq!(live_groups(&state), 1);
2892			assert_eq!(first_live_sequence(&state), 6);
2893			assert!(!state.duplicates.contains(&3));
2894			assert!(!state.duplicates.contains(&4));
2895			assert!(!state.duplicates.contains(&5));
2896			assert!(state.duplicates.contains(&6));
2897		}
2898	}
2899
2900	#[tokio::test]
2901	async fn max_sequence_at_front_blocks_trim() {
2902		tokio::time::pause();
2903
2904		let mut producer = track_producer("test", None);
2905
2906		// Arrive: seq 5, then seq 3.
2907		producer.create_group(group::Info { sequence: 5 }).unwrap();
2908
2909		tokio::time::advance(DEFAULT_LATENCY_MAX + Duration::from_secs(1)).await;
2910
2911		// Seq 3 arrives late; max_sequence is still 5 (at front).
2912		producer.create_group(group::Info { sequence: 3 }).unwrap();
2913
2914		// Seq 5 is max_sequence (protected). Seq 3 is not expired (just created).
2915		// Nothing should be evicted.
2916		{
2917			let state = producer.state.read();
2918			assert_eq!(live_groups(&state), 2);
2919			assert_eq!(state.offset, 0);
2920		}
2921
2922		// Expire seq 3 as well.
2923		tokio::time::advance(DEFAULT_LATENCY_MAX + Duration::from_secs(1)).await;
2924
2925		// Seq 2 arrives late, triggering eviction.
2926		producer.create_group(group::Info { sequence: 2 }).unwrap();
2927
2928		// Seq 5 is still max_sequence (protected, at front, blocks trim).
2929		// Seq 3 is expired → tombstoned.
2930		// Seq 2 is fresh → kept.
2931		// VecDeque: [Some(5), None, Some(2)]. Leading entry is Some, so offset stays.
2932		{
2933			let state = producer.state.read();
2934			assert_eq!(live_groups(&state), 2);
2935			assert_eq!(state.offset, 0);
2936			assert!(state.duplicates.contains(&5));
2937			assert!(!state.duplicates.contains(&3));
2938			assert!(state.duplicates.contains(&2));
2939		}
2940
2941		// Consumer should still be able to read through the hole.
2942		let mut consumer = producer.subscribe(None);
2943		let group = consumer.assert_group();
2944		// consume() starts at index 0, first non-tombstoned group is seq 5.
2945		assert_eq!(group.sequence, 5);
2946	}
2947
2948	#[tokio::test]
2949	async fn abort_clears_cached_groups() {
2950		let mut producer = track_producer("test", None);
2951		producer.append_group().unwrap();
2952		producer.append_group().unwrap();
2953
2954		// A stale consumer that never drains must not pin the cached groups.
2955		let mut consumer = producer.subscribe(None);
2956		assert_eq!(live_groups(&producer.state.read()), 2);
2957
2958		producer.clone().abort(Error::Cancel).unwrap();
2959
2960		{
2961			let state = producer.state.read();
2962			assert!(state.groups.is_empty(), "cached groups should be dropped on abort");
2963			assert!(state.duplicates.is_empty());
2964		}
2965
2966		// The consumer now surfaces the abort error rather than the leftover cache.
2967		let result = consumer.recv_group().now_or_never().expect("should not block");
2968		assert!(matches!(result, Err(Error::Cancel)));
2969	}
2970
2971	#[tokio::test]
2972	async fn drop_unfinished_clears_cached_groups() {
2973		let producer = track_producer("test", None);
2974		let mut writer = producer.clone();
2975		writer.append_group().unwrap();
2976
2977		// A stale consumer keeps the channel (and thus the cache) alive.
2978		let mut consumer = producer.subscribe(None);
2979		assert_eq!(live_groups(&producer.state.read()), 1);
2980
2981		// Drop every producer without finishing: the cache is released.
2982		drop(writer);
2983		drop(producer);
2984
2985		let result = consumer.recv_group().now_or_never().expect("should not block");
2986		assert!(matches!(result, Err(Error::Dropped)));
2987	}
2988
2989	#[tokio::test]
2990	async fn drop_finished_keeps_cached_groups() {
2991		let mut producer = track_producer("test", None);
2992		producer.append_group().unwrap();
2993		producer.finish().unwrap();
2994
2995		let mut consumer = producer.subscribe(None);
2996		drop(producer);
2997
2998		// A cleanly finished track keeps its cache so the consumer can still drain.
2999		assert_eq!(consumer.assert_group().sequence, 0);
3000		let done = consumer.recv_group().now_or_never().expect("should not block").unwrap();
3001		assert!(done.is_none(), "consumer should drain then see clean finish");
3002	}
3003
3004	#[test]
3005	fn append_finish_cannot_be_rewritten() {
3006		let mut producer = track_producer("test", None);
3007
3008		// Finishing an empty track is valid (fin = 0, total groups = 0).
3009		assert!(producer.finish().is_ok());
3010		assert!(producer.finish().is_err());
3011		assert!(producer.append_group().is_err());
3012	}
3013
3014	#[test]
3015	fn finish_after_groups() {
3016		let mut producer = track_producer("test", None);
3017
3018		producer.append_group().unwrap();
3019		assert!(producer.finish().is_ok());
3020		assert!(producer.finish().is_err());
3021		assert!(producer.append_group().is_err());
3022	}
3023
3024	#[test]
3025	fn finish_at_rejects_a_boundary_at_or_below_the_live_edge() {
3026		let mut producer = track_producer("test", None);
3027		producer.create_group(group::Info { sequence: 5 }).unwrap();
3028
3029		// The boundary is exclusive, so it must be strictly above the highest produced
3030		// group. 5 or below would orphan groups that already exist.
3031		assert!(producer.finish_at(4).is_err());
3032		assert!(producer.finish_at(5).is_err());
3033		assert!(producer.finish_at(6).is_ok());
3034
3035		{
3036			let state = producer.state.read();
3037			assert_eq!(state.final_sequence, Some(6));
3038		}
3039
3040		// Re-finishing is rejected, and no group at or above the boundary can be created.
3041		assert!(producer.finish_at(6).is_err());
3042		assert!(producer.create_group(group::Info { sequence: 4 }).is_ok());
3043		assert!(producer.create_group(group::Info { sequence: 6 }).is_err());
3044	}
3045
3046	#[test]
3047	fn final_sequence_reports_the_declared_boundary() {
3048		let mut producer = track_producer("test", None);
3049		assert_eq!(producer.final_sequence(), None);
3050
3051		producer.create_group(group::Info { sequence: 5 }).unwrap();
3052		assert_eq!(producer.final_sequence(), None, "a group does not declare a boundary");
3053
3054		producer.finish_at(9).unwrap();
3055		assert_eq!(producer.final_sequence(), Some(9));
3056
3057		// finish() would try to declare a second boundary, so callers check first.
3058		assert!(producer.finish().is_err());
3059	}
3060
3061	#[test]
3062	fn final_sequence_reports_the_live_edge_after_finish() {
3063		let mut producer = track_producer("test", None);
3064		producer.create_group(group::Info { sequence: 5 }).unwrap();
3065		producer.finish().unwrap();
3066		assert_eq!(producer.final_sequence(), Some(6));
3067	}
3068
3069	#[tokio::test]
3070	async fn finish_at_declares_a_future_boundary() {
3071		let mut producer = track_producer("test", None);
3072		producer.create_group(group::Info { sequence: 5 }).unwrap();
3073
3074		// Learn the track ends at group 6 (exclusive 7) while the live edge is still 5.
3075		producer.finish_at(7).unwrap();
3076
3077		let mut consumer = producer.subscribe(None);
3078		assert_eq!(consumer.assert_group().sequence, 5);
3079
3080		// The boundary is known immediately, but the track isn't done: group 6 is still
3081		// outstanding, so the consumer parks rather than seeing end-of-stream.
3082		let boundary = consumer
3083			.finished()
3084			.now_or_never()
3085			.expect("boundary is known immediately")
3086			.expect("would have errored");
3087		assert_eq!(boundary, 7);
3088		assert!(
3089			consumer.recv_group().now_or_never().is_none(),
3090			"should wait for the outstanding group"
3091		);
3092
3093		// The trailing group arrives (below the boundary), then the track completes.
3094		producer.create_group(group::Info { sequence: 6 }).unwrap();
3095		assert_eq!(consumer.assert_group().sequence, 6);
3096		let done = consumer
3097			.recv_group()
3098			.now_or_never()
3099			.expect("should not block")
3100			.expect("would have errored");
3101		assert!(done.is_none(), "track completes once the boundary is reached");
3102	}
3103
3104	#[tokio::test]
3105	async fn recv_group_finishes_without_waiting_for_gaps() {
3106		let mut producer = track_producer("test", None);
3107		producer.create_group(group::Info { sequence: 1 }).unwrap();
3108		producer.finish().unwrap();
3109
3110		let mut consumer = producer.subscribe(None);
3111		assert_eq!(consumer.assert_group().sequence, 1);
3112
3113		let done = consumer
3114			.recv_group()
3115			.now_or_never()
3116			.expect("should not block")
3117			.expect("would have errored");
3118		assert!(done.is_none(), "track should finish without waiting for gaps");
3119	}
3120
3121	#[tokio::test]
3122	async fn next_group_skips_late_arrivals() {
3123		let mut producer = track_producer("test", None);
3124		let mut consumer = producer.subscribe(None);
3125
3126		// Seq 5 arrives first.
3127		producer.create_group(group::Info { sequence: 5 }).unwrap();
3128		let group = consumer
3129			.next_group()
3130			.now_or_never()
3131			.expect("should not block")
3132			.expect("would have errored")
3133			.expect("track should not be closed");
3134		assert_eq!(group.sequence, 5);
3135
3136		// Seq 3 arrives late, skipped because 3 <= 5.
3137		producer.create_group(group::Info { sequence: 3 }).unwrap();
3138		// Seq 4 arrives late and is also skipped.
3139		producer.create_group(group::Info { sequence: 4 }).unwrap();
3140		// Seq 7 arrives and is returned.
3141		producer.create_group(group::Info { sequence: 7 }).unwrap();
3142
3143		let group = consumer
3144			.next_group()
3145			.now_or_never()
3146			.expect("should not block")
3147			.expect("would have errored")
3148			.expect("track should not be closed");
3149		assert_eq!(group.sequence, 7);
3150
3151		// No more groups. This would block.
3152		assert!(
3153			consumer.next_group().now_or_never().is_none(),
3154			"should block waiting for a higher sequence"
3155		);
3156	}
3157
3158	#[tokio::test]
3159	async fn next_group_returns_arrivals_in_order() {
3160		let mut producer = track_producer("test", None);
3161		let mut consumer = producer.subscribe(None);
3162
3163		// Seq 3 arrives first, then seq 5. Both should be returned in arrival order.
3164		producer.create_group(group::Info { sequence: 3 }).unwrap();
3165		producer.create_group(group::Info { sequence: 5 }).unwrap();
3166
3167		let group = consumer
3168			.next_group()
3169			.now_or_never()
3170			.expect("should not block")
3171			.expect("would have errored")
3172			.expect("track should not be closed");
3173		assert_eq!(group.sequence, 3);
3174
3175		let group = consumer
3176			.next_group()
3177			.now_or_never()
3178			.expect("should not block")
3179			.expect("would have errored")
3180			.expect("track should not be closed");
3181		assert_eq!(group.sequence, 5);
3182	}
3183
3184	#[tokio::test]
3185	async fn next_group_and_recv_group_use_independent_cursors() {
3186		let mut producer = track_producer("test", None);
3187		let mut consumer = producer.subscribe(None);
3188
3189		// Out-of-order arrivals: seq 5 first, then seq 3.
3190		producer.create_group(group::Info { sequence: 5 }).unwrap();
3191		producer.create_group(group::Info { sequence: 3 }).unwrap();
3192
3193		// next_group is sequence-ordered: it returns the smallest sequence first,
3194		// regardless of arrival order.
3195		let group = consumer
3196			.next_group()
3197			.now_or_never()
3198			.expect("should not block")
3199			.expect("would have errored")
3200			.expect("track should not be closed");
3201		assert_eq!(group.sequence, 3);
3202
3203		// recv_group is arrival-ordered and uses an independent cursor, so it
3204		// still starts at the first arrival.
3205		assert_eq!(consumer.assert_group().sequence, 5);
3206	}
3207
3208	#[tokio::test]
3209	async fn end_at_caps_next_group() {
3210		let mut producer = track_producer("test", None);
3211		let mut consumer = producer.subscribe(None);
3212
3213		for s in 0..6 {
3214			producer.create_group(group::Info { sequence: s }).unwrap();
3215		}
3216
3217		consumer.end_at(2);
3218
3219		// Groups 0, 1, 2 are within the cap.
3220		assert_eq!(
3221			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3222			0
3223		);
3224		assert_eq!(
3225			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3226			1
3227		);
3228		assert_eq!(
3229			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3230			2
3231		);
3232
3233		// Group 3 is beyond the cap: next_group parks even though cached groups exist.
3234		assert!(
3235			consumer.next_group().now_or_never().is_none(),
3236			"capped consumer must block instead of returning out-of-range groups"
3237		);
3238	}
3239
3240	#[tokio::test]
3241	async fn end_at_release_drains_cached_groups() {
3242		let mut producer = track_producer("test", None);
3243		let mut consumer = producer.subscribe(None);
3244
3245		for s in 0..6 {
3246			producer.create_group(group::Info { sequence: s }).unwrap();
3247		}
3248
3249		consumer.end_at(1);
3250		assert_eq!(
3251			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3252			0
3253		);
3254		assert_eq!(
3255			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3256			1
3257		);
3258		assert!(consumer.next_group().now_or_never().is_none(), "capped at 1");
3259
3260		// Raise the cap; previously-blocked cached groups become available again.
3261		consumer.end_at(4);
3262		assert_eq!(
3263			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3264			2
3265		);
3266		assert_eq!(
3267			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3268			3
3269		);
3270		assert_eq!(
3271			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3272			4
3273		);
3274		assert!(consumer.next_group().now_or_never().is_none(), "capped at 4");
3275
3276		// Remove the cap; everything remaining flows.
3277		consumer.end_at(None);
3278		assert_eq!(
3279			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3280			5
3281		);
3282		assert!(consumer.next_group().now_or_never().is_none(), "no more groups");
3283	}
3284
3285	#[tokio::test]
3286	async fn end_at_lower_than_cursor_parks_consumer() {
3287		let mut producer = track_producer("test", None);
3288		let mut consumer = producer.subscribe(None);
3289
3290		for s in 0..3 {
3291			producer.create_group(group::Info { sequence: s }).unwrap();
3292		}
3293
3294		// Drain everything with no cap.
3295		assert_eq!(
3296			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3297			0
3298		);
3299		assert_eq!(
3300			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3301			1
3302		);
3303		assert_eq!(
3304			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3305			2
3306		);
3307
3308		// Lower the cap below the cursor. New groups beyond the cap are blocked.
3309		consumer.end_at(1);
3310		producer.create_group(group::Info { sequence: 3 }).unwrap();
3311		producer.create_group(group::Info { sequence: 4 }).unwrap();
3312		assert!(
3313			consumer.next_group().now_or_never().is_none(),
3314			"cap is below cursor; nothing returnable until cap rises"
3315		);
3316
3317		// Restoring the cap to no-limit (or any value >= cursor) releases them.
3318		consumer.end_at(None);
3319		assert_eq!(
3320			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3321			3
3322		);
3323		assert_eq!(
3324			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3325			4
3326		);
3327	}
3328
3329	#[tokio::test]
3330	async fn end_at_toggling_around_late_arrivals() {
3331		let mut producer = track_producer("test", None);
3332		let mut consumer = producer.subscribe(None);
3333
3334		consumer.end_at(5);
3335
3336		// Out-of-order arrivals all within the cap.
3337		producer.create_group(group::Info { sequence: 2 }).unwrap();
3338		producer.create_group(group::Info { sequence: 5 }).unwrap();
3339		producer.create_group(group::Info { sequence: 3 }).unwrap();
3340		// One beyond the cap; should be held even though it arrived in the middle.
3341		producer.create_group(group::Info { sequence: 8 }).unwrap();
3342		producer.create_group(group::Info { sequence: 4 }).unwrap();
3343
3344		// next_group walks in sequence order through everything <= cap.
3345		assert_eq!(
3346			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3347			2
3348		);
3349		assert_eq!(
3350			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3351			3
3352		);
3353		assert_eq!(
3354			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3355			4
3356		);
3357		assert_eq!(
3358			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3359			5
3360		);
3361		// Now blocked: 8 is still beyond the cap.
3362		assert!(consumer.next_group().now_or_never().is_none());
3363
3364		// Raise the cap; cached seq 8 is finally served.
3365		consumer.end_at(10);
3366		assert_eq!(
3367			consumer.next_group().now_or_never().unwrap().unwrap().unwrap().sequence,
3368			8
3369		);
3370	}
3371
3372	#[tokio::test]
3373	async fn read_frame_returns_single_frame_per_group() {
3374		let mut producer = track_producer("test", None);
3375		let mut consumer = producer.subscribe(None);
3376
3377		producer.write_frame(Timestamp::ZERO, b"hello".as_slice()).unwrap();
3378		producer.write_frame(Timestamp::ZERO, b"world".as_slice()).unwrap();
3379
3380		let frame = consumer
3381			.read_frame()
3382			.now_or_never()
3383			.expect("should not block")
3384			.expect("would have errored")
3385			.expect("track should not be closed");
3386		assert_eq!(&frame.payload[..], b"hello");
3387
3388		let frame = consumer
3389			.read_frame()
3390			.now_or_never()
3391			.expect("should not block")
3392			.expect("would have errored")
3393			.expect("track should not be closed");
3394		assert_eq!(&frame.payload[..], b"world");
3395	}
3396
3397	#[tokio::test]
3398	async fn read_frame_preserves_timestamp() {
3399		let mut producer = track_producer("test", None);
3400		let mut consumer = producer.subscribe(None);
3401
3402		producer
3403			.write_frame(Timestamp::from_micros(20_000).unwrap(), b"hello".as_slice())
3404			.unwrap();
3405
3406		let frame = consumer
3407			.read_frame()
3408			.now_or_never()
3409			.expect("should not block")
3410			.expect("would have errored")
3411			.expect("track should not be closed");
3412		assert_eq!(frame.timestamp.as_micros(), 20_000);
3413		assert_eq!(&frame.payload[..], b"hello");
3414	}
3415
3416	#[tokio::test]
3417	async fn read_frame_skips_stalled_group_for_newer_ready_frame() {
3418		let mut producer = track_producer("test", None);
3419		let mut consumer = producer.subscribe(None);
3420
3421		// Seq 3: group open, no frame yet (stalled).
3422		let _stalled = producer.create_group(group::Info { sequence: 3 }).unwrap();
3423		// Seq 5: fully-written group with a frame.
3424		let mut g5 = producer.create_group(group::Info { sequence: 5 }).unwrap();
3425		g5.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"later"))
3426			.unwrap();
3427		g5.finish().unwrap();
3428
3429		// read_frame should not block on the stalled seq 3. It returns seq 5's frame.
3430		let frame = consumer
3431			.read_frame()
3432			.now_or_never()
3433			.expect("should not block on stalled earlier group")
3434			.expect("would have errored")
3435			.expect("track should not be closed");
3436		assert_eq!(&frame.payload[..], b"later");
3437	}
3438
3439	#[tokio::test]
3440	async fn read_frame_discards_rest_of_multi_frame_group() {
3441		let mut producer = track_producer("test", None);
3442		let mut consumer = producer.subscribe(None);
3443
3444		// Group 0 has two frames; only the first is returned.
3445		let mut g0 = producer.create_group(group::Info { sequence: 0 }).unwrap();
3446		g0.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"one"))
3447			.unwrap();
3448		g0.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"two"))
3449			.unwrap();
3450		g0.finish().unwrap();
3451
3452		// Group 1 is a normal single-frame group.
3453		producer.write_frame(Timestamp::ZERO, b"next".as_slice()).unwrap();
3454
3455		let frame = consumer
3456			.read_frame()
3457			.now_or_never()
3458			.expect("should not block")
3459			.expect("would have errored")
3460			.expect("track should not be closed");
3461		assert_eq!(&frame.payload[..], b"one");
3462
3463		// The second frame of group 0 is discarded; the next read jumps to group 1.
3464		let frame = consumer
3465			.read_frame()
3466			.now_or_never()
3467			.expect("should not block")
3468			.expect("would have errored")
3469			.expect("track should not be closed");
3470		assert_eq!(&frame.payload[..], b"next");
3471	}
3472
3473	#[tokio::test]
3474	async fn read_frame_waits_for_pending_group_after_finish() {
3475		// finish() sets final_sequence, but groups already created with lower sequences
3476		// can still produce frames. read_frame must not return None prematurely.
3477		let mut producer = track_producer("test", None);
3478		let mut consumer = producer.subscribe(None);
3479
3480		let mut g0 = producer.create_group(group::Info { sequence: 0 }).unwrap();
3481		producer.finish().unwrap();
3482
3483		// Track is finished but group 0 has no frame yet. It must block, not return None.
3484		assert!(
3485			consumer.read_frame().now_or_never().is_none(),
3486			"read_frame must block on a pending group even after finish()"
3487		);
3488
3489		// A late frame on the pending group is still delivered.
3490		g0.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"late"))
3491			.unwrap();
3492		let frame = consumer
3493			.read_frame()
3494			.now_or_never()
3495			.expect("should not block once a frame is written")
3496			.expect("would have errored")
3497			.expect("track should not be closed");
3498		assert_eq!(&frame.payload[..], b"late");
3499	}
3500
3501	#[tokio::test]
3502	async fn read_frame_respects_start_at() {
3503		// start_at sets min_sequence; read_frame must skip groups below it even though
3504		// next_sequence is still 0.
3505		let mut producer = track_producer("test", None);
3506		let mut consumer = producer.subscribe(None);
3507		consumer.start_at(5);
3508
3509		// Seq 3 has a frame but is below min_sequence, so it must be skipped.
3510		let mut g3 = producer.create_group(group::Info { sequence: 3 }).unwrap();
3511		g3.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"skip-me"))
3512			.unwrap();
3513		g3.finish().unwrap();
3514
3515		let mut g5 = producer.create_group(group::Info { sequence: 5 }).unwrap();
3516		g5.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"keep"))
3517			.unwrap();
3518		g5.finish().unwrap();
3519
3520		let frame = consumer
3521			.read_frame()
3522			.now_or_never()
3523			.expect("should not block")
3524			.expect("would have errored")
3525			.expect("track should not be closed");
3526		assert_eq!(&frame.payload[..], b"keep");
3527	}
3528
3529	#[tokio::test]
3530	async fn read_frame_returns_none_when_finished() {
3531		let mut producer = track_producer("test", None);
3532		let mut consumer = producer.subscribe(None);
3533
3534		producer.write_frame(Timestamp::ZERO, b"only".as_slice()).unwrap();
3535		producer.finish().unwrap();
3536
3537		let frame = consumer
3538			.read_frame()
3539			.now_or_never()
3540			.expect("should not block")
3541			.expect("would have errored")
3542			.expect("track should not be closed");
3543		assert_eq!(&frame.payload[..], b"only");
3544
3545		let done = consumer
3546			.read_frame()
3547			.now_or_never()
3548			.expect("should not block")
3549			.expect("would have errored");
3550		assert!(done.is_none());
3551	}
3552
3553	#[test]
3554	fn append_group_returns_bounds_exceeded_on_sequence_overflow() {
3555		let mut producer = track_producer("test", None);
3556		{
3557			let mut state = producer.state.write().ok().unwrap();
3558			state.max_sequence = Some(u64::MAX);
3559		}
3560
3561		assert!(matches!(producer.append_group(), Err(Error::BoundsExceeded(_))));
3562	}
3563
3564	#[tokio::test]
3565	async fn fetch_cache_hit() {
3566		let mut producer = track_producer("test", None);
3567
3568		// Produce a cached group.
3569		let mut group = producer.append_group().unwrap(); // seq 0
3570		group
3571			.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"hello"))
3572			.unwrap();
3573		group.finish().unwrap();
3574
3575		// A cached group resolves immediately and never queues a request. `peek_group`
3576		// also returns it synchronously.
3577		let dynamic = producer.dynamic();
3578		let consumer = producer.consume();
3579		assert!(consumer.peek_group(0).is_some());
3580		let mut g = consumer.fetch_group(0, None).await.unwrap();
3581		assert_eq!(g.sequence, 0);
3582		assert_eq!(&g.read_frame().await.unwrap().unwrap().payload[..], b"hello");
3583
3584		// Nothing was queued for the dynamic handler to serve.
3585		assert!(dynamic.poll_requested_group(&kio::Waiter::noop()).is_pending());
3586	}
3587
3588	#[tokio::test]
3589	async fn fetch_miss_signals_dynamic() {
3590		let producer = track_producer("test", None);
3591		let dynamic = producer.dynamic();
3592		let consumer = producer.consume();
3593
3594		// A cache miss isn't in `peek_group`, but a dynamic handler exists, so
3595		// `fetch_group` stays pending and queues a request. `*pending` derefs the
3596		// wrapper to the inner `Fetching` (a `kio::Pollable`).
3597		assert!(consumer.peek_group(5).is_none());
3598		let pending = consumer.fetch_group(5, group::Fetch::default().with_priority(7));
3599		assert!(kio::Pollable::poll(&*pending, &kio::Waiter::noop()).is_pending());
3600
3601		let req = dynamic
3602			.requested_group()
3603			.now_or_never()
3604			.expect("should not block")
3605			.unwrap();
3606		assert_eq!(req.sequence(), 5);
3607		assert_eq!(req.priority(), 7);
3608
3609		// Serve it by accepting the request; the fetch then resolves.
3610		let mut group = req.accept(None).unwrap();
3611		group
3612			.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"hi"))
3613			.unwrap();
3614		group.finish().unwrap();
3615
3616		let mut g = pending.await.unwrap();
3617		assert_eq!(g.sequence, 5);
3618		assert_eq!(&g.read_frame().await.unwrap().unwrap().payload[..], b"hi");
3619	}
3620
3621	#[tokio::test]
3622	async fn fetch_miss_rejects() {
3623		let producer = track_producer("test", None);
3624		let dynamic = producer.dynamic();
3625		let consumer = producer.consume();
3626
3627		let pending = consumer.fetch_group(5, None);
3628		let req = dynamic
3629			.requested_group()
3630			.now_or_never()
3631			.expect("should not block")
3632			.unwrap();
3633
3634		req.reject(Error::Cancel);
3635		assert!(matches!(pending.await, Err(Error::Cancel)));
3636		let fetch = producer.state.read().fetch.clone();
3637		assert!(fetch.read().is_empty());
3638	}
3639
3640	#[tokio::test]
3641	async fn fetch_miss_drop_rejects() {
3642		let producer = track_producer("test", None);
3643		let dynamic = producer.dynamic();
3644		let consumer = producer.consume();
3645
3646		let pending = consumer.fetch_group(5, None);
3647		let req = dynamic
3648			.requested_group()
3649			.now_or_never()
3650			.expect("should not block")
3651			.unwrap();
3652
3653		drop(req);
3654		assert!(matches!(pending.await, Err(Error::Dropped)));
3655	}
3656
3657	#[tokio::test]
3658	async fn fetch_reject_does_not_poison_retry() {
3659		let producer = track_producer("test", None);
3660		let dynamic = producer.dynamic();
3661		let consumer = producer.consume();
3662
3663		let pending = consumer.fetch_group(5, None);
3664		let req = dynamic
3665			.requested_group()
3666			.now_or_never()
3667			.expect("should not block")
3668			.unwrap();
3669		req.reject(Error::Cancel);
3670		assert!(matches!(pending.await, Err(Error::Cancel)));
3671
3672		let retry = consumer.fetch_group(5, None);
3673		let req = dynamic
3674			.requested_group()
3675			.now_or_never()
3676			.expect("should not block")
3677			.unwrap();
3678		let mut group = req.accept(None).unwrap();
3679		group
3680			.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"retry"))
3681			.unwrap();
3682		group.finish().unwrap();
3683
3684		let mut group = retry.await.unwrap();
3685		assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"retry");
3686	}
3687
3688	#[tokio::test]
3689	async fn fetch_coalesces_concurrent() {
3690		let producer = track_producer("test", None);
3691		let dynamic = producer.dynamic();
3692		let consumer = producer.consume();
3693
3694		// Two fetches for the same uncached group produce ONE handler request,
3695		// carrying the higher of the two priorities.
3696		let first = consumer.fetch_group(5, group::Fetch::default().with_priority(1));
3697		let second = consumer.fetch_group(5, group::Fetch::default().with_priority(7));
3698		assert!(kio::Pollable::poll(&*first, &kio::Waiter::noop()).is_pending());
3699
3700		let req = dynamic
3701			.requested_group()
3702			.now_or_never()
3703			.expect("should not block")
3704			.unwrap();
3705		assert_eq!(req.sequence(), 5);
3706		assert_eq!(req.priority(), 7);
3707		assert!(
3708			dynamic.poll_requested_group(&kio::Waiter::noop()).is_pending(),
3709			"the second fetch queued a duplicate request"
3710		);
3711
3712		// A fetch arriving while the request is already in flight joins it too.
3713		let third = consumer.fetch_group(5, None);
3714
3715		// One accept resolves all of them.
3716		let mut group = req.accept(None).unwrap();
3717		group
3718			.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"hi"))
3719			.unwrap();
3720		group.finish().unwrap();
3721
3722		assert_eq!(first.await.unwrap().sequence, 5);
3723		assert_eq!(second.await.unwrap().sequence, 5);
3724		assert_eq!(third.await.unwrap().sequence, 5);
3725	}
3726
3727	#[tokio::test]
3728	async fn fetch_coalesced_reject_fails_all() {
3729		let producer = track_producer("test", None);
3730		let dynamic = producer.dynamic();
3731		let consumer = producer.consume();
3732
3733		let first = consumer.fetch_group(5, None);
3734		let second = consumer.fetch_group(5, None);
3735		let req = dynamic
3736			.requested_group()
3737			.now_or_never()
3738			.expect("should not block")
3739			.unwrap();
3740		req.reject(Error::Cancel);
3741
3742		assert!(matches!(first.await, Err(Error::Cancel)));
3743		assert!(matches!(second.await, Err(Error::Cancel)));
3744
3745		// The rejected attempt is gone: a retry starts a fresh one.
3746		let retry = consumer.fetch_group(5, None);
3747		assert!(kio::Pollable::poll(&*retry, &kio::Waiter::noop()).is_pending());
3748		let req = dynamic
3749			.requested_group()
3750			.now_or_never()
3751			.expect("should not block")
3752			.unwrap();
3753		assert_eq!(req.sequence(), 5);
3754	}
3755
3756	#[tokio::test]
3757	async fn fetch_queued_fails_when_handlers_leave() {
3758		let producer = track_producer("test", None);
3759		let dynamic = producer.dynamic();
3760		let consumer = producer.consume();
3761
3762		// Queued but never popped: the last handler leaving fails it fast.
3763		let pending = consumer.fetch_group(5, None);
3764		assert!(kio::Pollable::poll(&*pending, &kio::Waiter::noop()).is_pending());
3765		drop(dynamic);
3766		assert!(matches!(pending.await, Err(Error::NotFound)));
3767
3768		// And the attempt didn't leak.
3769		let fetch = producer.state.read().fetch.clone();
3770		assert!(fetch.read().is_empty());
3771	}
3772
3773	#[tokio::test]
3774	async fn fetch_miss_no_dynamic_not_found() {
3775		// A track with no `Dynamic` can't serve old content, so a cache miss
3776		// resolves to NotFound instead of blocking forever.
3777		let mut producer = track_producer("test", None);
3778		producer.append_group().unwrap(); // seq 0, but we miss on seq 5
3779		let consumer = producer.consume();
3780		assert!(matches!(consumer.fetch_group(5, None).await, Err(Error::NotFound)));
3781	}
3782
3783	#[tokio::test]
3784	async fn fetch_past_final_not_found() {
3785		let mut producer = track_producer("test", None);
3786		producer.append_group().unwrap(); // seq 0
3787		producer.finish().unwrap(); // final_sequence = 1
3788
3789		// A group at or past the final sequence can never exist, even with a handler,
3790		// so it resolves to NotFound.
3791		let dynamic = producer.dynamic();
3792		let consumer = producer.consume();
3793		assert!(matches!(consumer.fetch_group(5, None).await, Err(Error::NotFound)));
3794
3795		// And it doesn't signal the dynamic handler.
3796		assert!(dynamic.poll_requested_group(&kio::Waiter::noop()).is_pending());
3797	}
3798
3799	/// Mint a track whose groups register with a bounded [`cache::Pool`].
3800	fn pooled_producer(capacity: u64) -> (Producer, cache::Pool) {
3801		let pool = cache::Pool::new(capacity);
3802		let broadcast = broadcast::Info {
3803			origin: crate::origin::Info::default().with_pool(pool.clone()),
3804			..Default::default()
3805		};
3806		let producer = Producer::new(Arc::new(broadcast), "test", None);
3807		(producer, pool)
3808	}
3809
3810	fn finished_group(producer: &mut Producer, size: usize) -> u64 {
3811		let mut group = producer.append_group().unwrap();
3812		group
3813			.write_frame(Timestamp::ZERO, bytes::Bytes::from(vec![0u8; size]))
3814			.unwrap();
3815		group.finish().unwrap();
3816		group.sequence
3817	}
3818
3819	#[tokio::test]
3820	async fn pool_evicts_oldest_group() {
3821		tokio::time::pause();
3822
3823		// Fits two 1000-byte groups (plus per-group overhead) but not three.
3824		let (mut producer, pool) = pooled_producer(3000);
3825
3826		finished_group(&mut producer, 1000); // seq 0
3827		tokio::time::advance(Duration::from_millis(10)).await;
3828		finished_group(&mut producer, 1000); // seq 1
3829		tokio::time::advance(Duration::from_millis(10)).await;
3830		finished_group(&mut producer, 1000); // seq 2, pinned as latest
3831
3832		// The write to seq 2 pushed the pool over budget: seq 0 (stalest, unpinned)
3833		// was evicted and its bytes released.
3834		assert!(pool.used() <= 3000, "pool should be back under budget");
3835
3836		let consumer = producer.consume();
3837		assert!(consumer.peek_group(0).is_none(), "evicted group is a cache miss");
3838		assert!(consumer.peek_group(1).is_some());
3839		assert!(consumer.peek_group(2).is_some());
3840
3841		// A fresh subscriber skips the evicted group entirely.
3842		let mut subscriber = producer.subscribe(None);
3843		assert_eq!(subscriber.assert_group().sequence, 1);
3844		assert_eq!(subscriber.assert_group().sequence, 2);
3845	}
3846
3847	#[tokio::test]
3848	async fn pool_never_evicts_latest() {
3849		tokio::time::pause();
3850
3851		// Far too small for even one group: the latest is pinned and survives anyway.
3852		let (mut producer, pool) = pooled_producer(100);
3853		finished_group(&mut producer, 1000);
3854
3855		assert!(pool.used() > 100, "pinned latest may exceed the budget");
3856		let mut subscriber = producer.subscribe(None);
3857		let mut group = subscriber.assert_group();
3858		assert_eq!(group.read_frame().await.unwrap().unwrap().payload.len(), 1000);
3859	}
3860
3861	#[tokio::test]
3862	async fn pool_reads_bump_recency() {
3863		tokio::time::pause();
3864
3865		let (mut producer, pool) = pooled_producer(3000);
3866		let mut subscriber = producer.subscribe(None);
3867
3868		finished_group(&mut producer, 1000); // seq 0
3869		tokio::time::advance(Duration::from_millis(10)).await;
3870		finished_group(&mut producer, 1000); // seq 1
3871		tokio::time::advance(Duration::from_millis(10)).await;
3872
3873		// Read seq 0 so seq 1 becomes the least recently used.
3874		let mut group = subscriber.assert_group();
3875		assert_eq!(group.sequence, 0);
3876		group.read_frame().await.unwrap().unwrap();
3877		tokio::time::advance(Duration::from_millis(10)).await;
3878
3879		// Over budget: seq 1 (stale) is the victim, the just-read seq 0 survives.
3880		finished_group(&mut producer, 1000); // seq 2, pinned
3881
3882		let consumer = producer.consume();
3883		assert!(
3884			consumer.peek_group(0).is_some(),
3885			"recently read group survives: {:?}",
3886			pool.debug_entries()
3887		);
3888		assert!(
3889			consumer.peek_group(1).is_none(),
3890			"stale group is evicted: {:?}",
3891			pool.debug_entries()
3892		);
3893	}
3894
3895	#[tokio::test]
3896	async fn pool_eviction_aborts_readers() {
3897		tokio::time::pause();
3898
3899		let (mut producer, pool) = pooled_producer(3000);
3900		let mut subscriber = producer.subscribe(None);
3901
3902		finished_group(&mut producer, 1000); // seq 0
3903		let group0 = subscriber.assert_group();
3904
3905		tokio::time::advance(Duration::from_millis(10)).await;
3906		finished_group(&mut producer, 1000); // seq 1
3907		tokio::time::advance(Duration::from_millis(10)).await;
3908		finished_group(&mut producer, 1000); // seq 2 evicts seq 0
3909
3910		// A consumer holding the evicted group surfaces the eviction, not a hang
3911		// or a truncated clean end.
3912		let mut group0 = group0;
3913		let read = group0.read_frame().await;
3914		assert!(
3915			matches!(read, Err(Error::Evicted)),
3916			"expected Evicted, got {read:?}: {:?}",
3917			pool.debug_entries()
3918		);
3919	}
3920
3921	#[tokio::test]
3922	async fn pool_growth_on_old_group_charges() {
3923		tokio::time::pause();
3924
3925		let (mut producer, pool) = pooled_producer(3000);
3926
3927		// Seq 0 stays open (a straggler still being written).
3928		let mut group0 = producer.append_group().unwrap();
3929		tokio::time::advance(Duration::from_millis(10)).await;
3930		// Seq 1 becomes the pinned latest; seq 0 is now evictable.
3931		let _group1 = producer.append_group().unwrap();
3932		tokio::time::advance(Duration::from_millis(10)).await;
3933
3934		// A late frame on the old group still counts against the budget, and can
3935		// evict that very group once it blows past capacity.
3936		group0
3937			.write_frame(Timestamp::ZERO, bytes::Bytes::from(vec![0u8; 4000]))
3938			.unwrap();
3939		assert!(pool.used() <= 3000, "growth on an old group triggers eviction");
3940		assert!(matches!(group0.abort(Error::Cancel), Err(Error::Evicted)));
3941	}
3942
3943	#[tokio::test]
3944	async fn refetched_latest_group_is_repinned() {
3945		tokio::time::pause();
3946
3947		let (mut producer, pool) = pooled_producer(3000);
3948		let dynamic = producer.dynamic();
3949
3950		// Seq 0 stays open: the straggler used to apply memory pressure later.
3951		let mut straggler = producer.append_group().unwrap();
3952		straggler
3953			.write_frame(Timestamp::ZERO, bytes::Bytes::from(vec![0u8; 1000]))
3954			.unwrap();
3955		tokio::time::advance(Duration::from_millis(10)).await;
3956
3957		// The publisher aborts its own latest group; the slot stays at max_sequence.
3958		let latest = producer.append_group().unwrap(); // seq 1
3959		latest.abort(Error::Cancel).unwrap();
3960		tokio::time::advance(Duration::from_millis(10)).await;
3961
3962		// Re-fetch it: the replacement takes over max_sequence and must be
3963		// re-pinned, or memory pressure could evict the live edge.
3964		let consumer = producer.consume();
3965		let pending = consumer.fetch_group(1, None);
3966		let req = dynamic
3967			.requested_group()
3968			.now_or_never()
3969			.expect("should not block")
3970			.unwrap();
3971		let mut group = req.accept(None).unwrap();
3972		group
3973			.write_frame(Timestamp::ZERO, bytes::Bytes::from(vec![0u8; 1000]))
3974			.unwrap();
3975		group.finish().unwrap();
3976		pending.await.unwrap();
3977		tokio::time::advance(Duration::from_millis(10)).await;
3978
3979		// Blow the budget with the straggler; the refetched latest is pinned, so
3980		// the straggler itself is the only eligible victim.
3981		straggler
3982			.write_frame(Timestamp::ZERO, bytes::Bytes::from(vec![0u8; 4000]))
3983			.unwrap();
3984
3985		assert!(pool.used() <= 3000);
3986		let mut group = consumer.peek_group(1).expect("refetched latest must stay pinned");
3987		assert_eq!(group.read_frame().await.unwrap().unwrap().payload.len(), 1000);
3988	}
3989
3990	#[tokio::test]
3991	async fn pool_eviction_allows_refetch() {
3992		tokio::time::pause();
3993
3994		let (mut producer, _pool) = pooled_producer(3000);
3995		let dynamic = producer.dynamic();
3996
3997		finished_group(&mut producer, 1000); // seq 0
3998		tokio::time::advance(Duration::from_millis(10)).await;
3999		finished_group(&mut producer, 1000); // seq 1
4000		tokio::time::advance(Duration::from_millis(10)).await;
4001		finished_group(&mut producer, 1000); // seq 2 evicts seq 0
4002
4003		// The evicted group is a miss, so the fetch queues for the dynamic handler
4004		// (a relay would issue a wire FETCH upstream).
4005		let consumer = producer.consume();
4006		assert!(consumer.peek_group(0).is_none());
4007		let pending = consumer.fetch_group(0, None);
4008
4009		let req = dynamic
4010			.requested_group()
4011			.now_or_never()
4012			.expect("should not block")
4013			.unwrap();
4014		assert_eq!(req.sequence(), 0);
4015
4016		// Accept replaces the evicted slot in place (not Error::Duplicate).
4017		let mut group = req.accept(None).unwrap();
4018		group
4019			.write_frame(Timestamp::ZERO, bytes::Bytes::from_static(b"refetched"))
4020			.unwrap();
4021		group.finish().unwrap();
4022
4023		let mut group = pending.await.unwrap();
4024		assert_eq!(&group.read_frame().await.unwrap().unwrap().payload[..], b"refetched");
4025	}
4026
4027	#[tokio::test]
4028	async fn fetch_aborts_with_track() {
4029		let producer = track_producer("test", None);
4030		let dynamic = producer.dynamic();
4031		let consumer = producer.consume();
4032
4033		let pending = consumer.fetch_group(3, None);
4034		assert!(kio::Pollable::poll(&*pending, &kio::Waiter::noop()).is_pending());
4035
4036		producer.abort(Error::Cancel).unwrap();
4037		assert!(pending.await.is_err());
4038		drop(dynamic);
4039	}
4040}