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