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 static metadata; [Route] is the dynamic path the broadcast takes to
9//! reach an origin, including whether it is announced to subscribers.
10use crate::{stats, track};
11use std::{
12	collections::{HashMap, VecDeque},
13	sync::Arc,
14	task::{Poll, ready},
15};
16
17use crate::Error;
18
19use super::{OriginList, Requests, WeakCache};
20
21/// A collection of media tracks that can be published and subscribed to.
22///
23/// Create via [`Info::produce`] to obtain both [`Producer`] and [`Consumer`] pair.
24/// This is the broadcast's static identity, fixed for its lifetime; the path it
25/// takes to get here is the dynamic [`Route`], observed via [`Consumer::route`].
26#[derive(Clone, Debug, Default)]
27#[non_exhaustive]
28pub struct Info {
29	/// The origin this broadcast belongs to (its identity, and the cache pool its
30	/// tracks and groups inherit). A track reaches its pool by walking up this link,
31	/// so the pool has a single home on the origin rather than being copied per
32	/// broadcast. Defaults to an unknown origin with an unbounded pool (a standalone
33	/// broadcast with no relay origin).
34	pub origin: super::origin::Info,
35}
36
37impl Info {
38	/// Create a new broadcast with default metadata.
39	pub fn new() -> Self {
40		Self::default()
41	}
42
43	/// Consume this [Info] to create a producer that carries its metadata.
44	///
45	/// Keep the returned [`Producer`] alive for as long as the broadcast should stay
46	/// available, and end it with [`Producer::finish`]. See the note on [`Producer`].
47	pub fn produce(self) -> Producer {
48		Producer::new(self)
49	}
50}
51
52/// The path a broadcast takes to reach this origin, and how preferable it is.
53///
54/// Unlike [`Info`], the route is dynamic: it changes when the serving session fails
55/// over, the upstream topology shifts, or the publisher re-advertises itself.
56/// Publish a change with [`Producer::set_route`] and observe one with
57/// [`Consumer::route_changed`]; downstream sessions forward updates as a restart
58/// on the wire, so route churn never looks like a new broadcast.
59#[derive(Clone, Debug, Default, PartialEq, Eq)]
60#[non_exhaustive]
61pub struct Route {
62	/// The chain of origins the broadcast has traversed, oldest first. Each relay
63	/// appends its own [`crate::Origin`] when forwarding; used for loop detection
64	/// and as the selection tie-break.
65	pub hops: OriginList,
66
67	/// The cost of pulling the broadcast via this route, accumulated per link:
68	/// lower wins, with ties broken by hop length and then a deterministic hash.
69	///
70	/// The original publisher seeds it with its production cost (zero for a live
71	/// publish, something large for a standby that would have to start working,
72	/// like a cold transcoder), and each link adds its own configured price as
73	/// the announcement crosses it, so a route over a metered backbone ranks
74	/// worse than an equal-length one within a datacenter. The accumulation
75	/// restarts at zero at any node actively carrying the broadcast: those
76	/// upstream legs already exist and are not re-paid by one more subscriber,
77	/// so the sum is the cost of the transfers a subscription would newly cause.
78	///
79	/// Carried on the wire from lite-06; older peers always report zero, leaving
80	/// the hop-count tie-break as the effective metric exactly as before.
81	pub cost: u64,
82
83	/// The cost as the announcing peer advertised it, before this link's charge
84	/// was added to [`Self::cost`]. Local bookkeeping, never forwarded: zero on a
85	/// chain of two or more hops means the announcing relay is actively carrying
86	/// the broadcast, which is what the origin's handover gate keys on.
87	pub(crate) advertised: u64,
88
89	/// Whether the broadcast should be announced: advertised to consumers via
90	/// [`crate::origin::Consumer::announced`] while this is the best route. A
91	/// non-announced broadcast stays reachable by exact path for subscribes and
92	/// fetches (e.g. serving cached or on-demand content), so toggling this via
93	/// [`Producer::set_route`] announces or unannounces without touching the
94	/// broadcast itself. Defaults to `false`.
95	pub announce: bool,
96}
97
98impl Route {
99	/// An unannounced direct route: no hops, best cost.
100	///
101	/// The broadcast is reachable only by its exact path, so subscribers must already
102	/// know it exists. Use [`announced`](Self::announced) to advertise it instead.
103	pub fn new() -> Self {
104		Self::default()
105	}
106
107	/// An announced direct route: no hops, best cost.
108	///
109	/// The broadcast is advertised to subscribers via
110	/// [`crate::origin::Consumer::announced`] while this is the best route, on top of
111	/// staying reachable by exact path. Use [`new`](Self::new) to keep it unadvertised.
112	pub fn announced() -> Self {
113		Self {
114			announce: true,
115			..Self::default()
116		}
117	}
118
119	/// Append a hop to the chain, oldest first.
120	///
121	/// Fails with [`crate::TooManyOrigins`] once the chain is full, the same limit
122	/// the wire enforces.
123	pub fn with_hop(mut self, origin: super::Origin) -> Result<Self, super::TooManyOrigins> {
124		self.hops.push(origin)?;
125		Ok(self)
126	}
127
128	/// Replace the hop chain.
129	pub fn with_hops(mut self, hops: OriginList) -> Self {
130		self.hops = hops;
131		self
132	}
133
134	/// Set the cost: lower wins among routes serving the same broadcast.
135	pub fn with_cost(mut self, cost: u64) -> Self {
136		self.cost = cost;
137		self
138	}
139
140	/// Set whether the broadcast is announced via this route.
141	pub fn with_announce(mut self, announce: bool) -> Self {
142		self.announce = announce;
143		self
144	}
145}
146
147#[derive(Default)]
148struct BroadcastState {
149	// Weak references for deduplication. Doesn't prevent track auto-close.
150	// Keyed by the track's shared `Arc<str>` name (the same Arc the handle holds).
151	// The cache reclaims closed entries incrementally on insert so a long-lived
152	// broadcast churning distinct track names stays bounded by the live count.
153	tracks: WeakCache<Arc<str>, track::TrackWeak>,
154
155	// Pending requests keyed by track name, coalescing concurrent `track()` calls
156	// and waiting for a dynamic handler to accept or deny them. A request leaves
157	// here once handed out (the handler caches it in `tracks`, so lookups keep
158	// coalescing onto it there).
159	requests: Requests<Arc<str>, track::Request>,
160
161	// Route-fed mode (a relay/origin "front"): tracks are spliced logical tracks
162	// joined across per-session tracks. `None` for an ordinary broadcast.
163	spliced: Option<SplicedState>,
164
165	// The path the broadcast currently takes to reach us, bumping `route_epoch`
166	// on every change so consumers can watch for updates.
167	route: Route,
168	route_epoch: u64,
169
170	// Set by an explicit `Producer::finish()` or `Producer::abort()` so `Drop` can
171	// tell a deliberate shutdown apart from a producer dropped by accident.
172	closing: bool,
173
174	// Set only by `Producer::finish()`: the broadcast ended deliberately, as
175	// opposed to aborting or losing its producer. The origin reads this to decide
176	// whether a detached source may linger for a replacement.
177	finished: bool,
178
179	// The error passed to `Producer::abort()`, reported by `Consumer::closed`.
180	// `None` for a finish or a dropped producer (reported as `Error::Dropped`).
181	abort: Option<Error>,
182}
183
184/// The spliced (route-fed) half of a broadcast: logical tracks that outlive any
185/// single session, plus the queue of tracks awaiting a serving route.
186#[derive(Default)]
187struct SplicedState {
188	// Logical tracks by name, owned strongly: they live as long as the broadcast
189	// (the origin's front), not as long as any consumer.
190	tracks: HashMap<Arc<str>, super::resume::Producer>,
191
192	// Names awaiting assignment to a route, in request order.
193	pending: VecDeque<Arc<str>>,
194}
195
196impl BroadcastState {
197	/// Insert a track weak handle into the lookup, returning an error if a live
198	/// track already holds the name. A closed entry under the name is reclaimed.
199	fn insert_track(&mut self, weak: track::TrackWeak) -> Result<(), Error> {
200		match self.tracks.insert(weak.name().clone(), weak) {
201			Some(_) => Err(Error::Duplicate),
202			None => Ok(()),
203		}
204	}
205
206	/// Live demand: a subscribed spliced track (route-fed broadcast), or a
207	/// pending request / consumed track (ordinary broadcast). See [`Demand`].
208	fn is_used(&self) -> bool {
209		if let Some(spliced) = &self.spliced {
210			return spliced.tracks.values().any(|track| track.is_used());
211		}
212		!self.requests.is_empty() || self.tracks.iter().any(|track| track.is_used())
213	}
214
215	/// Park `waiter` on every per-track channel feeding [`Self::is_used`]: the
216	/// consumer counts live on those channels, and their flips don't write this
217	/// state, so a watcher registered here alone would miss the edge. `want`
218	/// picks the direction; each channel only arms while its side is unmet.
219	fn register_demand(&self, waiter: &kio::Waiter, want: bool) {
220		if let Some(spliced) = &self.spliced {
221			for track in spliced.tracks.values() {
222				match want {
223					true => track.poll_used(waiter),
224					false => track.poll_unused(waiter),
225				}
226			}
227			return;
228		}
229		for track in self.tracks.iter() {
230			match want {
231				true => track.poll_used(waiter),
232				false => track.poll_unused(waiter),
233			}
234		}
235	}
236}
237
238/// Manages tracks within a broadcast.
239///
240/// Create tracks up front with [Self::create_track], reserve a name to fill in
241/// later with [Self::reserve_track], or handle on-demand consumer requests via
242/// [Self::dynamic].
243///
244/// # Lifetime
245///
246/// **You must keep this producer alive for as long as the broadcast should stay
247/// available.** A broadcast lives as long as at least one [`Producer`] exists;
248/// children do *not* keep it alive (cloning a [`Consumer`] or holding a
249/// [`track::Producer`] does nothing for the broadcast's lifetime). When the last
250/// producer goes away every consumer observes [`Error::Dropped`].
251///
252/// End the broadcast with [`Self::finish`] rather than dropping it. Dropping is an
253/// easy footgun in garbage-collected bindings (Go, Python, ...), where the handle
254/// can be collected the moment it falls out of scope even while you are still
255/// publishing, tearing the stream down mid-broadcast. Dropping the last producer
256/// without [`Self::finish`] logs a warning.
257#[derive(Clone)]
258pub struct Producer {
259	// Held behind an Arc so each track born from this broadcast can inherit a shared
260	// handle (threaded down by [`Self::create_track`] / [`Self::reserve_track`]).
261	info: Arc<Info>,
262
263	// Broadcast liveness. Consumers watch this (read-only) for close; dropping every
264	// producer (this handle and every `Dynamic`) ends the broadcast.
265	alive: kio::Producer<()>,
266
267	// Track registry plus the dynamic request queue, mutated by producers and
268	// consumers alike under one lock.
269	state: kio::Shared<BroadcastState>,
270
271	// Ingress stats scope, set by a tagged `origin::Producer` at
272	// `create_broadcast`. Inherited by the tracks this producer creates. Empty
273	// (no-op) for an untagged broadcast.
274	stats: stats::Scope,
275}
276
277impl Producer {
278	/// Create a producer for the given broadcast metadata. Prefer [`Info::produce`].
279	pub fn new(info: Info) -> Self {
280		Self {
281			info: Arc::new(info),
282			alive: Default::default(),
283			state: Default::default(),
284			stats: stats::Scope::default(),
285		}
286	}
287
288	/// Attach an ingress stats scope, inherited by the tracks created on this
289	/// broadcast. Set by a tagged `origin::Producer` at `create_broadcast`.
290	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
291		self.stats = scope;
292		self
293	}
294
295	/// Create a route-fed (spliced) broadcast: consumer track lookups mint logical
296	/// tracks that are spliced across per-session tracks, queued for a route to
297	/// serve. Used by the origin for broadcasts reached over the network.
298	pub(crate) fn new_spliced(info: Info) -> Self {
299		Self {
300			info: Arc::new(info),
301			alive: Default::default(),
302			state: kio::Shared::new(BroadcastState {
303				spliced: Some(SplicedState::default()),
304				..Default::default()
305			}),
306			// The origin-owned spliced broadcast stays untagged: egress attribution is
307			// applied when a tagged `origin::Consumer` hands the consumer out.
308			stats: stats::Scope::default(),
309		}
310	}
311
312	/// The broadcast's static metadata, fixed when it was created.
313	pub fn info(&self) -> &Info {
314		&self.info
315	}
316
317	/// A watch-only handle to the broadcast's demand. See [`Demand`].
318	pub fn demand(&self) -> Demand {
319		Demand {
320			alive: self.alive.consume().weak(),
321			state: self.state.clone(),
322		}
323	}
324
325	/// Remove a track from the lookup.
326	pub fn remove_track(&mut self, name: &str) -> Result<(), Error> {
327		self.state.lock().tracks.remove(name).ok_or(Error::NotFound)?;
328		Ok(())
329	}
330
331	/// Produce a new track and insert it into the broadcast.
332	///
333	/// Pass a name and an optional [`track::Info`], so a bare name works:
334	/// `create_track("video", None)`.
335	pub fn create_track(
336		&mut self,
337		name: impl Into<Arc<str>>,
338		info: impl Into<Option<track::Info>>,
339	) -> Result<track::Producer, Error> {
340		let info = info.into().unwrap_or_default();
341		let track = track::Producer::new(self.info.clone(), name, info).with_stats(self.stats.clone());
342		self.state.lock().insert_track(track.weak())?;
343		Ok(track)
344	}
345
346	/// Reserve a track by name without finalizing its [`track::Info`].
347	///
348	/// Returns a [`track::Request`] already discoverable by consumers; call
349	/// [`track::Request::accept`] to set its info and start producing. Use this when
350	/// the producer can't pick the track's properties (e.g. timescale) until it has
351	/// inspected the media, the same shape as a consumer-driven
352	/// [`Dynamic::requested_track`].
353	pub fn reserve_track(&mut self, name: impl Into<Arc<str>>) -> Result<track::Request, Error> {
354		let request = track::Request::new(self.info.clone(), name).with_stats(self.stats.clone());
355		self.state.lock().insert_track(request.weak())?;
356		Ok(request)
357	}
358
359	/// Create a track with a unique name using the given suffix.
360	///
361	/// Generates names like `0{suffix}`, `1{suffix}`, etc. and picks the first
362	/// one not already used in this broadcast.
363	pub fn unique_track(
364		&mut self,
365		suffix: &str,
366		info: impl Into<Option<track::Info>>,
367	) -> Result<track::Producer, Error> {
368		let name = self.unique_name(suffix);
369		self.create_track(name, info)
370	}
371
372	/// Generate a unique track name from a suffix without creating the track.
373	///
374	/// Returns a fresh name like `0{suffix}`, `1{suffix}`, etc. Use this when
375	/// you need to set non-default Track properties (e.g. `with_timescale`,
376	/// `with_latency_max`) before handing the Track to [`Self::create_track`].
377	pub fn unique_name(&self, suffix: &str) -> String {
378		let state = self.state.read();
379		(0u16..)
380			.map(|i| format!("{i}{suffix}"))
381			.find(|name| !state.tracks.contains_key(name.as_str()))
382			.expect("u16 namespace exhausted; wow")
383	}
384
385	/// Create a dynamic producer that handles on-demand track requests from consumers.
386	pub fn dynamic(&self) -> Dynamic {
387		Dynamic::new(
388			self.info.clone(),
389			self.alive.clone(),
390			self.state.clone(),
391			self.stats.clone(),
392		)
393	}
394
395	/// Set the broadcast's [`Route`]: the hop chain and cost it advertises.
396	///
397	/// Call this when the path to the content changes (an upstream failover) or the
398	/// publisher's preference changes (e.g. a transcoder warming up lowers its
399	/// cost). Consumers observe the change via [`Consumer::route_changed`] and
400	/// sessions forward it downstream as a restart, never as a new broadcast.
401	/// Setting the current route again is a no-op.
402	pub fn set_route(&mut self, route: Route) -> Result<(), Error> {
403		let mut state = self.state.lock();
404		if state.route == route {
405			return Ok(());
406		}
407		state.route = route;
408		state.route_epoch += 1;
409		Ok(())
410	}
411
412	/// Poll for the next spliced track awaiting a serving route, returning its name
413	/// and logical producer. Route-fed broadcasts only.
414	pub(crate) fn poll_spliced_assigned(&self, waiter: &kio::Waiter) -> Poll<(Arc<str>, super::resume::Producer)> {
415		let mut state = ready!(self.state.poll(waiter, |state| {
416			match &state.spliced {
417				Some(spliced) if !spliced.pending.is_empty() => Poll::Ready(()),
418				_ => Poll::Pending,
419			}
420		}));
421
422		let spliced = state.spliced.as_mut().expect("predicate guaranteed spliced");
423		let name = spliced.pending.pop_front().expect("predicate guaranteed a request");
424		let producer = spliced.tracks.get(&name).expect("pending name without a track").clone();
425		Poll::Ready((name, producer))
426	}
427
428	/// Abort every spliced track, releasing their subscribers with `err`. Called
429	/// when the broadcast closes for good.
430	pub(crate) fn abort_spliced(&self, err: Error) {
431		let mut state = self.state.lock();
432		if let Some(spliced) = state.spliced.as_mut() {
433			spliced.pending.clear();
434			for producer in spliced.tracks.values_mut() {
435				let _ = producer.abort(err.clone());
436			}
437		}
438	}
439
440	/// Create a consumer that can subscribe to tracks in this broadcast.
441	pub fn consume(&self) -> Consumer {
442		Consumer {
443			info: self.info.clone(),
444			alive: self.alive.consume(),
445			state: self.state.clone(),
446			route_seen: None,
447			stats: stats::Scope::default(),
448		}
449	}
450
451	/// Cleanly finish the broadcast once you are done publishing.
452	///
453	/// Marks the broadcast as deliberately finished so consumers observe a normal
454	/// end. Prefer this over dropping the producer: an accidental drop (see the note
455	/// on [`Producer`]) logs a warning, whereas `finish()` is silent.
456	///
457	/// Ends the broadcast outright: consumers observe a normal end immediately and no
458	/// new tracks are served, whether or not other producer clones are still alive.
459	/// Existing tracks stay readable so consumers can drain what they already have.
460	///
461	/// Borrows rather than consumes, matching [`track::Producer::finish`]. Finishing
462	/// declares the end, so it must not depend on the caller also surrendering the
463	/// handle.
464	pub fn finish(&mut self) {
465		{
466			let mut state = self.state.lock();
467			state.closing = true;
468			state.finished = true;
469		}
470		// Ending the broadcast is what consumers wait on, so signal it here rather
471		// than leaving it to the last handle drop.
472		let _ = self.alive.close();
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). Unlike a finish, consumers observe `err` from
481	/// [`Consumer::closed`], and an origin treats the source as ungracefully lost,
482	/// so the path may linger for a replacement (see
483	/// [`origin::Info::linger`](crate::origin::Info::linger)).
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);
495		}
496		let _ = self.alive.close();
497		Ok(())
498	}
499
500	/// Return true if this is the same broadcast instance.
501	pub fn is_clone(&self, other: &Self) -> bool {
502		self.state.same_channel(&other.state)
503	}
504}
505
506impl Drop for Producer {
507	fn drop(&mut self) {
508		// Only the last producer ending the broadcast matters; a clone dropping
509		// leaves it live (`alive` is shared with every `Dynamic` too). Warn if that
510		// last exit wasn't an explicit finish(), since consumers will then see
511		// Error::Dropped (classically a GC-collected handle in a language binding
512		// that tears the stream down mid-publish).
513		if !self.alive.is_last() {
514			return;
515		}
516		if !self.state.read().closing {
517			tracing::warn!(
518				"broadcast::Producer dropped without finish(). Keep the producer alive while publishing, then call finish()."
519			);
520		}
521	}
522}
523
524#[cfg(test)]
525#[allow(missing_docs)] // test-only assertion helpers
526impl Producer {
527	pub fn assert_create_track(
528		&mut self,
529		name: impl Into<Arc<str>>,
530		info: impl Into<Option<track::Info>>,
531	) -> track::Producer {
532		self.create_track(name, info).expect("should not have errored")
533	}
534}
535
536/// A session-owned handle to a source broadcast created via
537/// [`crate::origin::Producer::create_broadcast`]: [`Self::finish`] ends it
538/// deliberately, while dropping the guard aborts it as [`Error::Dropped`] (a dead
539/// session), letting the origin linger the path for a reconnect. Shared by the
540/// lite and IETF subscribers so the drop-vs-finish contract lives in one place.
541pub(crate) struct SourceGuard {
542	// `Option` so `finish` can consume the producer while `Drop` aborts it.
543	producer: Option<Producer>,
544}
545
546impl SourceGuard {
547	pub fn new(producer: Producer) -> Self {
548		Self {
549			producer: Some(producer),
550		}
551	}
552
553	/// A clone of the guarded producer.
554	pub fn producer(&self) -> Producer {
555		self.producer.clone().expect("guard holds a producer until finished")
556	}
557
558	/// End the source deliberately: the origin detaches it immediately,
559	/// unannouncing the path if it was the last.
560	pub fn finish(mut self) {
561		if let Some(mut producer) = self.producer.take() {
562			producer.finish();
563		}
564	}
565
566	/// Update the source's advertised route in place.
567	pub fn set_route(&mut self, route: Route) {
568		if let Some(producer) = &mut self.producer {
569			let _ = producer.set_route(route);
570		}
571	}
572}
573
574impl Drop for SourceGuard {
575	fn drop(&mut self) {
576		if let Some(producer) = self.producer.take() {
577			let _ = producer.abort(Error::Dropped);
578		}
579	}
580}
581
582/// Handles on-demand track creation for a broadcast.
583///
584/// When a consumer requests a track that doesn't exist, the dynamic producer
585/// picks up the request via [`Self::requested_track`] and either
586/// [`track::Request::accept`]s it with a concrete [`track::Info`] or
587/// [`track::Request::reject`]s it. Dropped when no longer needed; pending requests
588/// are automatically aborted.
589pub struct Dynamic {
590	info: Arc<Info>,
591	// Keeps the broadcast alive while a handler exists (mirrors a producer).
592	alive: kio::Producer<()>,
593	state: kio::Shared<BroadcastState>,
594	// Ingress stats scope, applied to the tracks this handler serves. Empty (no-op)
595	// for an untagged broadcast.
596	stats: stats::Scope,
597}
598
599impl Clone for Dynamic {
600	fn clone(&self) -> Self {
601		// Mirror `new`: count each live handle. Without this, deriving Clone would
602		// let `Drop` decrement past `new`'s single increment and prematurely flip
603		// the handler count to zero, causing future `track` calls to return `NotFound`.
604		self.state.lock().requests.add_handler();
605
606		Self {
607			info: self.info.clone(),
608			alive: self.alive.clone(),
609			state: self.state.clone(),
610			stats: self.stats.clone(),
611		}
612	}
613}
614
615impl Dynamic {
616	fn new(info: Arc<Info>, alive: kio::Producer<()>, state: kio::Shared<BroadcastState>, stats: stats::Scope) -> Self {
617		state.lock().requests.add_handler();
618
619		Self {
620			info,
621			alive,
622			state,
623			stats,
624		}
625	}
626
627	/// The broadcast's static metadata, fixed when it was created.
628	pub fn info(&self) -> &Info {
629		&self.info
630	}
631
632	/// Poll for the next consumer-requested track, without blocking.
633	///
634	/// Returns [`Error::Closed`] once the broadcast was deliberately ended
635	/// ([`Producer::finish`] or aborted), so a serving loop knows to stop and
636	/// release its handle.
637	pub fn poll_requested_track(&mut self, waiter: &kio::Waiter) -> Poll<Result<track::Request, Error>> {
638		let mut state = ready!(self.state.poll(waiter, |state| {
639			if state.requests.has_queued() || state.closing {
640				Poll::Ready(())
641			} else {
642				Poll::Pending
643			}
644		}));
645
646		if state.closing && !state.requests.has_queued() {
647			return Poll::Ready(Err(Error::Closed));
648		}
649
650		let name = state.requests.pop().expect("predicate guaranteed a request");
651		let pending = state.requests.remove(&name).expect("popped key must be pending");
652		// Cache the served track so concurrent lookups coalesce onto it. If a live track already
653		// holds the name (a publish raced the request), `insert` keeps it rather than shadowing it.
654		let _ = state.tracks.insert(name, pending.weak());
655		// Attribute the served track to this broadcast's ingress scope (no-op untagged).
656		Poll::Ready(Ok(pending.with_stats(self.stats.clone())))
657	}
658
659	/// Block until a consumer requests a track, returning a [`track::Request`] to serve.
660	pub async fn requested_track(&mut self) -> Result<track::Request, Error> {
661		kio::wait(|waiter| self.poll_requested_track(waiter)).await
662	}
663
664	/// Create a consumer that can subscribe to tracks in this broadcast.
665	pub fn consume(&self) -> Consumer {
666		Consumer {
667			info: self.info.clone(),
668			alive: self.alive.consume(),
669			state: self.state.clone(),
670			route_seen: None,
671			stats: stats::Scope::default(),
672		}
673	}
674
675	/// Block until the broadcast is closed, by [`Producer::finish`],
676	/// [`Producer::abort`], or every producer dropping, returning the cause.
677	pub async fn closed(&self) -> Error {
678		kio::wait(|waiter| self.poll_closed(waiter)).await
679	}
680
681	/// Poll until the broadcast closes; ready with the cause: the error passed to
682	/// [`Producer::abort`], or [`Error::Dropped`] for a [`Producer::finish`] or a
683	/// dropped producer (check [`Consumer::is_finished`] to tell those apart).
684	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Error> {
685		ready!(self.alive.poll_closed(waiter));
686		Poll::Ready(self.state.read().abort.clone().unwrap_or(Error::Dropped))
687	}
688
689	/// Return true if this is the same broadcast instance.
690	pub fn is_clone(&self, other: &Self) -> bool {
691		self.state.same_channel(&other.state)
692	}
693}
694
695impl Drop for Dynamic {
696	fn drop(&mut self) {
697		// Decrement and reject under one lock, so a `track` call that saw a live
698		// handler through the same lock can't slip a request past the rejection.
699		let mut state = self.state.lock();
700		if state.requests.remove_handler() {
701			// No handlers left to fulfill pending requests; reject them so consumers
702			// don't block forever on tracks nobody will serve.
703			for request in state.requests.drain_queued() {
704				request.reject(Error::Dropped);
705			}
706		}
707	}
708}
709
710#[cfg(test)]
711use futures::FutureExt;
712
713#[cfg(test)]
714#[allow(missing_docs)] // test-only assertion helpers
715impl Dynamic {
716	pub fn assert_request(&mut self) -> track::Request {
717		self.requested_track()
718			.now_or_never()
719			.expect("should not have blocked")
720			.expect("should not have errored")
721	}
722
723	pub fn assert_no_request(&mut self) {
724		assert!(self.requested_track().now_or_never().is_none(), "should have blocked");
725	}
726}
727
728/// Subscribe to arbitrary broadcast/tracks.
729pub struct Consumer {
730	info: Arc<Info>,
731	// Broadcast liveness (read-only): watched for close.
732	alive: kio::Consumer<()>,
733	// Track registry plus request queue; `track()` reads the registry and enqueues requests.
734	state: kio::Shared<BroadcastState>,
735	// The route epoch last yielded by `route_changed`, so each consumer clone
736	// observes the current route first and every change after it exactly once.
737	route_seen: Option<u64>,
738	// Egress stats scope, set by a tagged `origin::Consumer` at the broadcast
739	// handoff. Inherited by the tracks subscribed through this handle. Empty (no-op)
740	// for an untagged broadcast.
741	stats: stats::Scope,
742}
743
744impl Clone for Consumer {
745	fn clone(&self) -> Self {
746		Self {
747			info: self.info.clone(),
748			alive: self.alive.clone(),
749			state: self.state.clone(),
750			// Reset the cursor so the clone observes the current route first,
751			// even if the original already drained `route_changed`.
752			route_seen: None,
753			stats: self.stats.clone(),
754		}
755	}
756}
757
758impl Consumer {
759	/// Attach an egress stats scope, inherited by the tracks subscribed through this
760	/// handle. Set by a tagged `origin::Consumer` at the broadcast handoff.
761	pub(crate) fn with_stats(mut self, scope: stats::Scope) -> Self {
762		self.stats = scope;
763		self
764	}
765
766	/// The broadcast's static metadata, fixed when it was created.
767	pub fn info(&self) -> &Info {
768		&self.info
769	}
770
771	/// The [`Route`] the broadcast currently takes to reach this origin.
772	pub fn route(&self) -> Route {
773		self.state.read().route.clone()
774	}
775
776	/// Poll for a route change. See [`Self::route_changed`].
777	pub fn poll_route_changed(&mut self, waiter: &kio::Waiter) -> Poll<Result<Route, Error>> {
778		let seen = self.route_seen;
779		if let Poll::Ready(state) = self.state.poll(waiter, |state| {
780			if seen != Some(state.route_epoch) {
781				Poll::Ready(())
782			} else {
783				Poll::Pending
784			}
785		}) {
786			self.route_seen = Some(state.route_epoch);
787			return Poll::Ready(Ok(state.route.clone()));
788		}
789		// No pending change: surface the broadcast's end instead of parking forever.
790		ready!(self.alive.poll_closed(waiter));
791		Poll::Ready(Err(Error::Dropped))
792	}
793
794	/// Wait for the broadcast's [`Route`] to change.
795	///
796	/// The first call returns the current route immediately; each later call blocks
797	/// until it changes again, so a loop observes the initial value followed by
798	/// every update. Returns [`Error::Dropped`] once every producer is gone.
799	pub async fn route_changed(&mut self) -> Result<Route, Error> {
800		kio::wait(|waiter| self.poll_route_changed(waiter)).await
801	}
802
803	/// Get a handle to a track on this broadcast.
804	pub fn track(&self, name: &str) -> Result<track::Consumer, Error> {
805		// Tag the resolved track with this broadcast's egress scope so its
806		// subscriptions, fetches, and groups are attributed to the same broadcast.
807		self.track_inner(name).map(|track| track.with_stats(self.stats.clone()))
808	}
809
810	fn track_inner(&self, name: &str) -> Result<track::Consumer, Error> {
811		// A closed broadcast (every producer and handler gone) serves nothing.
812		if self.is_closed() {
813			return Err(Error::Dropped);
814		}
815
816		let mut state = self.state.lock();
817
818		// A route-fed broadcast mints spliced logical tracks: they outlive any
819		// session, and a route is asked (via the pending queue) to start serving.
820		if let Some(spliced) = state.spliced.as_mut() {
821			if let Some(producer) = spliced.tracks.get(name) {
822				return Ok(track::Consumer::spliced(name.into(), producer.consume()));
823			}
824			let name: Arc<str> = name.into();
825			let producer = super::resume::Producer::new();
826			let consumer = producer.consume();
827			spliced.tracks.insert(name.clone(), producer);
828			spliced.pending.push_back(name.clone());
829			return Ok(track::Consumer::spliced(name, consumer));
830		}
831
832		// Reuse a live producer if one is already publishing the track. `get` drops a
833		// closed entry and returns `None`, so we fall through to a fresh request.
834		if let Some(weak) = state.tracks.get(name) {
835			return Ok(weak.consume());
836		}
837
838		if let Some(pending) = state.requests.join(name) {
839			// Coalesce onto a queued request for the same name.
840			return Ok(pending.consume());
841		}
842
843		// A deliberately-ended broadcast serves nothing new; existing tracks above
844		// stay readable so consumers can drain the cache.
845		if state.closing {
846			return Err(Error::NotFound);
847		}
848
849		// Allocate the name once and share the same Arc across the request, the
850		// requests map, and the FIFO order. The request inherits the broadcast's
851		// cache pool through its `Arc<Info>`, same as a producer-created track.
852		let name: Arc<str> = name.into();
853		let request = track::Request::new(self.info.clone(), name.clone());
854		let consumer = request.consume();
855
856		// With no handler alive to serve it, the request is dropped: `NotFound` beats
857		// handing back a consumer that would only resolve `Dropped`.
858		if state.requests.insert(name, request).is_err() {
859			return Err(Error::NotFound);
860		}
861
862		Ok(consumer)
863	}
864
865	/// A watch-only handle to the broadcast's demand. See [`Demand`].
866	pub(crate) fn demand(&self) -> Demand {
867		Demand {
868			alive: self.alive.weak(),
869			state: self.state.clone(),
870		}
871	}
872
873	/// Block until the broadcast is closed, by [`Producer::finish`],
874	/// [`Producer::abort`], or every producer dropping, and return the cause.
875	///
876	/// Returns the error passed to [`Producer::abort`], or [`Error::Dropped`] for a
877	/// [`Producer::finish`] or a dropped producer (check [`Self::is_finished`] to
878	/// tell those apart).
879	pub async fn closed(&self) -> Error {
880		self.alive.closed().await;
881		self.state.read().abort.clone().unwrap_or(Error::Dropped)
882	}
883
884	/// Returns true if every [`Producer`] has been dropped.
885	pub fn is_closed(&self) -> bool {
886		self.alive.is_closed()
887	}
888
889	/// Whether the broadcast is on its way out: deliberately ended (finish/abort
890	/// marked, even while handles remain) or already fully closed. The origin's
891	/// dispatcher treats a rejection from such a source as imminent detach rather
892	/// than a strike.
893	pub(crate) fn is_closing(&self) -> bool {
894		self.is_closed() || self.state.read().closing
895	}
896
897	/// Whether the broadcast ended via a deliberate [`Producer::finish`], as opposed
898	/// to aborting or losing its producer. `false` while the broadcast is still live;
899	/// an origin uses this to close a front immediately on a deliberate end instead
900	/// of lingering for a replacement.
901	pub fn is_finished(&self) -> bool {
902		self.state.read().finished
903	}
904
905	/// Register a [`kio::Waiter`] that fires when the broadcast closes.
906	///
907	/// Returns [`Poll::Ready`] if already closed, otherwise [`Poll::Pending`] after
908	/// arming the waiter. Useful for composing close-detection into a larger poll
909	/// without spawning a task per broadcast.
910	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<()> {
911		self.alive.poll_closed(waiter)
912	}
913
914	/// Check if this is the exact same instance of a broadcast.
915	pub fn is_clone(&self, other: &Self) -> bool {
916		self.state.same_channel(&other.state)
917	}
918
919	/// Create a weak reference that doesn't keep the broadcast alive.
920	///
921	/// Used to deduplicate dynamically-served broadcasts in the origin: a live weak yields
922	/// a shared clone, a closed one is discarded so the next request re-serves.
923	pub(crate) fn weak(&self) -> WeakConsumer {
924		WeakConsumer {
925			info: self.info.clone(),
926			alive: self.alive.weak(),
927			state: self.state.clone(),
928		}
929	}
930}
931
932/// A weak reference to a broadcast that doesn't prevent it from closing.
933///
934/// Mirrors [`track::TrackWeak`]: held by the origin's dynamic cache to share one
935/// dynamically-served broadcast across repeat requests without pinning it alive.
936/// Only the `alive` handle needs to be weak; a [`kio::Shared`] carries no liveness,
937/// so holding the state outright pins nothing.
938#[derive(Clone)]
939pub(crate) struct WeakConsumer {
940	info: Arc<Info>,
941	alive: kio::ConsumerWeak<()>,
942	state: kio::Shared<BroadcastState>,
943}
944
945impl WeakConsumer {
946	/// Upgrade to a full [`Consumer`] sharing the same broadcast state.
947	pub fn consume(&self) -> Consumer {
948		Consumer {
949			info: self.info.clone(),
950			alive: self.alive.consume(),
951			state: self.state.clone(),
952			route_seen: None,
953			stats: stats::Scope::default(),
954		}
955	}
956}
957
958impl super::WeakEntry for WeakConsumer {
959	fn is_closed(&self) -> bool {
960		self.alive.is_closed()
961	}
962
963	fn same_channel(&self, other: &Self) -> bool {
964		self.state.same_channel(&other.state)
965	}
966}
967
968/// A cloneable, watch-only handle to a broadcast's subscriber demand.
969///
970/// Obtained from [`Producer::demand`]; the broadcast-level sibling of
971/// [`track::Demand`](crate::track::Demand). Demand means live interest in the
972/// broadcast's content: a subscribed spliced track on a route-fed broadcast, or
973/// a pending track request / a consumed track on an ordinary one. A publisher
974/// uses it to run expensive work only while someone is watching, and routing
975/// uses it to advertise a warm copy at zero cost.
976///
977/// It's a weak handle: it neither keeps the broadcast alive nor counts as
978/// demand itself. Once every producer is gone, [`used`](Self::used) /
979/// [`unused`](Self::unused) return [`Error::Dropped`].
980#[derive(Clone)]
981pub struct Demand {
982	alive: kio::ConsumerWeak<()>,
983	state: kio::Shared<BroadcastState>,
984}
985
986impl Demand {
987	/// Whether the broadcast has live demand right now.
988	///
989	/// A point-in-time snapshot with no registration; use [`Self::used`] /
990	/// [`Self::unused`] (or their `poll_*` forms) to wait for the edge.
991	pub fn is_used(&self) -> bool {
992		self.state.read().is_used()
993	}
994
995	/// Block until the broadcast has demand. Resolves immediately if it already
996	/// does; returns [`Error::Dropped`] once every producer is gone.
997	pub async fn used(&self) -> Result<(), Error> {
998		kio::wait(|waiter| self.poll_used(waiter)).await
999	}
1000
1001	/// Block until the broadcast has no demand. Resolves immediately if it has
1002	/// none; returns [`Error::Dropped`] once every producer is gone.
1003	pub async fn unused(&self) -> Result<(), Error> {
1004		kio::wait(|waiter| self.poll_unused(waiter)).await
1005	}
1006
1007	/// Poll-based variant of [`Self::used`].
1008	pub fn poll_used(&self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1009		self.poll_demand(waiter, true)
1010	}
1011
1012	/// Poll-based variant of [`Self::unused`].
1013	pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<Result<(), Error>> {
1014		self.poll_demand(waiter, false)
1015	}
1016
1017	fn poll_demand(&self, waiter: &kio::Waiter, want: bool) -> Poll<Result<(), Error>> {
1018		// Closure is checked first, matching `track::Demand`: a dead broadcast
1019		// reports Dropped rather than pretending to answer.
1020		if self.alive.poll_closed(waiter).is_ready() {
1021			return Poll::Ready(Err(Error::Dropped));
1022		}
1023		let ready = self.state.poll(waiter, |state| {
1024			// The consumer counts live on the per-track channels, whose flips
1025			// don't write this state: park on those channels too so the edge
1026			// wakes us, then recompute here.
1027			state.register_demand(waiter, want);
1028			match state.is_used() == want {
1029				true => Poll::Ready(()),
1030				false => Poll::Pending,
1031			}
1032		});
1033		match ready {
1034			Poll::Ready(_) => Poll::Ready(Ok(())),
1035			Poll::Pending => Poll::Pending,
1036		}
1037	}
1038}
1039
1040#[cfg(test)]
1041#[allow(missing_docs)] // test-only assertion helpers
1042impl Consumer {
1043	pub fn assert_not_closed(&self) {
1044		assert!(self.closed().now_or_never().is_none(), "should not be closed");
1045	}
1046
1047	pub fn assert_closed(&self) {
1048		assert!(self.closed().now_or_never().is_some(), "should be closed");
1049	}
1050}
1051
1052#[cfg(test)]
1053mod test {
1054	use super::*;
1055
1056	/// Await with a timeout so a missed demand wake fails the test instead of
1057	/// hanging it (time is paused, so the timeout fires instantly when idle).
1058	async fn expect<T>(fut: impl Future<Output = T>) -> T {
1059		tokio::time::timeout(std::time::Duration::from_secs(1), fut)
1060			.await
1061			.expect("timed out waiting for a demand edge")
1062	}
1063
1064	/// Demand on an ordinary broadcast tracks subscriber interest, not
1065	/// production: a live track producer alone is unused, a consumed track is
1066	/// used, and both edges wake parked waiters.
1067	#[tokio::test]
1068	async fn demand_ordinary() {
1069		tokio::time::pause();
1070
1071		let mut producer = Info::new().produce();
1072		let consumer = producer.consume();
1073		let demand = producer.demand();
1074
1075		// No demand yet; `unused` resolves immediately.
1076		assert!(!demand.is_used());
1077		demand.unused().await.unwrap();
1078
1079		// Producing alone is not demand.
1080		let _track = producer.create_track("a", None).unwrap();
1081		assert!(!demand.is_used());
1082
1083		// A consumer appearing wakes a parked `used`.
1084		let (used, handle) = tokio::join!(expect(demand.used()), async { consumer.track("a").unwrap() });
1085		used.unwrap();
1086		assert!(demand.is_used());
1087
1088		// The last consumer dropping wakes a parked `unused`.
1089		let (unused, ()) = tokio::join!(expect(demand.unused()), async { drop(handle) });
1090		unused.unwrap();
1091		assert!(!demand.is_used());
1092
1093		// Every producer gone: both edges report the closure.
1094		producer.finish();
1095		assert!(matches!(demand.used().await, Err(Error::Dropped)));
1096		assert!(matches!(demand.unused().await, Err(Error::Dropped)));
1097	}
1098
1099	/// Demand on a spliced (route-fed) broadcast follows the logical tracks'
1100	/// consumers, which is what flips a relay's advertised cost.
1101	#[tokio::test]
1102	async fn demand_spliced() {
1103		tokio::time::pause();
1104
1105		let producer = Producer::new_spliced(Info::new());
1106		let consumer = producer.consume();
1107		let demand = producer.demand();
1108
1109		assert!(!demand.is_used());
1110		let track = consumer.track("video").unwrap();
1111		assert!(demand.is_used());
1112
1113		// Dropping the only consumer wakes a parked `unused`, even though the
1114		// logical track itself stays cached in the broadcast.
1115		let (unused, ()) = tokio::join!(expect(demand.unused()), async { drop(track) });
1116		unused.unwrap();
1117		assert!(!demand.is_used());
1118
1119		// A repeat consumer for the cached track counts again.
1120		let _track = consumer.track("video").unwrap();
1121		assert!(demand.is_used());
1122	}
1123
1124	/// Subscribe and assert the result hasn't resolved yet (it stays pending until
1125	/// a publisher accepts). Returns the pending subscription to resolve after accepting.
1126	macro_rules! subscribe_pending {
1127		($consumer:expr, $name:expr) => {{
1128			let pending = $consumer.track($name).unwrap().subscribe(None);
1129			assert!(
1130				pending.poll_ok(&kio::Waiter::noop()).is_pending(),
1131				"subscribe should stay pending until the request is accepted"
1132			);
1133			pending
1134		}};
1135	}
1136
1137	#[tokio::test]
1138	async fn insert() {
1139		let mut producer = Info::new().produce();
1140
1141		// Create the track before any consumer exists.
1142		let mut track1 = producer.assert_create_track("track1", None);
1143		track1.append_group().unwrap();
1144
1145		let consumer = producer.consume();
1146
1147		// The track already exists, so subscribe resolves immediately.
1148		let mut track1_sub = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1149		track1_sub.assert_group();
1150
1151		let mut track2 = producer.assert_create_track("track2", None);
1152
1153		let consumer2 = producer.consume();
1154		let mut track2_consumer = consumer2.track("track2").unwrap().subscribe(None).await.unwrap();
1155		track2_consumer.assert_no_group();
1156
1157		track2.append_group().unwrap();
1158
1159		track2_consumer.assert_group();
1160	}
1161
1162	#[tokio::test]
1163	async fn closed() {
1164		let mut producer = Info::new().produce();
1165		let dynamic = producer.dynamic();
1166
1167		let consumer = producer.consume();
1168		consumer.assert_not_closed();
1169
1170		// Create a new track and insert it into the broadcast (resolves immediately).
1171		let track1 = producer.assert_create_track("track1", None);
1172		let mut track1c = consumer.track("track1").unwrap().subscribe(None).await.unwrap();
1173
1174		// A track nobody publishes stays pending until accepted.
1175		let track2_fut = subscribe_pending!(consumer, "track2");
1176
1177		// Dropping the last dynamic handler rejects pending requests, but must NOT
1178		// cascade to externally-owned tracks.
1179		drop(dynamic);
1180
1181		// track2 was a pending dynamic request, so its subscribe surfaces the rejection.
1182		assert!(track2_fut.await.is_err());
1183
1184		// track1's producer is held outside the broadcast, so it survives.
1185		assert!(!track1.is_closed());
1186		track1c.assert_not_closed();
1187	}
1188
1189	/// `closed()` reports the cause: the abort error, or `Dropped` for a finish or
1190	/// a dropped producer, with `is_finished` telling the latter two apart.
1191	#[tokio::test]
1192	async fn closed_cause() {
1193		// Abort: the error comes through, and it isn't a finish.
1194		let producer = Info::new().produce();
1195		let consumer = producer.consume();
1196		producer.abort(Error::Timeout).unwrap();
1197		assert!(matches!(consumer.closed().await, Error::Timeout));
1198		assert!(!consumer.is_finished());
1199
1200		// Finish: a deliberate clean end.
1201		let mut producer = Info::new().produce();
1202		let consumer = producer.consume();
1203		producer.finish();
1204		assert!(matches!(consumer.closed().await, Error::Dropped));
1205		assert!(consumer.is_finished());
1206
1207		// Plain drop: neither aborted nor finished.
1208		let producer = Info::new().produce();
1209		let consumer = producer.consume();
1210		// Deliberate for the test: exercises the accidental-drop path (warns).
1211		drop(producer);
1212		assert!(matches!(consumer.closed().await, Error::Dropped));
1213		assert!(!consumer.is_finished());
1214	}
1215
1216	#[tokio::test]
1217	async fn requests() {
1218		let mut producer = Info::new().produce().dynamic();
1219
1220		let consumer = producer.consume();
1221		let consumer2 = consumer.clone();
1222
1223		// Two subscribers to the same name coalesce into one request.
1224		let track1_fut = subscribe_pending!(consumer, "track1");
1225		let track2_fut = subscribe_pending!(consumer2, "track1");
1226
1227		// There should be exactly one request to serve.
1228		let request = producer.assert_request();
1229		producer.assert_no_request();
1230		assert_eq!(request.name(), "track1");
1231
1232		// Accept it, which resolves both waiting subscribers.
1233		let mut track3 = request.accept(None);
1234		let mut track1 = track1_fut.await.unwrap();
1235		let mut track2 = track2_fut.await.unwrap();
1236
1237		track1.assert_not_closed();
1238		track1.assert_is_clone(&track2);
1239		track3.subscribe(None).assert_is_clone(&track1);
1240
1241		// Append a group and make sure they all get it.
1242		track3.append_group().unwrap();
1243		track1.assert_group();
1244		track2.assert_group();
1245
1246		// A pending request is cancelled when the dynamic producer is dropped.
1247		let track4_fut = subscribe_pending!(consumer, "track2");
1248		drop(producer);
1249		assert!(track4_fut.await.is_err());
1250
1251		// With no dynamic producer left, requesting the handle fails outright.
1252		let track5 = consumer2.track("track3");
1253		assert!(track5.is_err(), "should have errored");
1254	}
1255
1256	#[tokio::test]
1257	async fn stale_producer() {
1258		let mut broadcast = Info::new().produce().dynamic();
1259		let consumer = broadcast.consume();
1260
1261		// Subscribe to a track and serve it.
1262		let track1_fut = subscribe_pending!(consumer, "track1");
1263		let mut producer1 = broadcast.assert_request().accept(None);
1264		let mut track1 = track1_fut.await.unwrap();
1265
1266		// Close the producer (simulating publisher disconnect).
1267		producer1.append_group().unwrap();
1268		producer1.finish().unwrap();
1269		drop(producer1);
1270
1271		// The consumer should see the track as closed.
1272		track1.assert_closed();
1273
1274		// Subscribe again to the same track: should get a NEW producer, not the stale one.
1275		let track2_fut = subscribe_pending!(consumer, "track1");
1276		let mut producer2 = broadcast.assert_request().accept(None);
1277		let mut track2 = track2_fut.await.unwrap();
1278		track2.assert_not_closed();
1279		track2.assert_not_clone(&track1);
1280
1281		// The new consumer should receive the new group.
1282		producer2.append_group().unwrap();
1283		track2.assert_group();
1284	}
1285
1286	#[tokio::test(start_paused = true)]
1287	async fn requested_unused() {
1288		let mut broadcast = Info::new().produce().dynamic();
1289		let bc = broadcast.consume();
1290
1291		// Subscribe to a track that doesn't exist yet, then serve it.
1292		let c1_fut = subscribe_pending!(bc, "unknown_track");
1293		let producer1 = broadcast.assert_request().accept(None);
1294		let consumer1 = c1_fut.await.unwrap();
1295
1296		// The producer should NOT be unused yet because there's a consumer.
1297		assert!(
1298			producer1.unused().now_or_never().is_none(),
1299			"track producer should be used"
1300		);
1301
1302		// A second subscriber reuses the live producer (fast path / dedup).
1303		let consumer2 = bc.track("unknown_track").unwrap().subscribe(None).await.unwrap();
1304		consumer2.assert_is_clone(&consumer1);
1305
1306		drop(consumer1);
1307		assert!(
1308			producer1.unused().now_or_never().is_none(),
1309			"track producer should be used"
1310		);
1311
1312		drop(consumer2);
1313		assert!(
1314			producer1.unused().now_or_never().is_some(),
1315			"track producer should be unused after all consumers are dropped"
1316		);
1317
1318		// While the producer is still alive, re-subscribing to the same name reuses
1319		// it (no new request). This is what lets the relay linger upstream
1320		// subscriptions across transient consumer churn.
1321		let consumer3 = bc.track("unknown_track").unwrap().subscribe(None).await.unwrap();
1322		consumer3.assert_is_clone(&producer1.subscribe(None));
1323		broadcast.assert_no_request();
1324		drop(consumer3);
1325
1326		// Aborting the producer closes its lookup entry; the next subscribe sees the
1327		// stale weak, evicts it, and creates a fresh request.
1328		producer1.abort(Error::Cancel).unwrap();
1329
1330		let c4_fut = subscribe_pending!(bc, "unknown_track");
1331		let producer2 = broadcast.assert_request().accept(None);
1332		let consumer4 = c4_fut.await.unwrap();
1333		drop(consumer4);
1334		assert!(
1335			producer2.unused().now_or_never().is_some(),
1336			"new track producer should be unused after its consumer is dropped"
1337		);
1338	}
1339
1340	// Cloning a `Consumer` resets its route cursor: a clone that inherited the
1341	// original's `route_seen` would skip the initial-value delivery that
1342	// `route_changed` promises.
1343	#[tokio::test]
1344	async fn route_clone_observes_current_route() {
1345		let mut producer = Info::new().produce();
1346		let mut consumer = producer.consume();
1347
1348		// Drain the initial route, then a change.
1349		consumer.route_changed().await.unwrap();
1350		let route = Route::new().with_cost(7);
1351		producer.set_route(route.clone()).unwrap();
1352		assert_eq!(consumer.route_changed().await.unwrap(), route);
1353
1354		// The original is fully drained: no update pending.
1355		assert!(consumer.route_changed().now_or_never().is_none());
1356
1357		// A clone starts fresh, yielding the current route immediately.
1358		let mut clone = consumer.clone();
1359		let seen = clone
1360			.route_changed()
1361			.now_or_never()
1362			.expect("clone should observe the current route immediately")
1363			.unwrap();
1364		assert_eq!(seen, route);
1365	}
1366
1367	// Cloning a `Dynamic` and dropping the clone must not flip the handler
1368	// count to zero. The relay's lite subscriber clones the
1369	// dynamic per spawned subscribe; if Clone skipped the increment, the
1370	// first finished subscribe would tear down the broadcast and any
1371	// follow-up `track` would return `NotFound`.
1372	#[tokio::test]
1373	async fn dynamic_clone_keeps_alive() {
1374		let broadcast = Info::new().produce().dynamic();
1375		let consumer = broadcast.consume();
1376
1377		let clone = broadcast.clone();
1378		drop(clone);
1379
1380		// Original handle is still live, so the request registers (stays pending)
1381		// instead of failing with NotFound.
1382		let _fut = subscribe_pending!(consumer, "track1");
1383	}
1384}