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