Skip to main content

moq_net/model/
broadcast.rs

1//! A broadcast is a named collection of tracks, split into a [Producer] and [Consumer] handle.
2//!
3//! A [Producer] creates tracks on demand: a [Consumer] subscribes by name, and the
4//! producer either serves a track it already has or is handed a [`track::Request`] to
5//! fill. Both handles are refcounted clones of one broadcast, which closes on
6//! [`Producer::finish`] or when the last producer drops.
7//!
8//! [Info] is the broadcast's static metadata, fixed for its lifetime.
9use crate::{cache, stats, track};
10use std::{
11	collections::{HashMap, VecDeque},
12	sync::Arc,
13	task::{Poll, ready},
14};
15
16use crate::Error;
17use crate::origin::Route;
18
19use super::origin_impl::Announcer;
20use super::{Requests, WeakCache};
21
22/// A collection of media tracks that can be published and subscribed to.
23///
24/// Create via [`Info::produce`] to obtain both [`Producer`] and [`Consumer`] pair.
25/// This is the broadcast's static identity, fixed for its lifetime.
26#[derive(Clone, Debug)]
27#[non_exhaustive]
28pub struct Info {
29	/// The cache pool this broadcast's tracks and groups inherit.
30	pub pool: cache::Pool,
31
32	/// Ceiling on each track's media-timestamp retention window.
33	pub cache_duration: std::time::Duration,
34
35	/// The path this broadcast is named by, which relative references in a catalog it
36	/// serves (hang's `broadcast` field) resolve against.
37	///
38	/// [`origin::Producer::create_broadcast`](super::origin::Producer::create_broadcast) stamps
39	/// the path the broadcast was created at, relative to the origin root (including through a
40	/// scoped producer). Every [`Consumer`] an origin hands out is then re-stamped with the path
41	/// *that handle* was requested or announced at, relative to its cursor's root, since the
42	/// same broadcast can be reached under more than one name: a dynamic handler may serve a
43	/// standalone broadcast at any path, and a rooted cursor names a broadcast more tightly
44	/// than the origin does.
45	///
46	/// Empty (the default) for a standalone broadcast with no origin, which is then its own
47	/// root: any `..` reference escapes.
48	pub path: crate::PathOwned,
49}
50
51impl Default for Info {
52	fn default() -> Self {
53		Self {
54			pool: cache::Pool::new(cache::Config::default().with_expiry(cache::DEFAULT_EXPIRY)),
55			cache_duration: std::time::Duration::MAX,
56			path: crate::PathOwned::default(),
57		}
58	}
59}
60
61impl Info {
62	/// Create a new broadcast with default metadata.
63	pub fn new() -> Self {
64		Self::default()
65	}
66
67	/// Consume this [Info] to create a producer that carries its metadata.
68	///
69	/// Keep the returned [`Producer`] alive for as long as the broadcast should stay
70	/// available, and end it with [`Producer::finish`]. See the note on [`Producer`].
71	pub fn produce(self) -> Producer {
72		Producer::new(self)
73	}
74}
75
76#[derive(Default)]
77struct BroadcastState {
78	// Weak references for deduplication. Doesn't prevent track auto-close.
79	// Keyed by the track's shared `Arc<str>` name (the same Arc the handle holds).
80	// The cache reclaims closed entries incrementally on insert so a long-lived
81	// broadcast churning distinct track names stays bounded by the live count.
82	tracks: WeakCache<Arc<str>, track::TrackWeak>,
83
84	// Shared across suffixes and producer clones; cache eviction must not reset IDs.
85	unique: u64,
86
87	// Pending requests keyed by track name, coalescing concurrent `track()` calls
88	// and waiting for a dynamic handler to accept or deny them. A request leaves
89	// here once handed out (the handler caches it in `tracks`, so lookups keep
90	// coalescing onto it there).
91	requests: Requests<Arc<str>, track::Request>,
92
93	// Route-fed mode (a relay/origin "front"): tracks are spliced logical tracks
94	// joined across per-session tracks. `None` for an ordinary broadcast.
95	spliced: Option<SplicedState>,
96
97	// Set by an explicit `Producer::finish()` or `Producer::abort()` so `Drop` can
98	// tell a deliberate shutdown apart from a producer dropped by accident.
99	closing: bool,
100
101	// Set only by `Producer::finish()`: the broadcast ended deliberately, as
102	// opposed to aborting or losing its producer.
103	finished: bool,
104
105	// The error passed to `Producer::abort()`, reported by `Consumer::closed`.
106	// `None` for a finish or a dropped producer (reported as `Error::Dropped`).
107	abort: Option<Error>,
108}
109
110/// The spliced (route-fed) half of a broadcast: logical tracks that outlive any
111/// single session, plus the queue of tracks awaiting a serving route.
112#[derive(Default)]
113struct SplicedState {
114	// Logical tracks by name, owned strongly: they live as long as the broadcast
115	// (the origin's front), not as long as any consumer.
116	tracks: HashMap<Arc<str>, super::resume::Producer>,
117
118	// Names awaiting assignment to a route, in request order.
119	pending: VecDeque<Arc<str>>,
120}
121
122impl BroadcastState {
123	/// Insert a track weak handle into the lookup, returning an error if a live
124	/// track already holds the name. A closed entry under the name is reclaimed.
125	fn insert_track(&mut self, weak: track::TrackWeak) -> Result<(), Error> {
126		match self.tracks.insert(weak.name().clone(), weak) {
127			Some(_) => Err(Error::Duplicate),
128			None => Ok(()),
129		}
130	}
131
132	/// Resolve every name the broadcast never filled, so subscribers waiting on a
133	/// [`track::Info`] that can no longer arrive fail with `err` instead of parking.
134	///
135	/// Covers a reservation nobody accepted, a request handed to a [`Dynamic`] that
136	/// never answered it, and one still queued for a handler. A track that carries
137	/// its info has a publisher and is left alone: an end there is that publisher's
138	/// call, and its cache stays readable.
139	fn reject_unserved(&mut self, err: Error) {
140		for request in self.requests.drain_queued() {
141			request.reject(err.clone());
142		}
143		for track in self.tracks.iter() {
144			track.reject(err.clone());
145		}
146	}
147
148	/// Live demand: a subscribed spliced track (route-fed broadcast), or a
149	/// pending request / consumed track (ordinary broadcast). See [`Demand`].
150	fn is_used(&self) -> bool {
151		if let Some(spliced) = &self.spliced {
152			return spliced.tracks.values().any(|track| track.is_used());
153		}
154		!self.requests.is_empty() || self.tracks.iter().any(|track| track.is_used())
155	}
156
157	/// Park `waiter` on every per-track channel feeding [`Self::is_used`]: the
158	/// consumer counts live on those channels, and their flips don't write this
159	/// state, so a watcher registered here alone would miss the edge. `want`
160	/// picks the direction; each channel only arms while its side is unmet.
161	fn register_demand(&self, waiter: &kio::Waiter, want: bool) {
162		if let Some(spliced) = &self.spliced {
163			for track in spliced.tracks.values() {
164				let _ = match want {
165					true => track.poll_used(waiter),
166					false => track.poll_unused(waiter),
167				};
168			}
169			return;
170		}
171		for track in self.tracks.iter() {
172			match want {
173				true => track.poll_used(waiter),
174				false => track.poll_unused(waiter),
175			}
176		}
177	}
178}
179
180/// Manages tracks within a broadcast.
181///
182/// Create tracks up front with [Self::create_track], reserve a name to fill in
183/// later with [Self::reserve_track], or handle on-demand consumer requests via
184/// [Self::dynamic].
185///
186/// # Lifetime
187///
188/// **You must keep this producer alive for as long as the broadcast should stay
189/// available.** A broadcast lives as long as at least one [`Producer`] exists;
190/// children do *not* keep it alive (cloning a [`Consumer`] or holding a
191/// [`track::Producer`] does nothing for the broadcast's lifetime). When the last
192/// producer goes away every consumer observes [`Error::Dropped`].
193///
194/// End the broadcast with [`Self::finish`] rather than dropping it. Dropping is an
195/// easy footgun in garbage-collected bindings (Go, Python, ...), where the handle
196/// can be collected the moment it falls out of scope even while you are still
197/// publishing, tearing the stream down mid-broadcast. Dropping the last producer
198/// without [`Self::finish`] logs a warning.
199#[derive(Clone)]
200pub struct Producer {
201	// Held behind an Arc so each track born from this broadcast can inherit a shared
202	// handle (threaded down by [`Self::create_track`] / [`Self::reserve_track`]).
203	info: Arc<Info>,
204
205	// Broadcast liveness, shared with every `Dynamic`. Consumers watch it (read-only)
206	// for close; the guard ends the broadcast when the last of those handles drops.
207	alive: Arc<Alive>,
208
209	// Track registry plus the dynamic request queue, mutated by producers and
210	// consumers alike under one lock.
211	state: kio::Shared<BroadcastState>,
212
213	// Ingress stats scope, set by a tagged `origin::Producer` at
214	// `create_broadcast`. Inherited by the tracks this producer creates. Empty
215	// (no-op) for an untagged broadcast.
216	stats: stats::Scope,
217}
218
219impl Producer {
220	/// Create a producer for the given broadcast metadata. Prefer [`Info::produce`].
221	pub fn new(info: Info) -> Self {
222		let state = kio::Shared::<BroadcastState>::default();
223		Self {
224			info: Arc::new(info),
225			alive: Alive::new(state.clone()),
226			state,
227			stats: stats::Scope::default(),
228		}
229	}
230
231	/// Attach an ingress stats scope, inherited by the tracks created on this
232	/// broadcast. Set by a tagged `origin::Producer` at `create_broadcast`.
233	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
234		self.stats = scope;
235		self
236	}
237
238	/// Attach the advertisement of this broadcast's exact path. Set by
239	/// `origin::Producer::create_broadcast`; a standalone broadcast has none.
240	pub(crate) fn with_announcer(self, announcer: Announcer) -> Self {
241		*self.alive.announcer.lock() = Some(announcer);
242		self
243	}
244
245	/// Advertise this broadcast's exact path as a route, or re-price the standing
246	/// advertisement in place.
247	///
248	/// Call it once the tracks a subscriber needs first (a catalog) exist, so the
249	/// advertisement lands with them in place: peers act on it immediately.
250	/// The origin's local cursor already enumerates the path from creation.
251	/// The peer route retracts on [`unannounce`](Self::unannounce), [`finish`](Self::finish),
252	/// [`abort`](Self::abort), or the last producer dropping.
253	///
254	/// Fails with [`Error::Closed`] on a standalone broadcast (one not created
255	/// through an origin, so there is nothing to announce into) or once the
256	/// origin's driver has been dropped.
257	pub fn announce(&self, route: Route) -> Result<(), Error> {
258		let mut announcer = self.alive.announcer.lock();
259		let announcer = announcer.as_mut().ok_or(Error::Closed)?;
260		announcer.announce(route)
261	}
262
263	/// Retract this broadcast's peer advertisement, if any. Local consumers
264	/// still discover and request the path until the broadcast ends.
265	pub fn unannounce(&self) {
266		self.alive.unannounce();
267	}
268
269	/// Create a route-fed (spliced) broadcast: consumer track lookups mint logical
270	/// tracks that are spliced across per-session tracks, queued for a route to
271	/// serve. Used by the origin for broadcasts reached over the network.
272	pub(crate) fn new_spliced(info: Info) -> Self {
273		let state = kio::Shared::new(BroadcastState {
274			spliced: Some(SplicedState::default()),
275			..Default::default()
276		});
277		Self {
278			info: Arc::new(info),
279			alive: Alive::new(state.clone()),
280			state,
281			// The origin-owned spliced broadcast stays untagged: egress attribution is
282			// applied when a tagged `origin::Consumer` hands the consumer out.
283			stats: stats::Scope::default(),
284		}
285	}
286
287	/// The broadcast's static metadata, fixed when it was created.
288	pub fn info(&self) -> &Info {
289		&self.info
290	}
291
292	/// A watch-only handle to the broadcast's demand. See [`Demand`].
293	pub fn demand(&self) -> Demand {
294		Demand {
295			alive: self.alive.token.consume().weak(),
296			state: self.state.clone(),
297		}
298	}
299
300	/// Produce a new track and insert it into the broadcast.
301	///
302	/// Pass a name and an optional [`track::Info`], so a bare name works:
303	/// `create_track("video", None)`.
304	pub fn create_track(
305		&self,
306		name: impl Into<Arc<str>>,
307		info: impl Into<Option<track::Info>>,
308	) -> Result<track::Producer, Error> {
309		let name = name.into();
310		let info = info.into().unwrap_or_default();
311		let mut state = self.state.lock();
312
313		// A consumer may have requested this name before it existed (a live
314		// [`Dynamic`] queues such requests). Creating the track fulfills that
315		// request: its consumers resolve against this very producer. Without
316		// this they would be stranded, since the name is taken the moment the
317		// track exists, so no handler could ever serve their queue entry.
318		if let Some(request) = state.requests.take(name.as_ref()) {
319			let track = request.with_stats(self.stats.clone()).accept(info);
320			// Cache it like a served request so concurrent lookups coalesce; a
321			// live same-name entry cannot exist (its presence would have kept
322			// the request from queuing).
323			let _ = state.tracks.insert(name, track.weak());
324			return Ok(track);
325		}
326
327		let track = track::Producer::new(self.info.clone(), name, info).with_stats(self.stats.clone());
328		state.insert_track(track.weak())?;
329		Ok(track)
330	}
331
332	/// Reserve a track by name without finalizing its [`track::Info`].
333	///
334	/// Returns a [`track::Request`] already discoverable by consumers; call
335	/// [`track::Request::accept`] to set its info and start producing. Use this when
336	/// the producer can't pick the track's properties (e.g. timescale) until it has
337	/// inspected the media, the same shape as a consumer-driven
338	/// [`Dynamic::requested_track`].
339	///
340	/// Subscribers wait on the name until it is accepted, so a reservation the producer
341	/// ends up never filling has to be dropped or rejected. Ending the broadcast
342	/// ([`Self::finish`] or [`Self::abort`]) resolves whatever is left.
343	pub fn reserve_track(&self, name: impl Into<Arc<str>>) -> Result<track::Request, Error> {
344		let request = track::Request::new(self.info.clone(), name).with_stats(self.stats.clone());
345		self.state.lock().insert_track(request.weak())?;
346		Ok(request)
347	}
348
349	/// Create a track with a unique name using the given suffix.
350	///
351	/// Uses [`Self::unique_name`]; minted names are never reused, even after closure.
352	pub fn unique_track(&self, suffix: &str, info: impl Into<Option<track::Info>>) -> Result<track::Producer, Error> {
353		let name = self.unique_name(suffix);
354		self.create_track(name, info)
355	}
356
357	/// Generate a unique track name from a suffix without creating the track.
358	///
359	/// Returns `{id}{suffix}` with an increasing ID shared across all suffixes and
360	/// producer clones in this broadcast, skipping names already in the lookup.
361	/// A digit-leading suffix gets a `-` separator so it cannot be confused with the ID.
362	/// Minted names are never reused, even if no track is created or it is closed.
363	/// Explicit calls to [`Self::create_track`] can still reuse names.
364	///
365	/// # Panics
366	///
367	/// Panics if the broadcast exhausts its `u64` IDs.
368	pub fn unique_name(&self, suffix: &str) -> String {
369		let mut state = self.state.lock();
370		let separator = if suffix.starts_with(|c: char| c.is_ascii_digit()) {
371			"-"
372		} else {
373			""
374		};
375		loop {
376			let id = state.unique;
377			state.unique = id.checked_add(1).expect("unique track IDs exhausted");
378			let name = format!("{id}{separator}{suffix}");
379			if !state.tracks.contains_key(name.as_str()) {
380				return name;
381			}
382		}
383	}
384
385	/// Create a dynamic producer that handles on-demand track requests from consumers.
386	pub fn dynamic(&self) -> Dynamic {
387		Dynamic::new(
388			self.info.clone(),
389			self.alive.clone(),
390			self.state.clone(),
391			self.stats.clone(),
392		)
393	}
394
395	/// Poll for the next spliced track awaiting a serving route, returning its name
396	/// and logical producer. Route-fed broadcasts only.
397	pub(crate) fn poll_spliced_assigned(&self, waiter: &kio::Waiter) -> Poll<(Arc<str>, super::resume::Producer)> {
398		let mut state = ready!(self.state.poll(waiter, |state| {
399			match &state.spliced {
400				Some(spliced) if !spliced.pending.is_empty() => Poll::Ready(()),
401				_ => Poll::Pending,
402			}
403		}));
404
405		let spliced = state.spliced.as_mut().expect("predicate guaranteed spliced");
406		let name = spliced.pending.pop_front().expect("predicate guaranteed a request");
407		let producer = spliced.tracks.get(&name).expect("pending name without a track").clone();
408		Poll::Ready((name, producer))
409	}
410
411	/// Let go of every spliced track, aborting with `err` the ones never handed
412	/// out by [`Self::poll_spliced_assigned`]. Called when the broadcast ends:
413	/// whoever took the others decides how they end.
414	pub(crate) fn release_spliced(&self, err: Error) {
415		let mut state = self.state.lock();
416		if let Some(spliced) = state.spliced.as_mut() {
417			for name in std::mem::take(&mut spliced.pending) {
418				if let Some(producer) = spliced.tracks.get_mut(&name) {
419					let _ = producer.abort(err.clone());
420				}
421			}
422			spliced.tracks.clear();
423		}
424	}
425
426	/// Create a consumer that can subscribe to tracks in this broadcast.
427	pub fn consume(&self) -> Consumer {
428		Consumer {
429			info: self.info.clone(),
430			alive: self.alive.token.consume(),
431			state: self.state.clone(),
432			stats: stats::Scope::default(),
433		}
434	}
435
436	/// Cleanly finish the broadcast once you are done publishing.
437	///
438	/// Marks the broadcast as deliberately finished so consumers observe a normal
439	/// end. Prefer this over dropping the producer: an accidental drop (see the note
440	/// on [`Producer`]) logs a warning, whereas `finish()` is silent.
441	///
442	/// Ends the broadcast outright: consumers observe a normal end immediately and no
443	/// new tracks are served, whether or not other producer clones are still alive.
444	/// Existing tracks stay readable so consumers can drain what they already have.
445	///
446	/// A name that was reserved or requested but never served resolves with
447	/// [`Error::NotFound`]: nothing can fill it now, so its subscribers fail rather
448	/// than waiting on a [`track::Info`] that is never coming.
449	///
450	/// Borrows rather than consumes, matching [`track::Producer::finish`]. Finishing
451	/// declares the end, so it must not depend on the caller also surrendering the
452	/// handle.
453	pub fn finish(&self) {
454		{
455			let mut state = self.state.lock();
456			state.closing = true;
457			state.finished = true;
458			// A name that was reserved or requested but never served can't arrive now,
459			// and `Consumer::track` already answers `NotFound` for one asked about after
460			// this point. Say the same to whoever asked earlier.
461			state.reject_unserved(Error::NotFound);
462		}
463		// Ending the broadcast is what consumers wait on, so signal it here rather
464		// than leaving it to the last handle drop.
465		let _ = self.alive.token.close();
466		self.alive.retire();
467	}
468
469	/// Abort the broadcast, ending it for consumers with `err`.
470	///
471	/// Like [`finish`](Self::finish) the end is immediate, whether or not other
472	/// producer clones are still alive, and existing tracks stay readable so
473	/// consumers can drain what they already have (an abort does not cascade into
474	/// the tracks), while a name nothing ever served resolves with `err` the same
475	/// way [`finish`](Self::finish) resolves it. Unlike a finish, consumers observe
476	/// `err` from [`Consumer::closed`], so the end reads as a failure rather than a
477	/// deliberate one.
478	///
479	/// Consumes the producer: an abort is terminal. Errors if the broadcast was
480	/// already finished or aborted.
481	pub fn abort(self, err: Error) -> Result<(), Error> {
482		{
483			let mut state = self.state.lock();
484			if state.closing {
485				return Err(Error::Closed);
486			}
487			state.closing = true;
488			state.abort = Some(err.clone());
489			// Same as a finish: an unserved name is answerable now, with the reason the
490			// broadcast ended. Published tracks keep their cache (no cascade).
491			state.reject_unserved(err);
492		}
493		let _ = self.alive.token.close();
494		self.alive.retire();
495		Ok(())
496	}
497
498	/// Return true if this is the same broadcast instance.
499	pub fn is_clone(&self, other: &Self) -> bool {
500		self.state.same_channel(&other.state)
501	}
502}
503
504/// Ends the broadcast when the last [`Producer`] or [`Dynamic`] drops, closing the
505/// liveness channel every [`Consumer`] watches.
506///
507/// A refcount rather than a "am I the last one?" check inside `Drop`: that answer is
508/// a snapshot, and acting on it is exactly what invalidates it.
509struct Alive {
510	token: kio::Producer<()>,
511	state: kio::Shared<BroadcastState>,
512	// The advertisement of the broadcast's exact path, owned here so it retracts
513	// with the broadcast: on finish, abort, or the last producer-side handle
514	// dropping. `None` for a standalone broadcast.
515	announcer: kio::Lock<Option<Announcer>>,
516}
517
518impl Alive {
519	fn new(state: kio::Shared<BroadcastState>) -> Arc<Self> {
520		Arc::new(Self {
521			token: kio::Producer::default(),
522			state,
523			announcer: kio::Lock::new(None),
524		})
525	}
526
527	/// Withdraw peer advertising while leaving the path discoverable locally.
528	fn unannounce(&self) {
529		if let Some(announcer) = self.announcer.lock().as_mut() {
530			announcer.withdraw();
531		}
532	}
533
534	/// End the broadcast's advertising for good: retract the standing advertisement
535	/// and drop the announcer, so a later `announce` fails with `Closed`.
536	fn retire(&self) {
537		let announcer = self.announcer.lock().take();
538		// Dropped outside the announcer lock: the entry's removal re-syncs the
539		// origin's cursors under the origin's own lock.
540		drop(announcer);
541	}
542}
543
544impl Drop for Alive {
545	fn drop(&mut self) {
546		// Warn if the last exit wasn't an explicit finish(), since consumers will
547		// then see Error::Dropped (classically a GC-collected handle in a language
548		// binding that tears the stream down mid-publish).
549		if !self.state.read().closing {
550			tracing::warn!(
551				"broadcast::Producer dropped without finish(). Keep the producer alive while publishing, then call finish()."
552			);
553		}
554		self.retire();
555	}
556}
557
558#[cfg(test)]
559#[allow(missing_docs)] // test-only assertion helpers
560impl Producer {
561	pub fn assert_create_track(
562		&mut self,
563		name: impl Into<Arc<str>>,
564		info: impl Into<Option<track::Info>>,
565	) -> track::Producer {
566		self.create_track(name, info).expect("should not have errored")
567	}
568}
569
570/// A session-owned handle to a source broadcast created via
571/// [`crate::origin::Producer::create_broadcast`]: [`Self::finish`] ends it
572/// deliberately, while dropping the guard aborts it as [`Error::Dropped`] (a dead
573/// session), so consumers observe the loss as an error. Shared by the lite and
574/// IETF subscribers so the drop-vs-finish contract lives in one place.
575pub(crate) struct SourceGuard {
576	// `Option` so `finish` can consume the producer while `Drop` aborts it.
577	producer: Option<Producer>,
578}
579
580impl SourceGuard {
581	pub fn new(producer: Producer) -> Self {
582		Self {
583			producer: Some(producer),
584		}
585	}
586
587	/// End the source deliberately: the origin detaches it immediately,
588	/// unannouncing the path if it was the last.
589	pub fn finish(mut self) {
590		if let Some(producer) = self.producer.take() {
591			producer.finish();
592		}
593	}
594}
595
596impl Drop for SourceGuard {
597	fn drop(&mut self) {
598		if let Some(producer) = self.producer.take() {
599			let _ = producer.abort(Error::Dropped);
600		}
601	}
602}
603
604/// Handles on-demand track creation for a broadcast.
605///
606/// When a consumer requests a track that doesn't exist, the dynamic producer
607/// picks up the request via [`Self::requested_track`] and either
608/// [`track::Request::accept`]s it with a concrete [`track::Info`] or
609/// [`track::Request::reject`]s it. Dropped when no longer needed; pending requests
610/// are automatically aborted.
611pub struct Dynamic {
612	info: Arc<Info>,
613	// Keeps the broadcast alive while a handler exists (mirrors a producer).
614	alive: Arc<Alive>,
615	state: kio::Shared<BroadcastState>,
616	// Ingress stats scope, applied to the tracks this handler serves. Empty (no-op)
617	// for an untagged broadcast.
618	stats: stats::Scope,
619}
620
621impl Clone for Dynamic {
622	fn clone(&self) -> Self {
623		// Mirror `new`: count each live handle. Without this, deriving Clone would
624		// let `Drop` decrement past `new`'s single increment and prematurely flip
625		// the handler count to zero, causing future `track` calls to return `NotFound`.
626		self.state.lock().requests.add_handler();
627
628		Self {
629			info: self.info.clone(),
630			alive: self.alive.clone(),
631			state: self.state.clone(),
632			stats: self.stats.clone(),
633		}
634	}
635}
636
637impl Dynamic {
638	fn new(info: Arc<Info>, alive: Arc<Alive>, state: kio::Shared<BroadcastState>, stats: stats::Scope) -> Self {
639		state.lock().requests.add_handler();
640
641		Self {
642			info,
643			alive,
644			state,
645			stats,
646		}
647	}
648
649	/// The broadcast's static metadata, fixed when it was created.
650	pub fn info(&self) -> &Info {
651		&self.info
652	}
653
654	/// Poll for the next consumer-requested track, without blocking.
655	///
656	/// Returns [`Error::Closed`] once the broadcast was deliberately ended
657	/// ([`Producer::finish`] or aborted), so a serving loop knows to stop and
658	/// release its handle.
659	pub fn poll_requested_track(&mut self, waiter: &kio::Waiter) -> Poll<Result<track::Request, Error>> {
660		let mut state = ready!(self.state.poll(waiter, |state| {
661			if state.requests.has_queued() || state.closing {
662				Poll::Ready(())
663			} else {
664				Poll::Pending
665			}
666		}));
667
668		if state.closing && !state.requests.has_queued() {
669			return Poll::Ready(Err(Error::Closed));
670		}
671
672		let name = state.requests.pop().expect("predicate guaranteed a request");
673		let pending = state.requests.remove(&name).expect("popped key must be pending");
674		// Cache the served track so concurrent lookups coalesce onto it. If a live track already
675		// holds the name (a publish raced the request), `insert` keeps it rather than shadowing it.
676		let _ = state.tracks.insert(name, pending.weak());
677		// Attribute the served track to this broadcast's ingress scope (no-op untagged).
678		Poll::Ready(Ok(pending.with_stats(self.stats.clone())))
679	}
680
681	/// Block until a consumer requests a track, returning a [`track::Request`] to serve.
682	pub async fn requested_track(&mut self) -> Result<track::Request, Error> {
683		kio::wait(|waiter| self.poll_requested_track(waiter)).await
684	}
685
686	/// Create a consumer that can subscribe to tracks in this broadcast.
687	pub fn consume(&self) -> Consumer {
688		Consumer {
689			info: self.info.clone(),
690			alive: self.alive.token.consume(),
691			state: self.state.clone(),
692			stats: stats::Scope::default(),
693		}
694	}
695
696	/// Block until the broadcast is closed, by [`Producer::finish`],
697	/// [`Producer::abort`], or every producer dropping, returning the cause.
698	pub async fn closed(&self) -> Error {
699		kio::wait(|waiter| self.poll_closed(waiter)).await
700	}
701
702	/// Poll until the broadcast closes; ready with the cause: the error passed to
703	/// [`Producer::abort`], or [`Error::Dropped`] for a [`Producer::finish`] or a
704	/// dropped producer (check [`Consumer::is_finished`] to tell those apart).
705	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Error> {
706		ready!(self.alive.token.poll_closed(waiter));
707		Poll::Ready(self.state.read().abort.clone().unwrap_or(Error::Dropped))
708	}
709
710	/// Return true if this is the same broadcast instance.
711	pub fn is_clone(&self, other: &Self) -> bool {
712		self.state.same_channel(&other.state)
713	}
714}
715
716impl Drop for Dynamic {
717	fn drop(&mut self) {
718		// Decrement and reject under one lock, so a `track` call that saw a live
719		// handler through the same lock can't slip a request past the rejection.
720		let mut state = self.state.lock();
721		if state.requests.remove_handler() {
722			// No handlers left to fulfill pending requests; reject them so consumers
723			// don't block forever on tracks nobody will serve.
724			for request in state.requests.drain_queued() {
725				request.reject(Error::Dropped);
726			}
727		}
728	}
729}
730
731#[cfg(test)]
732use futures::FutureExt;
733
734#[cfg(test)]
735#[allow(missing_docs)] // test-only assertion helpers
736impl Dynamic {
737	pub fn assert_request(&mut self) -> track::Request {
738		self.requested_track()
739			.now_or_never()
740			.expect("should not have blocked")
741			.expect("should not have errored")
742	}
743
744	pub fn assert_no_request(&mut self) {
745		assert!(self.requested_track().now_or_never().is_none(), "should have blocked");
746	}
747}
748
749/// Subscribe to arbitrary broadcast/tracks.
750pub struct Consumer {
751	info: Arc<Info>,
752	// Broadcast liveness (read-only): watched for close.
753	alive: kio::Consumer<()>,
754	// Track registry plus request queue; `track()` reads the registry and enqueues requests.
755	state: kio::Shared<BroadcastState>,
756	// Egress stats scope, set by a tagged `origin::Consumer` at the broadcast
757	// handoff. Inherited by the tracks subscribed through this handle. Empty (no-op)
758	// for an untagged broadcast.
759	stats: stats::Scope,
760}
761
762impl Clone for Consumer {
763	fn clone(&self) -> Self {
764		Self {
765			info: self.info.clone(),
766			alive: self.alive.clone(),
767			state: self.state.clone(),
768			stats: self.stats.clone(),
769		}
770	}
771}
772
773impl Consumer {
774	/// Attach an egress stats scope, inherited by the tracks subscribed through this
775	/// handle. Set by a tagged `origin::Consumer` at the broadcast handoff.
776	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
777		self.stats = scope;
778		self
779	}
780
781	/// Stamp the path this handle was handed out at, overriding [`Info::path`].
782	///
783	/// The origin applies it to every broadcast it resolves, because the name belongs to
784	/// the (broadcast, cursor) pair rather than to the broadcast: what a catalog's relative
785	/// references resolve against is where the *reader* found the broadcast, not where its
786	/// producer happened to create it. Free when the two already agree, which is the case
787	/// for a broadcast created at the path an unrooted cursor asks for.
788	pub(crate) fn with_path(mut self, path: crate::PathOwned) -> Self {
789		if self.info.path != path {
790			let mut info = (*self.info).clone();
791			info.path = path;
792			self.info = Arc::new(info);
793		}
794		self
795	}
796
797	/// The broadcast's metadata, as reached through this handle.
798	pub fn info(&self) -> &Info {
799		&self.info
800	}
801
802	/// Get a handle to a track on this broadcast.
803	pub fn track(&self, name: &str) -> Result<track::Consumer, Error> {
804		// Rebind the track to *this* handle's view of the broadcast, so a catalog track
805		// resolves its relative references against the path we were handed out at rather
806		// than the one the producer was created at, and tag it with this broadcast's egress
807		// scope so its subscriptions, fetches, and groups are attributed to the same broadcast.
808		self.track_inner(name)
809			.map(|track| track.with_broadcast(self.info.clone()).with_stats(self.stats.clone()))
810	}
811
812	fn track_inner(&self, name: &str) -> Result<track::Consumer, Error> {
813		// A closed broadcast (every producer and handler gone) serves nothing.
814		if self.is_closed() {
815			return Err(Error::Dropped);
816		}
817
818		let mut state = self.state.lock();
819
820		// A route-fed broadcast mints spliced logical tracks: they outlive any
821		// session, and a route is asked (via the pending queue) to start serving.
822		let closing = state.closing;
823		if let Some(spliced) = state.spliced.as_mut() {
824			// An aborted logical track is a verdict from the sources attached at
825			// the time, not a property of the name: a publisher that had not yet
826			// created the track may have it now. Drop it so this request reaches a
827			// source again, exactly as the plain lookup below reclaims a closed
828			// entry. A *finished* one stays, since its cache is still readable.
829			//
830			// So a name, once finished, never comes back here: a publisher that
831			// finishes a track and publishes it again is serving new content, not
832			// resuming this one, and a subscriber has to re-read the catalog and
833			// re-initialize rather than be spliced onto it. Resuming the same
834			// content across routes is the transparent case, and that is what
835			// `resume::Producer` already does. Publish new content under a new
836			// name.
837			if spliced.tracks.get(name).is_some_and(|track| track.is_aborted()) {
838				spliced.tracks.remove(name);
839			}
840			if let Some(producer) = spliced.tracks.get(name) {
841				return Ok(track::Consumer::spliced(
842					name.into(),
843					self.info.clone(),
844					producer.consume(),
845				));
846			}
847			// A deliberately-ended broadcast serves nothing new; nothing drains the
848			// pending queue once the front is torn down.
849			if closing {
850				return Err(Error::NotFound);
851			}
852			let name: Arc<str> = name.into();
853			let producer = super::resume::Producer::new();
854			let consumer = producer.consume();
855			spliced.tracks.insert(name.clone(), producer);
856			spliced.pending.push_back(name.clone());
857			return Ok(track::Consumer::spliced(name, self.info.clone(), consumer));
858		}
859
860		// Reuse a live producer if one is already publishing the track. `get` drops a
861		// closed entry and returns `None`, so we fall through to a fresh request.
862		if let Some(weak) = state.tracks.get(name) {
863			match weak.try_consume() {
864				Some(consumer) => return Ok(consumer),
865				// It closed between the liveness probe and the count bump (an idle
866				// teardown committing under us). Reclaim it and request the track
867				// again, rather than handing back a consumer of a dead track.
868				None => {
869					state.tracks.remove(name);
870				}
871			}
872		}
873
874		if let Some(pending) = state.requests.join(name) {
875			// Coalesce onto a queued request for the same name.
876			return Ok(pending.consume());
877		}
878
879		// A deliberately-ended broadcast serves nothing new; existing tracks above
880		// stay readable so consumers can drain the cache.
881		if state.closing {
882			return Err(Error::NotFound);
883		}
884
885		// Allocate the name once and share the same Arc across the request, the
886		// requests map, and the FIFO order. The request inherits the broadcast's
887		// cache pool through its `Arc<Info>`, same as a producer-created track.
888		let name: Arc<str> = name.into();
889		let request = track::Request::new(self.info.clone(), name.clone());
890		let consumer = request.consume();
891
892		// With no handler alive to serve it, the request is dropped: `NotFound` beats
893		// handing back a consumer that would only resolve `Dropped`.
894		if state.requests.insert(name, request).is_err() {
895			return Err(Error::NotFound);
896		}
897
898		Ok(consumer)
899	}
900
901	/// A watch-only handle to the broadcast's demand. See [`Demand`].
902	///
903	/// The consumer-side sibling of [`Producer::demand`], for a holder that has
904	/// only a read handle. Holding this handle, or the [`Consumer`] it came from,
905	/// is not itself demand.
906	///
907	/// Demand going away is [`Demand::unused`] resolving. The broadcast going
908	/// away is [`Error::Dropped`], which here means the upstream producer ended.
909	pub fn demand(&self) -> Demand {
910		Demand {
911			alive: self.alive.weak(),
912			state: self.state.clone(),
913		}
914	}
915
916	/// Block until the broadcast is closed, by [`Producer::finish`],
917	/// [`Producer::abort`], or every producer dropping, and return the cause.
918	///
919	/// Returns the error passed to [`Producer::abort`], or [`Error::Dropped`] for a
920	/// [`Producer::finish`] or a dropped producer (check [`Self::is_finished`] to
921	/// tell those apart).
922	pub async fn closed(&self) -> Error {
923		self.alive.closed().await;
924		self.state.read().abort.clone().unwrap_or(Error::Dropped)
925	}
926
927	/// Returns true if every [`Producer`] has been dropped.
928	pub fn is_closed(&self) -> bool {
929		self.alive.is_closed()
930	}
931
932	/// Whether the broadcast is on its way out: deliberately ended (finish/abort
933	/// marked, even while handles remain) or already fully closed. The origin's
934	/// dispatcher treats a rejection from such a source as imminent detach rather
935	/// than a strike.
936	pub(crate) fn is_closing(&self) -> bool {
937		self.is_closed() || self.state.read().closing
938	}
939
940	/// Whether the broadcast ended via a deliberate [`Producer::finish`], as opposed
941	/// to aborting or losing its producer. `false` while the broadcast is still live.
942	pub fn is_finished(&self) -> bool {
943		self.state.read().finished
944	}
945
946	/// Register a [`kio::Waiter`] that fires when the broadcast closes.
947	///
948	/// Returns [`Poll::Ready`] if already closed, otherwise [`Poll::Pending`] after
949	/// arming the waiter. Useful for composing close-detection into a larger poll
950	/// without spawning a task per broadcast.
951	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<()> {
952		self.alive.poll_closed(waiter)
953	}
954
955	/// Check if this is the exact same instance of a broadcast.
956	pub fn is_clone(&self, other: &Self) -> bool {
957		self.state.same_channel(&other.state)
958	}
959
960	/// Create a weak reference that doesn't keep the broadcast alive.
961	///
962	/// Used to deduplicate dynamically-served broadcasts in the origin: a live weak yields
963	/// a shared clone, a closed one is discarded so the next request re-serves.
964	pub(crate) fn weak(&self) -> WeakConsumer {
965		WeakConsumer {
966			info: self.info.clone(),
967			alive: self.alive.weak(),
968			state: self.state.clone(),
969		}
970	}
971}
972
973/// A weak reference to a broadcast that doesn't prevent it from closing.
974///
975/// Mirrors [`track::TrackWeak`]: held by the origin's dynamic cache to share one
976/// dynamically-served broadcast across repeat requests without pinning it alive.
977/// Only the `alive` handle needs to be weak; a [`kio::Shared`] carries no liveness,
978/// so holding the state outright pins nothing.
979#[derive(Clone)]
980pub(crate) struct WeakConsumer {
981	info: Arc<Info>,
982	alive: kio::ConsumerWeak<()>,
983	state: kio::Shared<BroadcastState>,
984}
985
986impl WeakConsumer {
987	/// Upgrade to a full [`Consumer`] sharing the same broadcast state.
988	pub fn consume(&self) -> Consumer {
989		Consumer {
990			info: self.info.clone(),
991			alive: self.alive.consume(),
992			state: self.state.clone(),
993			stats: stats::Scope::default(),
994		}
995	}
996}
997
998impl super::WeakEntry for WeakConsumer {
999	fn is_closed(&self) -> bool {
1000		self.alive.is_closed()
1001	}
1002
1003	fn same_channel(&self, other: &Self) -> bool {
1004		self.state.same_channel(&other.state)
1005	}
1006}
1007
1008/// A cloneable, watch-only handle to a broadcast's subscriber demand.
1009///
1010/// Obtained from [`Producer::demand`] or [`Consumer::demand`]; the broadcast-level sibling of
1011/// [`track::Demand`](crate::track::Demand). Demand means live interest in the
1012/// broadcast's content: a subscribed spliced track on a route-fed broadcast, or
1013/// a pending track request / a consumed track on an ordinary one. A publisher
1014/// uses it to run expensive work only while someone is watching, and routing
1015/// uses it to advertise a warm copy at zero cost.
1016///
1017/// It's a weak handle: it neither keeps the broadcast alive nor counts as
1018/// demand itself. Once every producer is gone, [`used`](Self::used) /
1019/// [`unused`](Self::unused) return [`Error::Dropped`].
1020#[derive(Clone)]
1021pub struct Demand {
1022	alive: kio::ConsumerWeak<()>,
1023	state: kio::Shared<BroadcastState>,
1024}
1025
1026impl Demand {
1027	/// Whether the broadcast has live demand right now.
1028	///
1029	/// A point-in-time snapshot with no registration; use [`Self::used`] /
1030	/// [`Self::unused`] (or their `poll_*` forms) to wait for the edge.
1031	pub fn is_used(&self) -> bool {
1032		self.state.read().is_used()
1033	}
1034
1035	/// Block until the broadcast has demand. Resolves immediately if it already
1036	/// does; returns [`Error::Dropped`] once every producer is gone.
1037	pub async fn used(&self) -> Result<(), Error> {
1038		kio::wait(|waiter| self.poll_used(waiter)).await
1039	}
1040
1041	/// Block until the broadcast has no demand. Resolves immediately if it has
1042	/// none; returns [`Error::Dropped`] once every producer is gone.
1043	pub async fn unused(&self) -> Result<(), Error> {
1044		kio::wait(|waiter| self.poll_unused(waiter)).await
1045	}
1046
1047	/// Poll-based variant of [`Self::used`].
1048	pub fn poll_used(&self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1049		self.poll_demand(waiter, true)
1050	}
1051
1052	/// Poll-based variant of [`Self::unused`].
1053	pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1054		self.poll_demand(waiter, false)
1055	}
1056
1057	fn poll_demand(&self, waiter: &kio::Waiter, want: bool) -> Poll<Result<(), Error>> {
1058		// Closure is checked first, matching `track::Demand`: a dead broadcast
1059		// reports Dropped rather than pretending to answer.
1060		if self.alive.poll_closed(waiter).is_ready() {
1061			return Poll::Ready(Err(Error::Dropped));
1062		}
1063		let ready = self.state.poll(waiter, |state| {
1064			// The consumer counts live on the per-track channels, whose flips
1065			// don't write this state: park on those channels too so the edge
1066			// wakes us, then recompute here.
1067			state.register_demand(waiter, want);
1068			match state.is_used() == want {
1069				true => Poll::Ready(()),
1070				false => Poll::Pending,
1071			}
1072		});
1073		match ready {
1074			Poll::Ready(_) => Poll::Ready(Ok(())),
1075			Poll::Pending => Poll::Pending,
1076		}
1077	}
1078}
1079
1080#[cfg(test)]
1081#[allow(missing_docs)] // test-only assertion helpers
1082impl Consumer {
1083	pub fn assert_not_closed(&self) {
1084		assert!(self.closed().now_or_never().is_none(), "should not be closed");
1085	}
1086
1087	pub fn assert_closed(&self) {
1088		assert!(self.closed().now_or_never().is_some(), "should be closed");
1089	}
1090}
1091
1092#[cfg(test)]
1093mod test {
1094	use super::*;
1095	use std::time::Duration;
1096
1097	#[test]
1098	fn unique_names_are_never_reused() {
1099		let producer = Info::new().produce();
1100		let name = producer.unique_name(".opus");
1101		assert_eq!(name, "0.opus");
1102		let track = producer.create_track(name.clone(), None).unwrap();
1103		assert_eq!(producer.unique_name(".opus"), "1.opus");
1104		drop(track);
1105	}
1106
1107	#[test]
1108	fn unique_names_survive_closed_track_pruning() {
1109		let producer = Info::new().produce();
1110		let consumer = producer.consume();
1111		let track = producer.unique_track(".opus", None).unwrap();
1112		assert_eq!(track.name(), "0.opus");
1113		drop(track);
1114		assert!(matches!(consumer.track_inner("0.opus"), Err(Error::NotFound)));
1115		assert_eq!(producer.unique_name(".opus"), "1.opus");
1116	}
1117
1118	#[test]
1119	fn unique_name_skips_a_live_collision() {
1120		let producer = Info::new().produce();
1121		let track = producer.create_track("0.opus", None).unwrap();
1122		assert_eq!(producer.unique_name(".opus"), "1.opus");
1123		drop(track);
1124		assert_eq!(producer.unique_name(".opus"), "2.opus");
1125	}
1126
1127	#[test]
1128	fn unique_names_share_a_counter() {
1129		let producer = Info::new().produce();
1130		assert_eq!(producer.unique_name("-video"), "0-video");
1131		assert_eq!(producer.clone().unique_name("-audio"), "1-audio");
1132		assert_eq!(producer.unique_name("-video"), "2-video");
1133	}
1134
1135	#[test]
1136	fn unique_names_separate_numeric_suffixes() {
1137		let producer = Info::new().produce();
1138		assert_eq!(producer.unique_name(""), "0");
1139		let name = producer.unique_name("2");
1140		assert_eq!(name, "1-2");
1141		for _ in 2..12 {
1142			producer.unique_name("");
1143		}
1144		assert_eq!(producer.unique_name(""), "12");
1145	}
1146
1147	/// Await with a timeout so a missed demand wake fails the test instead of
1148	/// hanging it (time is paused, so the timeout fires instantly when idle).
1149	async fn expect<T>(fut: impl Future<Output = T>) -> T {
1150		tokio::time::timeout(Duration::from_secs(1), fut)
1151			.await
1152			.expect("timed out waiting for a demand edge")
1153	}
1154
1155	/// Demand on an ordinary broadcast tracks subscriber interest, not
1156	/// production: a live track producer alone is unused, a consumed track is
1157	/// used, and both edges wake parked waiters.
1158	#[tokio::test]
1159	async fn demand_ordinary() {
1160		tokio::time::pause();
1161
1162		let producer = Info::new().produce();
1163		let consumer = producer.consume();
1164		let demand = producer.demand();
1165
1166		// No demand yet; `unused` resolves immediately.
1167		assert!(!demand.is_used());
1168		demand.unused().await.unwrap();
1169
1170		// Producing alone is not demand.
1171		let _track = producer.create_track("a", None).unwrap();
1172		assert!(!demand.is_used());
1173
1174		// A consumer appearing wakes a parked `used`.
1175		let (used, handle) = tokio::join!(expect(demand.used()), async { consumer.track("a").unwrap() });
1176		used.unwrap();
1177		assert!(demand.is_used());
1178
1179		// The last consumer dropping wakes a parked `unused`.
1180		let (unused, ()) = tokio::join!(expect(demand.unused()), async { drop(handle) });
1181		unused.unwrap();
1182		assert!(!demand.is_used());
1183
1184		// Every producer gone: both edges report the closure.
1185		producer.finish();
1186		assert!(matches!(demand.used().await, Err(Error::Dropped)));
1187		assert!(matches!(demand.unused().await, Err(Error::Dropped)));
1188	}
1189
1190	/// Demand on a spliced (route-fed) broadcast follows the logical tracks'
1191	/// consumers, which is what flips a relay's advertised cost.
1192	#[tokio::test]
1193	async fn demand_spliced() {
1194		tokio::time::pause();
1195
1196		let producer = Producer::new_spliced(Info::new());
1197		let consumer = producer.consume();
1198		let demand = producer.demand();
1199		let watched = consumer.demand();
1200
1201		assert!(!demand.is_used());
1202		assert!(!watched.is_used());
1203		let track = consumer.track("video").unwrap();
1204		assert!(demand.is_used());
1205		assert!(watched.is_used());
1206
1207		// Dropping the only consumer wakes a parked `unused`, even though the
1208		// logical track itself stays cached in the broadcast.
1209		let (unused, ()) = tokio::join!(expect(watched.unused()), async { drop(track) });
1210		unused.unwrap();
1211		assert!(!demand.is_used());
1212		assert!(!watched.is_used());
1213
1214		// A repeat consumer for the cached track counts again.
1215		let _track = consumer.track("video").unwrap();
1216		assert!(demand.is_used());
1217	}
1218
1219	/// A consumer demand handle distinguishes lost demand from a dropped producer.
1220	#[tokio::test]
1221	async fn consumer_demand_reports_dropped_producer() {
1222		let producer = Producer::new_spliced(Info::new());
1223		let consumer = producer.consume();
1224		let watched = consumer.demand();
1225
1226		let track = consumer.track("video").unwrap();
1227		assert!(watched.is_used());
1228
1229		let (unused, ()) = tokio::join!(expect(watched.unused()), async { drop(track) });
1230		unused.unwrap();
1231
1232		drop(producer);
1233		assert!(matches!(watched.used().await, Err(Error::Dropped)));
1234		assert!(matches!(watched.unused().await, Err(Error::Dropped)));
1235	}
1236
1237	/// Subscribe and assert the result hasn't resolved yet (it stays pending until
1238	/// a publisher accepts). Returns the pending subscription to resolve after accepting.
1239	macro_rules! subscribe_pending {
1240		($consumer:expr, $name:expr) => {{
1241			let pending = $consumer.track($name).unwrap().subscribe(None);
1242			assert!(
1243				pending.poll_ok(&kio::Waiter::noop()).is_pending(),
1244				"subscribe should stay pending until the request is accepted"
1245			);
1246			pending
1247		}};
1248	}
1249
1250	#[tokio::test]
1251	async fn insert() {
1252		let mut producer = Info::new().produce();
1253
1254		// Create the track before any consumer exists.
1255		let track1 = producer.assert_create_track("track1", None);
1256		track1.append_group().unwrap();
1257
1258		let consumer = producer.consume();
1259
1260		// The track already exists, so subscribe resolves immediately.
1261		let mut track1_sub = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1262		track1_sub.assert_group();
1263
1264		let track2 = producer.assert_create_track("track2", None);
1265
1266		let consumer2 = producer.consume();
1267		let mut track2_consumer = consumer2.track("track2").unwrap().subscribe(None).await.unwrap();
1268		track2_consumer.assert_no_group();
1269
1270		track2.append_group().unwrap();
1271
1272		track2_consumer.assert_group();
1273	}
1274
1275	#[tokio::test]
1276	async fn closed() {
1277		let mut producer = Info::new().produce();
1278		let dynamic = producer.dynamic();
1279
1280		let consumer = producer.consume();
1281		consumer.assert_not_closed();
1282
1283		// Create a new track and insert it into the broadcast (resolves immediately).
1284		let track1 = producer.assert_create_track("track1", None);
1285		let mut track1c = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1286
1287		// A track nobody publishes stays pending until accepted.
1288		let track2_fut = subscribe_pending!(consumer, "track2");
1289
1290		// Dropping the last dynamic handler rejects pending requests, but must NOT
1291		// cascade to externally-owned tracks.
1292		drop(dynamic);
1293
1294		// track2 was a pending dynamic request, so its subscribe surfaces the rejection.
1295		assert!(track2_fut.await.is_err());
1296
1297		// track1's producer is held outside the broadcast, so it survives.
1298		assert!(!track1.is_closed());
1299		track1c.assert_not_closed();
1300	}
1301
1302	/// `closed()` reports the cause: the abort error, or `Dropped` for a finish or
1303	/// a dropped producer, with `is_finished` telling the latter two apart.
1304	#[tokio::test]
1305	async fn closed_cause() {
1306		// Abort: the error comes through, and it isn't a finish.
1307		let producer = Info::new().produce();
1308		let consumer = producer.consume();
1309		producer.abort(Error::Timeout).unwrap();
1310		assert!(matches!(consumer.closed().await, Error::Timeout));
1311		assert!(!consumer.is_finished());
1312
1313		// Finish: a deliberate clean end.
1314		let producer = Info::new().produce();
1315		let consumer = producer.consume();
1316		producer.finish();
1317		assert!(matches!(consumer.closed().await, Error::Dropped));
1318		assert!(consumer.is_finished());
1319
1320		// Plain drop: neither aborted nor finished.
1321		let producer = Info::new().produce();
1322		let consumer = producer.consume();
1323		// Deliberate for the test: exercises the accidental-drop path (warns).
1324		drop(producer);
1325		assert!(matches!(consumer.closed().await, Error::Dropped));
1326		assert!(!consumer.is_finished());
1327	}
1328
1329	#[tokio::test]
1330	async fn requests() {
1331		let mut producer = Info::new().produce().dynamic();
1332
1333		let consumer = producer.consume();
1334		let consumer2 = consumer.clone();
1335
1336		// Two subscribers to the same name coalesce into one request.
1337		let track1_fut = subscribe_pending!(consumer, "track1");
1338		let track2_fut = subscribe_pending!(consumer2, "track1");
1339
1340		// There should be exactly one request to serve.
1341		let request = producer.assert_request();
1342		producer.assert_no_request();
1343		assert_eq!(request.name(), "track1");
1344
1345		// Accept it, which resolves both waiting subscribers.
1346		let track3 = request.accept(None);
1347		let mut track1 = track1_fut.await.unwrap();
1348		let mut track2 = track2_fut.await.unwrap();
1349
1350		track1.assert_not_closed();
1351		track1.assert_is_clone(&track2);
1352		track3.subscribe(None).assert_is_clone(&track1);
1353
1354		// Append a group and make sure they all get it.
1355		track3.append_group().unwrap();
1356		track1.assert_group();
1357		track2.assert_group();
1358
1359		// A pending request is cancelled when the dynamic producer is dropped.
1360		let track4_fut = subscribe_pending!(consumer, "track2");
1361		drop(producer);
1362		assert!(track4_fut.await.is_err());
1363
1364		// With no dynamic producer left, requesting the handle fails outright.
1365		let track5 = consumer2.track("track3");
1366		assert!(track5.is_err(), "should have errored");
1367	}
1368
1369	#[tokio::test]
1370	async fn stale_producer() {
1371		let mut broadcast = Info::new().produce().dynamic();
1372		let consumer = broadcast.consume();
1373
1374		// Subscribe to a track and serve it.
1375		let track1_fut = subscribe_pending!(consumer, "track1");
1376		let producer1 = broadcast.assert_request().accept(None);
1377		let mut track1 = track1_fut.await.unwrap();
1378
1379		// Close the producer (simulating publisher disconnect).
1380		producer1.append_group().unwrap();
1381		producer1.finish().unwrap();
1382		drop(producer1);
1383
1384		// The consumer should see the track as closed.
1385		track1.assert_closed();
1386
1387		// Subscribe again to the same track: should get a NEW producer, not the stale one.
1388		let track2_fut = subscribe_pending!(consumer, "track1");
1389		let producer2 = broadcast.assert_request().accept(None);
1390		let mut track2 = track2_fut.await.unwrap();
1391		track2.assert_not_closed();
1392		track2.assert_not_clone(&track1);
1393
1394		// The new consumer should receive the new group.
1395		producer2.append_group().unwrap();
1396		track2.assert_group();
1397	}
1398
1399	#[tokio::test(start_paused = true)]
1400	async fn requested_unused() {
1401		let mut broadcast = Info::new().produce().dynamic();
1402		let bc = broadcast.consume();
1403
1404		// Subscribe to a track that doesn't exist yet, then serve it.
1405		let c1_fut = subscribe_pending!(bc, "unknown_track");
1406		let producer1 = broadcast.assert_request().accept(None);
1407		let consumer1 = c1_fut.await.unwrap();
1408
1409		// The producer should NOT be unused yet because there's a consumer.
1410		assert!(
1411			producer1.unused().now_or_never().is_none(),
1412			"track producer should be used"
1413		);
1414
1415		// A second subscriber reuses the live producer (fast path / dedup).
1416		let consumer2 = bc.track("unknown_track").unwrap().subscribe(None).await.unwrap();
1417		consumer2.assert_is_clone(&consumer1);
1418
1419		drop(consumer1);
1420		assert!(
1421			producer1.unused().now_or_never().is_none(),
1422			"track producer should be used"
1423		);
1424
1425		drop(consumer2);
1426		assert!(
1427			producer1.unused().now_or_never().is_some(),
1428			"track producer should be unused after all consumers are dropped"
1429		);
1430
1431		// While the producer is still alive, re-subscribing to the same name reuses
1432		// it (no new request). This is what lets the relay linger upstream
1433		// subscriptions across transient consumer churn.
1434		let consumer3 = bc.track("unknown_track").unwrap().subscribe(None).await.unwrap();
1435		consumer3.assert_is_clone(&producer1.subscribe(None));
1436		broadcast.assert_no_request();
1437		drop(consumer3);
1438
1439		// Aborting the producer closes its lookup entry; the next subscribe sees the
1440		// stale weak, evicts it, and creates a fresh request.
1441		producer1.abort(Error::Cancel).unwrap();
1442
1443		let c4_fut = subscribe_pending!(bc, "unknown_track");
1444		let producer2 = broadcast.assert_request().accept(None);
1445		let consumer4 = c4_fut.await.unwrap();
1446		drop(consumer4);
1447		assert!(
1448			producer2.unused().now_or_never().is_some(),
1449			"new track producer should be unused after its consumer is dropped"
1450		);
1451	}
1452
1453	/// Creating a track a consumer already requested fulfills that request: the
1454	/// waiting subscriber resolves against the created producer, and no handler
1455	/// ever sees the (now-taken) name. Without this the requester is stranded:
1456	/// the name exists the moment the track does, so the queue entry could
1457	/// never be served under it.
1458	#[tokio::test]
1459	async fn create_track_fulfills_queued_request() {
1460		let producer = Info::new().produce();
1461		let mut dynamic = producer.dynamic();
1462		let bc = dynamic.consume();
1463
1464		// Queue a request for a track that doesn't exist yet.
1465		let subscribing = subscribe_pending!(bc, "video");
1466
1467		// The producer creates the track before any handler drains the queue.
1468		let track = producer.create_track("video", None).unwrap();
1469		let mut sub = subscribing.await.expect("fulfilled by create_track");
1470
1471		// The fulfilled subscription is live against this very producer.
1472		track.append_group().unwrap();
1473		sub.recv_group().await.expect("recv").expect("group");
1474
1475		// The handler never sees the request; a fresh subscribe reuses the track.
1476		dynamic.assert_no_request();
1477		let again = bc.track("video").unwrap().subscribe(None).await.unwrap();
1478		again.assert_is_clone(&track.subscribe(None));
1479	}
1480
1481	// Cloning a `Dynamic` and dropping the clone must not flip the handler
1482	// count to zero. The relay's lite subscriber clones the
1483	// dynamic per spawned subscribe; if Clone skipped the increment, the
1484	// first finished subscribe would tear down the broadcast and any
1485	// follow-up `track` would return `NotFound`.
1486	#[tokio::test]
1487	async fn dynamic_clone_keeps_alive() {
1488		let broadcast = Info::new().produce().dynamic();
1489		let consumer = broadcast.consume();
1490
1491		let clone = broadcast.clone();
1492		drop(clone);
1493
1494		// Original handle is still live, so the request registers (stays pending)
1495		// instead of failing with NotFound.
1496		let _fut = subscribe_pending!(consumer, "track1");
1497	}
1498
1499	/// A reserved name nobody accepts is the parking case a publisher has to be able to
1500	/// end. Ending the broadcast is where it does: `Consumer::track` already answers
1501	/// `NotFound` for a name asked about after this point, so whoever asked earlier gets
1502	/// the same answer instead of waiting on info that can never arrive.
1503	#[tokio::test]
1504	async fn finish_resolves_a_reserved_name() {
1505		let producer = Info::new().produce();
1506		let consumer = producer.consume();
1507
1508		let _request = producer.reserve_track("track1").unwrap();
1509		let pending = subscribe_pending!(consumer, "track1");
1510
1511		producer.finish();
1512		assert!(matches!(pending.await, Err(Error::NotFound)));
1513	}
1514
1515	/// An abort says why the broadcast ended, and an unserved name resolves with that
1516	/// reason rather than a generic failure.
1517	#[tokio::test]
1518	async fn abort_resolves_a_reserved_name_with_its_reason() {
1519		let producer = Info::new().produce();
1520		let consumer = producer.consume();
1521
1522		let request = producer.reserve_track("track1").unwrap();
1523		let pending = subscribe_pending!(consumer, "track1");
1524
1525		producer.abort(Error::Cancel).unwrap();
1526		assert!(matches!(pending.await, Err(Error::Cancel)));
1527
1528		let track = request.accept(None);
1529		let mut subscriber = track.subscribe(None);
1530		assert!(matches!(subscriber.recv_group().await, Err(Error::Cancel)));
1531	}
1532
1533	/// A request still queued for a handler is the same parking case reached from the
1534	/// consumer side, so it ends the same way.
1535	#[tokio::test]
1536	async fn finish_resolves_a_queued_request() {
1537		let producer = Info::new().produce();
1538		let dynamic = producer.dynamic();
1539		let consumer = dynamic.consume();
1540
1541		let pending = subscribe_pending!(consumer, "track1");
1542
1543		producer.finish();
1544		assert!(matches!(pending.await, Err(Error::NotFound)));
1545		drop(dynamic);
1546	}
1547
1548	/// A request a handler already took parks the same way if the handler never answers
1549	/// it, so the sweep has to reach that one too.
1550	#[tokio::test]
1551	async fn finish_resolves_a_request_a_handler_never_answered() {
1552		let producer = Info::new().produce();
1553		let mut dynamic = producer.dynamic();
1554		let consumer = dynamic.consume();
1555
1556		let pending = subscribe_pending!(consumer, "track1");
1557		let _request = dynamic.requested_track().await.unwrap();
1558
1559		producer.finish();
1560		assert!(matches!(pending.await, Err(Error::NotFound)));
1561		drop(dynamic);
1562	}
1563
1564	/// A reverse fetch can install the track metadata before the live request is
1565	/// accepted, but it does not create a live publisher. Finishing the broadcast
1566	/// must still reject that name so an arrival-order subscriber does not park on
1567	/// backfill that is deliberately absent from its queue.
1568	#[tokio::test]
1569	async fn finish_resolves_an_unaccepted_track_with_fetched_info() {
1570		let producer = Info::new().produce();
1571		let consumer = producer.consume();
1572
1573		let request = producer.reserve_track("track1").unwrap();
1574		let dynamic = request.dynamic();
1575		let track = consumer.track("track1").unwrap();
1576		let pending_fetch = track.fetch_group(0, None);
1577		let fetch = dynamic.requested_group().await.unwrap();
1578		let group = fetch.accept(None).unwrap();
1579		group.finish().unwrap();
1580		pending_fetch.await.unwrap();
1581
1582		let mut subscriber = track.subscribe(None).await.unwrap();
1583		producer.finish();
1584		assert!(matches!(subscriber.recv_group().await, Err(Error::NotFound)));
1585
1586		let stale = request.accept(None);
1587		assert!(stale.append_group().is_err());
1588	}
1589
1590	/// Ending the broadcast doesn't cascade into a track someone is publishing: it keeps
1591	/// its cache and its publisher decides when it ends.
1592	#[tokio::test]
1593	async fn finish_spares_a_served_track() {
1594		let producer = Info::new().produce();
1595		let consumer = producer.consume();
1596
1597		let track = producer.create_track("track1", None).unwrap();
1598		let mut subscriber = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1599
1600		producer.finish();
1601
1602		track.append_group().unwrap();
1603		subscriber.assert_group();
1604		track.finish().unwrap();
1605	}
1606
1607	/// The publisher may still be holding the `track::Request` for a name the broadcast
1608	/// just gave up on. Accepting it afterwards must not resurrect the track, or a
1609	/// subscriber that was told `NotFound` could be contradicted by a later one.
1610	#[tokio::test]
1611	async fn finish_leaves_a_stale_reservation_inert() {
1612		let producer = Info::new().produce();
1613		let consumer = producer.consume();
1614
1615		let request = producer.reserve_track("track1").unwrap();
1616		let pending = subscribe_pending!(consumer, "track1");
1617
1618		producer.finish();
1619		assert!(matches!(pending.await, Err(Error::NotFound)));
1620
1621		let track = request.accept(None);
1622		assert!(track.append_group().is_err());
1623		let mut subscriber = track.subscribe(None);
1624		assert!(matches!(subscriber.recv_group().await, Err(Error::NotFound)));
1625		assert!(consumer.track("track1").is_err());
1626	}
1627
1628	/// Dropping a `track::Request` is not a verdict about the name, so it resolves as
1629	/// `Dropped` (a handler lost to a crashed publisher or a dead transport), never as
1630	/// `NotFound`. Only an explicit rejection may claim the track is absent.
1631	#[tokio::test]
1632	async fn dropping_a_reserved_request_resolves_dropped() {
1633		let producer = Info::new().produce();
1634		let consumer = producer.consume();
1635
1636		let request = producer.reserve_track("track1").unwrap();
1637		let pending = subscribe_pending!(consumer, "track1");
1638
1639		drop(request);
1640		assert!(matches!(pending.await, Err(Error::Dropped)));
1641		producer.finish();
1642	}
1643
1644	/// `track::Request::reject` carries its reason the same way, which is what lets a
1645	/// subscriber tell "no such track" from "the publisher went away".
1646	#[tokio::test]
1647	async fn rejecting_a_reserved_request_carries_the_reason() {
1648		let producer = Info::new().produce();
1649		let consumer = producer.consume();
1650
1651		let request = producer.reserve_track("track1").unwrap();
1652		let pending = subscribe_pending!(consumer, "track1");
1653
1654		request.reject(Error::NotFound);
1655		assert!(matches!(pending.await, Err(Error::NotFound)));
1656		producer.finish();
1657	}
1658
1659	/// The interleave every unused-driven teardown has to survive: a wire subscriber
1660	/// observes zero consumers, and a viewer looks the track up again before the
1661	/// teardown commits. The returning viewer keeps the track alive, and once it
1662	/// really does commit the cached handle is reclaimed rather than handed out
1663	/// cancelled.
1664	#[tokio::test]
1665	async fn an_idle_teardown_yields_to_a_returning_viewer() {
1666		let producer = Info::new().produce();
1667		let consumer = producer.consume();
1668		let track = producer.create_track("video", None).unwrap();
1669
1670		// The unused wake a teardown acts on.
1671		assert!(track.poll_unused(&kio::Waiter::noop()).is_ready());
1672
1673		// Demand returns in the gap before it commits.
1674		let viewer = consumer.track("video").unwrap();
1675		let track = track
1676			.abort_unused(Error::Cancel)
1677			.expect_err("viewer keeps the track alive");
1678
1679		// So the viewer holds a live track, not a cancelled one.
1680		assert!(!track.is_closed());
1681		let mut subscriber = viewer.subscribe(None).await.unwrap();
1682		subscriber.assert_no_group();
1683		track.append_group().unwrap();
1684		assert!(subscriber.recv_group().await.unwrap().is_some());
1685
1686		// Once the viewer really leaves, the same teardown commits, and the lookup
1687		// re-requests the track instead of resolving the closed one.
1688		drop(subscriber);
1689		drop(viewer);
1690		assert!(track.abort_unused(Error::Cancel).is_ok());
1691		assert!(matches!(consumer.track("video"), Err(Error::NotFound)));
1692
1693		producer.finish();
1694	}
1695
1696	#[test]
1697	fn abort_unused_accepts_an_already_closed_track_with_consumers() {
1698		let producer = Info::new().produce();
1699		let consumer = producer.consume();
1700		let track = producer.create_track("video", None).unwrap();
1701		let _viewer = consumer.track("video").unwrap();
1702		assert!(track.is_used());
1703		track.clone().abort(Error::Cancel).unwrap();
1704		assert!(!track.is_used());
1705		assert!(track.abort_unused(Error::Cancel).is_ok());
1706		producer.finish();
1707	}
1708}