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