Skip to main content

moq_net/model/
origin.rs

1use crate::{broadcast, cache, stats, track};
2use kio::Pollable;
3use std::{
4	cmp::Reverse,
5	collections::{BTreeMap, HashMap, HashSet},
6	fmt,
7	sync::Arc,
8	sync::atomic::{AtomicU64, Ordering},
9	task::{Poll, ready},
10	time::Duration,
11};
12
13use rand::RngExt;
14use web_async::Lock;
15
16use super::{Requests, WeakCache};
17use crate::{
18	AsPath, Error, Path, PathOwned, PathPrefixes,
19	coding::{BoundsExceeded, Decode, DecodeError, Encode, EncodeError},
20};
21
22/// A relay origin, identified by a 62-bit varint on the wire.
23///
24/// Local origins are built with [`Origin::new`] or [`Origin::random`], both of
25/// which guarantee a non-zero id so loop detection can work. Remote peers may
26/// still send `0`; it is legal on the wire but cannot be used for loop detection.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct Origin {
29	/// 62-bit identifier. Encoded as a QUIC varint on the wire.
30	id: u64,
31}
32
33/// Returned when a local origin id is zero or outside the 62-bit wire range.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[non_exhaustive]
36pub struct InvalidOrigin;
37
38impl fmt::Display for InvalidOrigin {
39	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40		write!(f, "local origin id must be non-zero and below 2^62")
41	}
42}
43
44impl std::error::Error for InvalidOrigin {}
45
46impl Origin {
47	/// Placeholder for hop entries whose actual id is not on the wire (Lite03).
48	/// Also used for remote peers that choose the legal but loop-blind id 0.
49	pub(crate) const UNKNOWN: Self = Self { id: 0 };
50
51	/// Build an origin from a stable id.
52	///
53	/// The id must be non-zero and fit in the 62-bit QUIC varint range. Wire
54	/// decode accepts remote id 0, but local origins should not use it because
55	/// downstream peers cannot exclude it for loop detection.
56	pub fn new(id: u64) -> Result<Self, InvalidOrigin> {
57		if id == 0 || id >= 1u64 << 62 {
58			return Err(InvalidOrigin);
59		}
60		Ok(Self { id })
61	}
62
63	/// Generate a fresh origin with a random non-zero id. Use this for any
64	/// origin that does not need a stable identity across restarts.
65	///
66	/// TEMPORARY: the wire format allows 62 bits, but older `@moq/lite` JS
67	/// clients decode `AnnounceInterest.exclude_hop` as a u53 (number) and
68	/// throw on anything > 2^53-1. To keep those clients alive against
69	/// fresh relays, we cap the random id at 53 bits. Restore to 62 bits
70	/// once the JS u62 fix has propagated to deployed bundles.
71	pub fn random() -> Self {
72		let mut rng = rand::rng();
73		let id = rng.random_range(1..(1u64 << 53));
74		Self { id }
75	}
76
77	/// Return the origin's wire id.
78	pub fn id(self) -> u64 {
79		self.id
80	}
81
82	/// Consume this [Origin] to create a producer that carries its id, with an
83	/// unbounded cache pool. Use [`Info::produce`] to configure the pool.
84	pub fn produce(self) -> Producer {
85		Info::new(self).produce()
86	}
87}
88
89/// An origin's identity plus the cache pool its broadcasts inherit.
90///
91/// Doubles as the construction config for an [origin `Producer`](Producer) and as the
92/// parent handle every broadcast carries ([`broadcast::Info::origin`]): the origin owns
93/// the [`cache::Pool`] every group in the tree charges into, so a relay configures one
94/// bounded pool here and every broadcast, track, and group beneath it reaches that single
95/// budget by walking up the ownership chain. Defaults to an unbounded pool
96/// ([`Origin::produce`] is the shorthand for that). Cheap to clone (a `Copy` id plus an
97/// `Arc`-handle bump), so it's stored by value rather than behind another `Arc`.
98#[derive(Clone, Debug)]
99#[non_exhaustive]
100pub struct Info {
101	/// The origin's wire identity, appended to broadcast hop chains for loop
102	/// detection and shortest-path routing.
103	pub id: Origin,
104
105	/// The cache pool broadcasts under this origin charge their groups into. It flows
106	/// down the ownership chain (origin -> broadcast -> track -> group): a track opens
107	/// an account against it, and its groups charge through that. Unbounded by
108	/// default; a relay sets a bounded one (via [`Self::with_pool`]) so cached groups
109	/// across the whole process share one memory budget.
110	pub pool: cache::Pool,
111
112	/// Ceiling on how long any non-latest group under this origin is retained. Each
113	/// track's own [`latency_max`](track::Info::latency_max) window is clamped down to
114	/// this when the track binds, so a group is never held longer than this regardless
115	/// of what a publisher advertises. The age budget alongside [`Self::pool`]'s byte
116	/// budget: a relay bounds memory by both. [`Duration::MAX`] (the default) imposes no
117	/// ceiling, leaving each track's own window in force.
118	pub cache_duration: Duration,
119
120	/// The retention window given to a track whose publisher advertises none.
121	///
122	/// moq-lite 05+ carries [`latency_max`](track::Info::latency_max) in TRACK_INFO, so a
123	/// track relayed over it keeps the window its publisher chose. Every moq-transport
124	/// draft and moq-lite 01-04 have no such wire property, so a track arriving over one
125	/// of them lands here instead. Raise it on a relay fronting a segmented egress
126	/// (HLS/DASH), which needs a playlist window's worth of history rather than the live
127	/// edge. Defaults to [`track::DEFAULT_LATENCY_MAX`], and [`Self::cache_duration`]
128	/// still caps it.
129	pub latency_default: Duration,
130
131	/// How long a broadcast under this origin outlives the *ungraceful* loss of its
132	/// last source before closing. Within the window the path stays announced and a
133	/// source re-attaching at it (a session reconnecting, a publisher re-announcing)
134	/// splices in seamlessly, so consumers never observe the gap. A source that ends
135	/// deliberately ([`broadcast::Producer::finish`], a clean unannounce from a peer)
136	/// closes the broadcast immediately regardless. Zero (the default) closes
137	/// immediately either way; a duration too large to represent as a deadline
138	/// (e.g. [`Duration::MAX`]) lingers indefinitely.
139	pub linger: Duration,
140}
141
142impl Default for Info {
143	/// An unknown origin (id `0`, no loop detection) with an unbounded pool. This is
144	/// what a standalone broadcast (no relay origin) inherits.
145	fn default() -> Self {
146		Self {
147			id: Origin::UNKNOWN,
148			pool: cache::Pool::default(),
149			cache_duration: Duration::MAX,
150			latency_default: track::DEFAULT_LATENCY_MAX,
151			linger: Duration::ZERO,
152		}
153	}
154}
155
156impl Info {
157	/// Config for the given origin id with an unbounded cache pool.
158	pub fn new(id: Origin) -> Self {
159		Self { id, ..Self::default() }
160	}
161
162	/// Set the cache pool this origin's broadcasts inherit, returning `self` for chaining.
163	pub fn with_pool(mut self, pool: cache::Pool) -> Self {
164		self.pool = pool;
165		self
166	}
167
168	/// Set the retention ceiling (see [`Self::cache_duration`]) applied to every track
169	/// under this origin, returning `self` for chaining.
170	pub fn with_cache_duration(mut self, cache_duration: Duration) -> Self {
171		self.cache_duration = cache_duration;
172		self
173	}
174
175	/// Set the retention window (see [`Self::latency_default`]) used for tracks whose
176	/// publisher advertises none, returning `self` for chaining.
177	pub fn with_latency_default(mut self, latency_default: Duration) -> Self {
178		self.latency_default = latency_default;
179		self
180	}
181
182	/// Set how long a broadcast survives ungracefully losing its last source (see
183	/// [`Self::linger`]), returning `self` for chaining.
184	pub fn with_linger(mut self, linger: Duration) -> Self {
185		self.linger = linger;
186		self
187	}
188
189	/// Consume this config to create an origin [`Producer`].
190	pub fn produce(self) -> Producer {
191		Producer::new(self)
192	}
193}
194
195impl TryFrom<u64> for Origin {
196	type Error = InvalidOrigin;
197
198	fn try_from(id: u64) -> Result<Self, Self::Error> {
199		Self::new(id)
200	}
201}
202
203impl fmt::Display for Origin {
204	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205		self.id.fmt(f)
206	}
207}
208
209impl<V: Copy> Encode<V> for Origin
210where
211	u64: Encode<V>,
212{
213	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
214		self.id.encode(w, version)
215	}
216}
217
218impl<V: Copy> Decode<V> for Origin
219where
220	u64: Decode<V>,
221{
222	fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
223		let id = u64::decode(r, version)?;
224		if id >= 1u64 << 62 {
225			return Err(DecodeError::InvalidValue);
226		}
227		Ok(Self { id })
228	}
229}
230
231/// Maximum number of origins (hops) an [`OriginList`] can hold.
232///
233/// Caps pathological or loop-induced announcements at a reasonable cluster
234/// diameter; appending past this limit returns [`TooManyOrigins`] rather than
235/// silently truncating.
236pub(crate) const MAX_HOPS: usize = 32;
237
238/// Bounded list of [`Origin`] entries, typically the hop chain of a broadcast.
239///
240/// Guarantees `len() <= MAX_HOPS`. Construct via [`OriginList::new`] +
241/// [`OriginList::push`], or fall back to the fallible [`TryFrom<Vec<Origin>>`].
242#[derive(Debug, Clone, Default, PartialEq, Eq)]
243pub struct OriginList(Vec<Origin>);
244
245/// Returned when an operation would grow an [`OriginList`] past its hop-count cap.
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247#[non_exhaustive]
248pub struct TooManyOrigins;
249
250impl fmt::Display for TooManyOrigins {
251	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252		write!(f, "too many origins (max {MAX_HOPS})")
253	}
254}
255
256impl std::error::Error for TooManyOrigins {}
257
258impl From<TooManyOrigins> for DecodeError {
259	fn from(_: TooManyOrigins) -> Self {
260		DecodeError::BoundsExceeded
261	}
262}
263
264impl OriginList {
265	/// Create an empty list.
266	pub fn new() -> Self {
267		Self(Vec::new())
268	}
269
270	/// Append an [`Origin`]. Returns [`TooManyOrigins`] if the list is full.
271	pub fn push(&mut self, origin: Origin) -> Result<(), TooManyOrigins> {
272		if self.0.len() >= MAX_HOPS {
273			return Err(TooManyOrigins);
274		}
275		self.0.push(origin);
276		Ok(())
277	}
278
279	/// Replace the first entry equal to `target` with `replacement`, returning
280	/// true if a match was found. The length is unchanged.
281	pub fn replace_first(&mut self, target: Origin, replacement: Origin) -> bool {
282		for entry in &mut self.0 {
283			if *entry == target {
284				*entry = replacement;
285				return true;
286			}
287		}
288		false
289	}
290
291	/// Returns true if any entry matches `origin`.
292	pub fn contains(&self, origin: &Origin) -> bool {
293		self.0.contains(origin)
294	}
295
296	/// Number of entries currently in the list (always `<= MAX_HOPS`).
297	pub fn len(&self) -> usize {
298		self.0.len()
299	}
300
301	/// Whether the list contains no entries.
302	pub fn is_empty(&self) -> bool {
303		self.0.is_empty()
304	}
305
306	/// Iterate over the entries in hop order (oldest first).
307	pub fn iter(&self) -> std::slice::Iter<'_, Origin> {
308		self.0.iter()
309	}
310
311	/// Borrow the entries as a slice.
312	pub fn as_slice(&self) -> &[Origin] {
313		&self.0
314	}
315}
316
317impl TryFrom<Vec<Origin>> for OriginList {
318	type Error = TooManyOrigins;
319
320	fn try_from(v: Vec<Origin>) -> Result<Self, Self::Error> {
321		if v.len() > MAX_HOPS {
322			return Err(TooManyOrigins);
323		}
324		Ok(Self(v))
325	}
326}
327
328impl<'a> IntoIterator for &'a OriginList {
329	type Item = &'a Origin;
330	type IntoIter = std::slice::Iter<'a, Origin>;
331
332	fn into_iter(self) -> Self::IntoIter {
333		self.iter()
334	}
335}
336
337impl<V: Copy> Encode<V> for OriginList
338where
339	u64: Encode<V>,
340	Origin: Encode<V>,
341{
342	fn encode<W: bytes::BufMut>(&self, w: &mut W, version: V) -> Result<(), EncodeError> {
343		(self.0.len() as u64).encode(w, version)?;
344		for origin in &self.0 {
345			origin.encode(w, version)?;
346		}
347		Ok(())
348	}
349}
350
351impl<V: Copy> Decode<V> for OriginList
352where
353	u64: Decode<V>,
354	Origin: Decode<V>,
355{
356	fn decode<R: bytes::Buf>(r: &mut R, version: V) -> Result<Self, DecodeError> {
357		let count = u64::decode(r, version)? as usize;
358		if count > MAX_HOPS {
359			return Err(DecodeError::BoundsExceeded);
360		}
361		let mut list = Vec::with_capacity(count);
362		for _ in 0..count {
363			list.push(Origin::decode(r, version)?);
364		}
365		Ok(Self(list))
366	}
367}
368
369static NEXT_CONSUMER_ID: AtomicU64 = AtomicU64::new(0);
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
372struct ConsumerId(u64);
373
374impl ConsumerId {
375	fn new() -> Self {
376		Self(NEXT_CONSUMER_ID.fetch_add(1, Ordering::Relaxed))
377	}
378}
379
380// The origin-owned broadcast at a leaf: the spliced broadcast consumers see,
381// the table of sources feeding it, and whether the path is currently announced.
382// `announced` is gated on the best source's `live` flag; a non-announced entry
383// is still returned by lookups, so an offline broadcast stays reachable for
384// subscribes and fetches.
385struct OriginBroadcast {
386	path: PathOwned,
387	/// The shared, spliced broadcast; its `consume()` is what consumers get.
388	broadcast: broadcast::Producer,
389	/// The source table, shared with every source watcher and the front task.
390	/// Also the broadcast's identity for stale-teardown checks.
391	state: kio::Producer<FrontState>,
392	announced: bool,
393}
394
395/// Ordering key used to pick the active route among broadcasts at the same path.
396///
397/// Lower wins. Shorter hop chains sort first (routing prefers the shortest path);
398/// remaining ties break on a deterministic hash of the broadcast name and hop
399/// chain. Every node in the cluster, given the same candidate routes, converges
400/// on the same winner: the hops are forwarded unchanged, and the hash is
401/// build-stable. Mixing the name in spreads equal routes across different
402/// upstreams rather than funneling onto one.
403fn route_key(name: &Path, hops: &OriginList) -> (usize, u64) {
404	(hops.len(), fnv_key(name, hops.iter().copied()))
405}
406
407/// FNV-1a over the broadcast name and a sequence of origin ids.
408///
409/// FNV-1a, not the std hasher: its output is fixed across Rust versions and
410/// builds, which matters when nodes run mismatched binaries during a rolling
411/// deploy and still need to agree on the same route. SEED is a custom basis
412/// (any nonzero u64 works, the textbook one is just as arbitrary); FNV_PRIME is
413/// the standard FNV-64 prime and should stay put.
414///
415/// Two callers, two different id sequences: [`route_key`] hashes a route's hop
416/// chain to pick among *routes*, and [`FrontState::handover_allowed`] hashes a
417/// single relay's origin to pick among *relays*. Mixing the name in spreads
418/// equal candidates across different winners rather than funneling onto one.
419fn fnv_key(name: &Path, origins: impl IntoIterator<Item = Origin>) -> u64 {
420	const SEED: u64 = 0x420C0DECB00B; // 420 C0DEC B00B
421	const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
422
423	let mut hash = SEED;
424	for &byte in name.as_str().as_bytes() {
425		hash = (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME);
426	}
427	for origin in origins {
428		for &byte in &origin.id().to_le_bytes() {
429			hash = (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME);
430		}
431	}
432
433	hash
434}
435
436/// Full ordering key for an attached route: announced routes first (an actively
437/// published source beats an offline one), then the marginal cost of pulling via
438/// the route, then the [`route_key`] hop ordering, and finally the newest source
439/// to attach. Lower wins.
440///
441/// Hop length stays the tie-break below cost, so peers that never carry a cost
442/// (pre-lite-06, or a plain local publish) rank exactly as they did before route
443/// cost existed, and equal-cost warm copies resolve to the closest one, which
444/// bounds same-datacenter chains to a single hop.
445///
446/// Recency is the last word, so it only separates routes that are identical in
447/// every advertised respect: same hop chain, same cost. That is a publisher
448/// reconnecting over a fresh session while its old one is still being kept alive
449/// by the transport, and the new session is the one actually carrying frames, so
450/// it wins the moment it attaches instead of after the QUIC idle timeout finally
451/// retires the corpse. Local attach order never leaks into cluster convergence:
452/// routes it can reorder are indistinguishable downstream, since what is
453/// forwarded is the chain and cost, which are equal by construction here.
454fn route_order(name: &Path, route: &FrontRoute) -> (bool, u64, usize, u64, Reverse<u64>) {
455	let (len, hash) = route_key(name, &route.route.hops);
456	(!route.route.announce, route.route.cost, len, hash, Reverse(route.id))
457}
458
459/// One coalesced update queued for an `AnnounceConsumer`.
460///
461/// At most one entry exists per path, so a slow consumer's pending set is bounded
462/// by the number of distinct paths. `UnannounceAnnounce` preserves the signal
463/// that a broadcast genuinely went away and a different one took its place (the
464/// consumer must see the `None` before the `Some`), while a stale `Announce`
465/// cancels with a subsequent `unannounce` because the consumer has not yet
466/// observed it.
467enum PendingUpdate {
468	Announce(broadcast::Consumer),
469	Unannounce,
470	UnannounceAnnounce(broadcast::Consumer),
471}
472
473/// Pending updates keyed by path. `BTreeMap` keeps memory strictly bounded by
474/// the number of distinct paths with outstanding work (collapsed pairs are
475/// fully erased) and gives a deterministic lexicographic delivery order so
476/// tests can predict it.
477#[derive(Default)]
478struct OriginConsumerState {
479	pending: BTreeMap<PathOwned, PendingUpdate>,
480}
481
482impl OriginConsumerState {
483	fn apply_announce(&mut self, path: PathOwned, broadcast: broadcast::Consumer) {
484		let new = match self.pending.remove(&path) {
485			// First announce, or a stale announce being replaced.
486			None | Some(PendingUpdate::Announce(_)) => PendingUpdate::Announce(broadcast),
487			// Consumer needs to observe the unannounce before this announce.
488			Some(PendingUpdate::Unannounce | PendingUpdate::UnannounceAnnounce(_)) => {
489				PendingUpdate::UnannounceAnnounce(broadcast)
490			}
491		};
492		self.pending.insert(path, new);
493	}
494
495	fn apply_unannounce(&mut self, path: PathOwned) {
496		match self.pending.remove(&path) {
497			// Consumer has not seen the pending announce; drop both entirely.
498			Some(PendingUpdate::Announce(_)) => {}
499			None | Some(PendingUpdate::Unannounce) => {
500				self.pending.insert(path, PendingUpdate::Unannounce);
501			}
502			// The embedded announce cancels with this unannounce; the consumer still
503			// needs the leading unannounce.
504			Some(PendingUpdate::UnannounceAnnounce(_)) => {
505				self.pending.insert(path, PendingUpdate::Unannounce);
506			}
507		}
508	}
509
510	/// Take one update to deliver to the consumer, if any.
511	fn take(&mut self) -> Option<OriginAnnounce> {
512		let path = self.pending.keys().next()?.clone();
513		let broadcast = match self.pending.remove(&path).unwrap() {
514			PendingUpdate::Announce(broadcast) => Some(broadcast),
515			PendingUpdate::Unannounce => None,
516			PendingUpdate::UnannounceAnnounce(broadcast) => {
517				// Deliver the unannounce now; leave the trailing announce pending so
518				// the next take returns it for the same path.
519				self.pending.insert(path.clone(), PendingUpdate::Announce(broadcast));
520				None
521			}
522		};
523		Some(OriginAnnounce { path, broadcast })
524	}
525}
526
527#[derive(Clone)]
528struct AnnounceConsumerNotify {
529	root: PathOwned,
530	state: kio::Producer<OriginConsumerState>,
531	/// The peer this stream advertises to, when it is scoped to one (see
532	/// [`Consumer::excluding`]). Every broadcast handed out registers an
533	/// [`ExclusionGuard`] for it, which is what tells the front it is exposed to that
534	/// peer even before they subscribe.
535	exclude: Option<Origin>,
536}
537
538impl AnnounceConsumerNotify {
539	fn announce(&self, path: impl AsPath, broadcast: broadcast::Consumer, front: &kio::Producer<FrontState>) {
540		let path = path.as_path().strip_prefix(&self.root).unwrap().to_owned();
541
542		// Advertising a path to a peer exposes the front to them, so register the
543		// exclusion now rather than waiting for them to subscribe. A reflection can
544		// come back before any subscription does, and the front has to already know
545		// the route it arrives on leads to a peer we are feeding.
546		let broadcast = match self.exclude.and_then(|peer| ExclusionGuard::new(front, peer)) {
547			Some(guard) => broadcast.with_exclusion(guard),
548			None => broadcast,
549		};
550
551		self.state
552			.write()
553			.ok()
554			.expect("consumer closed")
555			.apply_announce(path, broadcast);
556	}
557
558	fn unannounce(&self, path: impl AsPath) {
559		let path = path.as_path().strip_prefix(&self.root).unwrap().to_owned();
560		self.state.write().ok().expect("consumer closed").apply_unannounce(path);
561	}
562}
563
564struct NotifyNode {
565	parent: Option<Lock<NotifyNode>>,
566
567	// Consumers that are subscribed to this node.
568	// We store a consumer ID so we can remove it easily when it closes.
569	consumers: HashMap<ConsumerId, AnnounceConsumerNotify>,
570}
571
572impl NotifyNode {
573	fn new(parent: Option<Lock<NotifyNode>>) -> Self {
574		Self {
575			parent,
576			consumers: HashMap::new(),
577		}
578	}
579
580	/// `state` is the announced front's source table, so a consumer scoped to a peer
581	/// can register its [`ExclusionGuard`] against it (see
582	/// [`AnnounceConsumerNotify::announce`]).
583	fn announce(&mut self, path: impl AsPath, broadcast: &broadcast::Consumer, state: &kio::Producer<FrontState>) {
584		for consumer in self.consumers.values() {
585			consumer.announce(path.as_path(), broadcast.clone(), state);
586		}
587
588		if let Some(parent) = &self.parent {
589			parent.lock().announce(path, broadcast, state);
590		}
591	}
592
593	fn unannounce(&mut self, path: impl AsPath) {
594		for consumer in self.consumers.values() {
595			consumer.unannounce(path.as_path());
596		}
597
598		if let Some(parent) = &self.parent {
599			parent.lock().unannounce(path);
600		}
601	}
602}
603
604/// Keeps a peer registered in [`FrontState::excluded`] for as long as it holds the
605/// shared front, so the front stays off routes that flow back through it.
606///
607/// Carried by the [`broadcast::Consumer`] handed to that peer and shared by its
608/// clones, so the registration ends when the last of them drops. A guard whose
609/// front has already closed is inert.
610pub(crate) struct ExclusionGuard {
611	state: kio::Producer<FrontState>,
612	peer: Origin,
613}
614
615impl ExclusionGuard {
616	/// Register `peer` and return the guard that releases it, or `None` if the
617	/// front is closing (nothing left to keep off a route).
618	fn new(state: &kio::Producer<FrontState>, peer: Origin) -> Option<Arc<Self>> {
619		let mut s = state.write().ok()?;
620		if s.closed {
621			return None;
622		}
623		*s.excluded.entry(peer).or_default() += 1;
624		drop(s);
625		Some(Arc::new(Self {
626			state: state.clone(),
627			peer,
628		}))
629	}
630}
631
632impl Drop for ExclusionGuard {
633	fn drop(&mut self) {
634		let Ok(mut state) = self.state.write() else { return };
635		if let std::collections::hash_map::Entry::Occupied(mut entry) = state.excluded.entry(self.peer) {
636			match entry.get() {
637				1 => drop(entry.remove()),
638				n => *entry.get_mut() = n - 1,
639			}
640		}
641	}
642}
643
644/// How a path resolves against the announce tree for one consumer.
645///
646/// `Excluded` is deliberately not folded into `Missing`: they mean opposite
647/// things to a caller holding a dynamic handler. Missing is "nobody here, go
648/// ask"; excluded is "here, but not for you", and asking a handler to route it
649/// anyway is how a split-horizon violation gets in through the back door.
650enum Resolved {
651	/// A broadcast this consumer may read: the shared front, or a single source
652	/// pinned because the front holds a route back through the requester.
653	Found(broadcast::Consumer),
654	/// The path is live, but every route to it flows through the requester.
655	Excluded,
656	/// Nothing is published at the path, or it is outside the consumer's scope.
657	Missing,
658}
659
660struct OriginNode {
661	// The origin-owned broadcast published at this node, if any (see
662	// [`Producer::create_broadcast`]).
663	broadcast: Option<OriginBroadcast>,
664
665	// Nested nodes, one level down the tree.
666	nested: HashMap<String, Lock<OriginNode>>,
667
668	// Unfortunately, to notify consumers we need to traverse back up the tree.
669	notify: Lock<NotifyNode>,
670}
671
672impl OriginNode {
673	fn new(parent: Option<Lock<NotifyNode>>) -> Self {
674		Self {
675			broadcast: None,
676			nested: HashMap::new(),
677			notify: Lock::new(NotifyNode::new(parent)),
678		}
679	}
680
681	fn leaf(&mut self, path: &Path) -> Lock<OriginNode> {
682		let (dir, rest) = path.next_part().expect("leaf called with empty path");
683
684		let next = self.entry(dir);
685		if rest.is_empty() { next } else { next.lock().leaf(&rest) }
686	}
687
688	fn entry(&mut self, dir: &str) -> Lock<OriginNode> {
689		match self.nested.get(dir) {
690			Some(next) => next.clone(),
691			None => {
692				let next = Lock::new(OriginNode::new(Some(self.notify.clone())));
693				self.nested.insert(dir.to_string(), next.clone());
694				next
695			}
696		}
697	}
698
699	/// Toggle the announce state of this leaf's broadcast, notifying consumers on
700	/// a change. The identity check keeps a stale front from toggling its
701	/// successor.
702	fn set_announced(&mut self, expect: &kio::Producer<FrontState>, announce: bool) {
703		let Some(existing) = &mut self.broadcast else { return };
704		if !existing.state.same_channel(expect) || existing.announced == announce {
705			return;
706		}
707		existing.announced = announce;
708		let path = existing.path.clone();
709		let consumer = existing.broadcast.consume();
710		let state = existing.state.clone();
711		let mut notify = self.notify.lock();
712		if announce {
713			notify.announce(&path, &consumer, &state);
714		} else {
715			notify.unannounce(&path);
716		}
717	}
718
719	fn consume(&mut self, id: ConsumerId, mut notify: AnnounceConsumerNotify) {
720		self.consume_initial(&mut notify);
721		self.notify.lock().consumers.insert(id, notify);
722	}
723
724	fn consume_initial(&mut self, notify: &mut AnnounceConsumerNotify) {
725		// Only announced (live) broadcasts replay; offline ones are reachable by
726		// exact path but never advertised.
727		if let Some(broadcast) = &self.broadcast
728			&& broadcast.announced
729		{
730			notify.announce(&broadcast.path, broadcast.broadcast.consume(), &broadcast.state);
731		}
732
733		// Recursively subscribe to all nested nodes.
734		for nested in self.nested.values() {
735			nested.lock().consume_initial(notify);
736		}
737	}
738
739	fn resolve_broadcast(&self, rest: impl AsPath, exclude: Option<Origin>) -> Resolved {
740		let rest = rest.as_path();
741
742		if let Some((dir, rest)) = rest.next_part() {
743			let Some(node) = self.nested.get(dir) else {
744				return Resolved::Missing;
745			};
746			let node = node.lock();
747			return node.resolve_broadcast(&rest, exclude);
748		}
749
750		let Some(broadcast) = self.broadcast.as_ref() else {
751			return Resolved::Missing;
752		};
753		let Some(origin) = exclude else {
754			return Resolved::Found(broadcast.broadcast.consume());
755		};
756
757		// Data-plane split horizon: never serve a requester from a source whose
758		// chain flows through them (they'd receive their own bytes back, or worse,
759		// a subscription cycle).
760		//
761		// The shared spliced front is safe only while *no* attached route is
762		// tainted for the requester: the front picks per track and re-picks on
763		// failover, so a tainted route anywhere in the table is one the front may
764		// serve them from. Checking the whole table rather than just the active
765		// route is what keeps that honest at this instant; the guard registered
766		// below keeps it honest afterwards, holding the front off a route through
767		// this peer for as long as they are reading it. Otherwise pin them to the
768		// best clean source. A pinned broadcast skips the front's re-splicing,
769		// which is fine: the requester is itself a relay (only a forwarder's origin
770		// can appear in a chain) and re-splices via its own front when the pinned
771		// source dies.
772		let state = broadcast.state.read();
773		if !state.routes.iter().any(|r| r.route.hops.contains(&origin)) {
774			drop(state);
775			let shared = broadcast.broadcast.consume();
776			return match ExclusionGuard::new(&broadcast.state, origin) {
777				Some(guard) => Resolved::Found(shared.with_exclusion(guard)),
778				// The front closed between the lookup and the registration; it
779				// serves nothing now, so there is nothing to keep off a route.
780				None => Resolved::Found(shared),
781			};
782		}
783		match state
784			.dispatch(Some(origin))
785			.and_then(|clean| state.routes.iter().find(|r| r.id == clean))
786		{
787			Some(route) => Resolved::Found(route.source.clone()),
788			None => Resolved::Excluded,
789		}
790	}
791
792	fn unconsume(&mut self, id: ConsumerId) {
793		self.notify.lock().consumers.remove(&id).expect("consumer not found");
794		if self.is_empty() {
795			//tracing::warn!("TODO: empty node; memory leak");
796			// This happens when consuming a path that is not being broadcasted.
797		}
798	}
799
800	/// Remove the broadcast at `relative` if it is `expect`, unannouncing it if
801	/// needed and pruning empty nodes on the way back up. The identity check
802	/// keeps a stale teardown from clobbering a replacement.
803	fn remove(&mut self, expect: &kio::Producer<FrontState>, relative: impl AsPath) {
804		let relative = relative.as_path();
805
806		if let Some((dir, relative)) = relative.next_part() {
807			let Some(nested) = self.nested.get(dir) else { return };
808			let nested = nested.clone();
809			let mut locked = nested.lock();
810			locked.remove(expect, &relative);
811
812			if locked.is_empty() {
813				drop(locked);
814				self.nested.remove(dir);
815			}
816		} else if let Some(existing) = &self.broadcast
817			&& existing.state.same_channel(expect)
818		{
819			let existing = self.broadcast.take().expect("checked above");
820			if existing.announced {
821				self.notify.lock().unannounce(&existing.path);
822			}
823		}
824	}
825
826	fn is_empty(&self) -> bool {
827		self.broadcast.is_none() && self.nested.is_empty() && self.notify.lock().consumers.is_empty()
828	}
829}
830
831#[derive(Clone)]
832struct OriginNodes {
833	nodes: Vec<(PathOwned, Lock<OriginNode>)>,
834}
835
836impl OriginNodes {
837	// Returns nested roots that match the prefixes.
838	// PathPrefixes guarantees no duplicates or overlapping prefixes.
839	pub fn select(&self, prefixes: &PathPrefixes) -> Option<Self> {
840		let mut roots = Vec::new();
841
842		for (root, state) in &self.nodes {
843			for prefix in prefixes {
844				if root.has_prefix(prefix) {
845					// Keep the existing node if we're allowed to access it.
846					roots.push((root.to_owned(), state.clone()));
847					continue;
848				}
849
850				if let Some(suffix) = prefix.strip_prefix(root) {
851					// If the requested prefix is larger than the allowed prefix, then we further scope it.
852					let nested = state.lock().leaf(&suffix);
853					roots.push((prefix.to_owned(), nested));
854				}
855			}
856		}
857
858		if roots.is_empty() {
859			None
860		} else {
861			Some(Self { nodes: roots })
862		}
863	}
864
865	pub fn root(&self, new_root: impl AsPath) -> Option<Self> {
866		let new_root = new_root.as_path();
867		let mut roots = Vec::new();
868
869		if new_root.is_empty() {
870			return Some(self.clone());
871		}
872
873		for (root, state) in &self.nodes {
874			if let Some(suffix) = root.strip_prefix(&new_root) {
875				// If the old root is longer than the new root, shorten the keys.
876				roots.push((suffix.to_owned(), state.clone()));
877			} else if let Some(suffix) = new_root.strip_prefix(root) {
878				// If the new root is longer than the old root, add a new root.
879				// NOTE: suffix can't be empty
880				let nested = state.lock().leaf(&suffix);
881				roots.push(("".into(), nested));
882			}
883		}
884
885		if roots.is_empty() {
886			None
887		} else {
888			Some(Self { nodes: roots })
889		}
890	}
891
892	// Returns the root that has this prefix.
893	pub fn get(&self, path: impl AsPath) -> Option<(Lock<OriginNode>, PathOwned)> {
894		let path = path.as_path();
895
896		for (root, state) in &self.nodes {
897			if let Some(suffix) = path.strip_prefix(root) {
898				return Some((state.clone(), suffix.to_owned()));
899			}
900		}
901
902		None
903	}
904}
905
906impl Default for OriginNodes {
907	fn default() -> Self {
908		Self {
909			nodes: vec![("".into(), Lock::new(OriginNode::new(None)))],
910		}
911	}
912}
913
914/// A path and the broadcast now available there, delivered by [`AnnounceConsumer`].
915#[derive(Clone)]
916pub struct OriginAnnounce {
917	/// The path of the broadcast, relative to the consuming cursor's root.
918	pub path: PathOwned,
919	/// The broadcast now available at that path, or `None` if it is no longer available.
920	///
921	/// A replacement (a relay failover, or a shorter hop path arriving) is delivered as a
922	/// `None` followed by a `Some`, never as a swap in place. A route change alone is invisible here (the handles stay
923	/// valid); observe it via [`broadcast::Consumer::route_changed`].
924	pub broadcast: Option<broadcast::Consumer>,
925}
926
927/// Announces broadcasts to consumers over the network.
928#[derive(Clone)]
929pub struct Producer {
930	// Identity for this origin. Appended to broadcast hops when
931	// re-announcing so downstream relays can detect loops and prefer the
932	// shortest path.
933	info: Origin,
934
935	// The roots of the tree that we are allowed to publish.
936	// A path of "" means we can publish anything.
937	nodes: OriginNodes,
938
939	// The prefix that is automatically stripped from all paths.
940	root: PathOwned,
941
942	// Fallback request queue, shared with every derived consumer. Separate from
943	// `nodes` because dynamic broadcasts are never announced: they only resolve a
944	// consumer's `request_broadcast` when no live announcement exists.
945	dynamic: kio::Shared<OriginDynamicState>,
946
947	// The cache pool inherited by broadcasts created under this origin (sessions
948	// mint their remote broadcasts with it). Unbounded by default.
949	pool: cache::Pool,
950
951	// Retention ceiling inherited by broadcasts created under this origin (see
952	// [`Info::cache_duration`]). `Duration::MAX` (no ceiling) by default.
953	cache_duration: Duration,
954
955	// Retention window for a track whose publisher advertises none (see
956	// [`Info::latency_default`]).
957	latency_default: Duration,
958
959	// How long a broadcast outlives ungracefully losing its last source (see
960	// [`Info::linger`]). Zero by default.
961	linger: Duration,
962
963	// Ingress stats context. Broadcasts created through this producer are attributed
964	// to it (writes counted on the subscriber/ingress side). Empty (no-op) unless a
965	// session tagged this handle via [`Self::with_stats`].
966	stats: stats::Session,
967}
968
969impl std::ops::Deref for Producer {
970	type Target = Origin;
971
972	fn deref(&self) -> &Self::Target {
973		&self.info
974	}
975}
976
977impl Producer {
978	/// Build a producer from an [`Info`] (identity + cache pool) with no scoped
979	/// prefix and no pre-existing broadcasts. Prefer [`Info::produce`] /
980	/// [`Origin::produce`].
981	pub fn new(info: Info) -> Self {
982		Self {
983			info: info.id,
984			nodes: OriginNodes::default(),
985			root: PathOwned::default(),
986			dynamic: kio::Shared::default(),
987			pool: info.pool,
988			cache_duration: info.cache_duration,
989			latency_default: info.latency_default,
990			linger: info.linger,
991			stats: stats::Session::default(),
992		}
993	}
994
995	/// Attach an ingress stats context: broadcasts created through this handle (and
996	/// any handle derived from it) are attributed to `session` on the subscriber
997	/// (ingress) side. Pass [`stats::Session::default`] to opt out.
998	pub fn with_stats(mut self, session: stats::Session) -> Self {
999		self.stats = session;
1000		self
1001	}
1002
1003	/// Set the linger (see [`Info::linger`]) for broadcasts created through this
1004	/// handle and any handle derived from it.
1005	///
1006	/// A broadcast adopts the window of the handle whose source *created* it (the
1007	/// first source at the path), so set this before handing the producer to
1008	/// whatever attaches sources through it (e.g. a session). Lets one supplier
1009	/// declare its own recovery promise, like a reconnecting client lingering for
1010	/// as long as its retry loop keeps trying, without reconfiguring the origin.
1011	pub fn with_linger(mut self, linger: Duration) -> Self {
1012		self.linger = linger;
1013		self
1014	}
1015
1016	/// This origin's [`Info`] (identity + cache pool), the parent handle a broadcast
1017	/// created under this origin carries (see [`broadcast::Info::origin`]).
1018	pub fn info(&self) -> Info {
1019		Info {
1020			id: self.info,
1021			pool: self.pool.clone(),
1022			cache_duration: self.cache_duration,
1023			latency_default: self.latency_default,
1024			linger: self.linger,
1025		}
1026	}
1027
1028	// The retention window for a track whose publisher advertises none (see
1029	// [`Info::latency_default`]). Cheaper than `info()`, which clones the pool.
1030	pub(crate) fn latency_default(&self) -> Duration {
1031		self.latency_default
1032	}
1033
1034	/// A producer with *no* allowed prefixes: it can't publish anything and
1035	/// advertises no subscribe interest (its `allowed()` is empty, so the
1036	/// subscriber issues no ANNOUNCE_PLEASE). Used to fill an unset session half
1037	/// so both the publisher and subscriber loops still run.
1038	pub(crate) fn empty(info: Origin) -> Self {
1039		Self {
1040			info,
1041			nodes: OriginNodes { nodes: Vec::new() },
1042			root: PathOwned::default(),
1043			dynamic: kio::Shared::default(),
1044			pool: cache::Pool::default(),
1045			cache_duration: Duration::MAX,
1046			latency_default: track::DEFAULT_LATENCY_MAX,
1047			linger: Duration::ZERO,
1048			stats: stats::Session::default(),
1049		}
1050	}
1051
1052	/// Create a broadcast at `path`, fed through the returned producer.
1053	///
1054	/// This is the sole way content enters an origin. The returned
1055	/// [`broadcast::Producer`] is a route source: the origin owns the broadcast
1056	/// consumers actually see, and splices its tracks across every source created
1057	/// at the same path (other local publishers, or sessions attaching announces
1058	/// from the network), always serving from the best [`broadcast::Route`] (live
1059	/// first, then lowest cost, then shortest hops with a deterministic
1060	/// tie-break, and the newest source among otherwise equal routes). When the
1061	/// best source changes, tracks resume from the replacement at the first
1062	/// missing group; consumers never observe the swap.
1063	///
1064	/// Splicing requires the same content identity: every source at a path must
1065	/// share the first hop of its route, which is a promise that they produce
1066	/// interchangeable tracks. An *announced* source arriving with a different
1067	/// first hop is a replacement instead: it takes the path immediately and
1068	/// consumers see an unannounce followed by an announce, rather than unrelated
1069	/// content spliced into a live subscription. So a publisher reconnecting
1070	/// under a fresh identity displaces the session it replaced right away,
1071	/// without waiting for the transport to notice the old one is gone. An
1072	/// offline source never displaces anything: it ranks below every announced
1073	/// route, so it waits invisibly for the incumbent to end.
1074	///
1075	/// `route` is the source's initial metadata; update it with
1076	/// [`broadcast::Producer::set_route`]. The [`broadcast::Route::announce`] flag
1077	/// controls whether the path is announced: a non-live broadcast is invisible
1078	/// to [`Consumer::announced`] but stays reachable by exact path for
1079	/// subscribes and fetches (e.g. serving cached or on-demand content), so
1080	/// toggling `live` announces or unannounces without touching the broadcast.
1081	///
1082	/// The broadcast becomes visible to consumers asynchronously, shortly after
1083	/// this returns. Create tracks and register a
1084	/// [`broadcast::Producer::dynamic`] handler before awaiting, so the first
1085	/// consumer finds them.
1086	///
1087	/// End the broadcast with [`broadcast::Producer::finish`]; dropping it
1088	/// without finishing also works, but logs a warning. A finish closes and
1089	/// unannounces the path immediately once it was the last source. An unfinished
1090	/// drop is treated as an outage: the path survives for the origin's
1091	/// [`Info::linger`] (zero by default), so a replacement source attaching within
1092	/// that window splices in without consumers noticing.
1093	///
1094	/// Fails with [`Error::Unauthorized`] if `path` is outside the prefixes this
1095	/// producer may publish under (after [`scope`](Self::scope) /
1096	/// [`with_root`](Self::with_root)), or [`Error::BoundsExceeded`] if the full
1097	/// rooted path exceeds [`Path::MAX_PARTS`]. Must be called with a runtime
1098	/// available (it spawns the broadcast's lifecycle task). Callers must not use
1099	/// a route whose hop chain contains this origin's id (it would form a routing
1100	/// loop); relays filter such reflections before they reach here, checked by a
1101	/// `debug_assert`.
1102	pub fn create_broadcast(&self, path: impl AsPath, route: broadcast::Route) -> Result<broadcast::Producer, Error> {
1103		let path = path.as_path();
1104
1105		debug_assert!(
1106			!route.hops.contains(&self.info),
1107			"create_broadcast called with a looping hop chain",
1108		);
1109
1110		let (node, rest) = self.nodes.get(&path).ok_or(Error::Unauthorized)?;
1111		let full = self.root.join(&path).to_owned();
1112
1113		// A decoded announce prefix and suffix are each within the wire limit, but their
1114		// join might not be. Enforcing here bounds the tree depth and guarantees the path
1115		// can be re-encoded when forwarded.
1116		if full.parts().count() > Path::MAX_PARTS {
1117			return Err(BoundsExceeded.into());
1118		}
1119
1120		// Resolve the ingress counters once, keyed by the absolute broadcast path.
1121		// The source producer tags its tracks; run_source drives the announce guard
1122		// off route transitions.
1123		let ingress = self.stats.ingress(&full);
1124
1125		let mut source = broadcast::Info { origin: self.info() }
1126			.produce()
1127			.with_stats(ingress.clone());
1128		source.set_route(route).expect("fresh producer");
1129
1130		web_async::spawn(run_source(self.info(), node, full, rest, source.consume(), ingress));
1131
1132		Ok(source)
1133	}
1134
1135	/// Returns a new Producer restricted to publishing under one of `prefixes`.
1136	///
1137	/// Returns None if there are no legal prefixes (the requested prefixes are
1138	/// disjoint from this producer's current scope).
1139	// TODO accept PathPrefixes instead of &[Path]
1140	pub fn scope(&self, prefixes: &[Path]) -> Option<Producer> {
1141		let prefixes = PathPrefixes::new(prefixes);
1142		Some(Producer {
1143			info: self.info,
1144			nodes: self.nodes.select(&prefixes)?,
1145			root: self.root.clone(),
1146			dynamic: self.dynamic.clone(),
1147			pool: self.pool.clone(),
1148			cache_duration: self.cache_duration,
1149			latency_default: self.latency_default,
1150			linger: self.linger,
1151			stats: self.stats.clone(),
1152		})
1153	}
1154
1155	/// Create a dynamic handler that picks up [`Consumer::request_broadcast`]
1156	/// calls for paths that are not announced.
1157	///
1158	/// This is the origin-level analogue of [`broadcast::Producer::dynamic`]: it serves
1159	/// broadcasts on demand rather than tracks. Crucially the served broadcasts are
1160	/// *not* announced, so [`Consumer::announced`] never sees them; they exist
1161	/// only as a fallback for a consumer that asks for an exact path with no live
1162	/// announcement. Drop the handler (and every clone) to reject pending requests.
1163	pub fn dynamic(&self) -> Dynamic {
1164		Dynamic::new(self.info, self.root.clone(), self.dynamic.clone())
1165	}
1166
1167	/// Cheap read handle over this origin's broadcast tree.
1168	///
1169	/// Use [`Consumer::announced`] to register interest and start receiving
1170	/// announcement events; the consumer itself does not allocate any channels.
1171	pub fn consume(&self) -> Consumer {
1172		// Untagged: a session tags the egress consumer separately via
1173		// `origin::Consumer::with_stats` (ingress and egress are distinct sides).
1174		Consumer::new(
1175			self.info,
1176			self.root.clone(),
1177			self.nodes.clone(),
1178			self.dynamic.clone(),
1179			stats::Session::default(),
1180		)
1181	}
1182
1183	/// Handle to the announcement stream for this producer's subtree.
1184	///
1185	/// Symmetric counterpart to [`Self::consume`]; call
1186	/// [`AnnounceProducer::consume`] to get an [`AnnounceConsumer`] that
1187	/// receives announce / unannounce events.
1188	pub fn announces(&self) -> AnnounceProducer {
1189		AnnounceProducer::new(self.root.clone(), self.nodes.clone())
1190	}
1191
1192	/// Returns a new Producer that automatically strips out the provided prefix.
1193	///
1194	/// Returns None if the provided root is not authorized; when [`Self::scope`]
1195	/// was already used without a wildcard.
1196	pub fn with_root(&self, prefix: impl AsPath) -> Option<Self> {
1197		let prefix = prefix.as_path();
1198
1199		Some(Self {
1200			info: self.info,
1201			root: self.root.join(&prefix).to_owned(),
1202			nodes: self.nodes.root(&prefix)?,
1203			dynamic: self.dynamic.clone(),
1204			pool: self.pool.clone(),
1205			cache_duration: self.cache_duration,
1206			latency_default: self.latency_default,
1207			linger: self.linger,
1208			stats: self.stats.clone(),
1209		})
1210	}
1211
1212	/// Returns the root that is automatically stripped from all paths.
1213	pub fn root(&self) -> &Path<'_> {
1214		&self.root
1215	}
1216
1217	/// Iterate over the path prefixes this handle is permitted to publish or subscribe under.
1218	// TODO return PathPrefixes
1219	pub fn allowed(&self) -> impl Iterator<Item = &Path<'_>> {
1220		self.nodes.nodes.iter().map(|(root, _)| root)
1221	}
1222
1223	/// Converts a relative path to an absolute path.
1224	pub fn absolute(&self, path: impl AsPath) -> Path<'_> {
1225		self.root.join(path)
1226	}
1227}
1228
1229/// How long a spliced track stays warm after its last reader leaves.
1230///
1231/// Within the window a returning viewer, or the next of a run of back-to-back
1232/// fetches, reuses the source's copy: no new track request, and no second round
1233/// trip for its `TRACK_INFO`. After it, the copy is released so an idle track
1234/// costs nothing upstream.
1235///
1236/// Sized above the fetch cadence of a segmented consumer: HLS polls every
1237/// `TARGETDURATION` seconds, commonly 6 or 10, so a shorter window would drop the
1238/// copy between every segment and re-request the track each time. A warm copy
1239/// holds no upstream subscription (that is canceled as soon as demand ends), so
1240/// waiting longer costs cached state, not a viewer.
1241const TRACK_IDLE_LINGER: Duration = Duration::from_secs(30);
1242
1243/// One attached source in a [`FrontState`] table.
1244struct FrontRoute {
1245	id: u64,
1246	/// The source's latest [`broadcast::Route`], mirrored from its
1247	/// `route_changed` stream; picks the active source and gates the announce.
1248	route: broadcast::Route,
1249	/// The source broadcast tracks are served from.
1250	source: broadcast::Consumer,
1251}
1252
1253/// Shared state behind a [`Front`]: the attached sources and which one is active.
1254struct FrontState {
1255	/// Absolute path of the broadcast, mixed into the route tie-break hash.
1256	path: PathOwned,
1257	/// The local origin's identity, the other half of the handover key gate.
1258	self_origin: Origin,
1259	/// Content identity: the original publisher (first hop) shared by every
1260	/// attached source, or `None` for a broadcast produced locally (no hops).
1261	/// Fixed for the front's lifetime; a source with a different first hop is new
1262	/// content, not an alternate route, so it replaces this front (or waits for
1263	/// it, when offline) rather than joining it (see [`attach_source`]). This is
1264	/// the same rule the session layer applies to a restart whose first hop
1265	/// changed.
1266	publisher: Option<Origin>,
1267	/// Attach counter, handed to each [`FrontRoute`] so [`route_order`] can break
1268	/// an exact tie toward the newest source.
1269	next_route: u64,
1270	routes: Vec<FrontRoute>,
1271	/// Peers this front is exposed to, refcounted by live [`ExclusionGuard`]s: those
1272	/// reading it through the shared broadcast, and those we merely advertise it to.
1273	/// The resolve-time check only proves the table is clean for a requester at that
1274	/// instant; the front picks per track and re-picks on failover, so without this a
1275	/// route tainted for an attached peer could be adopted underneath them and hand
1276	/// them back their own bytes. Routes through these origins are avoided while any
1277	/// clean alternative exists.
1278	///
1279	/// Advertising registers a peer too, because a peer that cannot echo our identity
1280	/// back (moq-transport carries no hop ids) may re-advertise the path to us before
1281	/// it ever subscribes. That reflection is otherwise indistinguishable from a rival
1282	/// publisher, and this is what tells [`attach_source`] apart.
1283	excluded: HashMap<Origin, usize>,
1284	/// The source tracks are dispatched to. Backups park until promoted.
1285	active: Option<u64>,
1286	/// How long the front outlives ungracefully losing its last source (see
1287	/// [`Info::linger`]). [`run_front`] arms the countdown when the table empties.
1288	linger: Duration,
1289	/// Terminal: no more sources may attach and every poller stops. Set
1290	/// synchronously by the detach that empties the table or by an
1291	/// [`attach_source`] takeover, and by [`run_front`] when the linger window
1292	/// expires without a replacement.
1293	closed: bool,
1294}
1295
1296impl FrontState {
1297	/// The one selection primitive every picker goes through: the best route by
1298	/// [`route_order`] among those surviving `keep`. With `untainted`, the pick
1299	/// also steers away from routes that flow through a peer currently reading
1300	/// the shared front, unless that leaves nothing (see
1301	/// [`Self::prefer_untainted`]); it is off for [`Self::dispatch`], which pins
1302	/// one requester to one source rather than serving the shared front.
1303	fn pick(&self, keep: impl Fn(&FrontRoute) -> bool, untainted: bool) -> Option<u64> {
1304		let candidates: Vec<&FrontRoute> = self.routes.iter().filter(|r| keep(r)).collect();
1305		let candidates = match untainted {
1306			true => self.prefer_untainted(&candidates),
1307			false => candidates,
1308		};
1309		candidates
1310			.into_iter()
1311			.min_by_key(|r| route_order(&self.path.as_path(), r))
1312			.map(|r| r.id)
1313	}
1314
1315	/// The source new track requests should dispatch to: live first, then lowest
1316	/// cost, then shortest hop chain with a deterministic hash tie-break and the
1317	/// newest source last, skipping routes that flow through a peer currently
1318	/// reading the front while any other route remains.
1319	fn best_route(&self) -> Option<u64> {
1320		self.pick(|_| true, true)
1321	}
1322
1323	/// The source a subscription from `exclude` should dispatch to: the best
1324	/// route whose hop chain does not contain the requester. This is the same
1325	/// selection a session uses to pick what it announces to that peer, and the
1326	/// two being one computation is the loop-freedom invariant: chains stay
1327	/// truthful, so any would-be cycle surfaces the requester's own origin in
1328	/// the candidate chain and is filtered here, at any cycle length.
1329	fn dispatch(&self, exclude: Option<Origin>) -> Option<u64> {
1330		self.pick(|r| exclude.is_none_or(|origin| !r.route.hops.contains(&origin)), false)
1331	}
1332
1333	/// Whether `route` flows back through a peer this front is exposed to, so serving
1334	/// (or attaching) it would hand that peer its own bytes back.
1335	///
1336	/// Doubles as the reflection test in [`attach_source`]: a route arriving through a
1337	/// peer we are already advertising this path to is our own broadcast coming home,
1338	/// whatever its chain claims.
1339	fn taints_a_reader(&self, route: &broadcast::Route) -> bool {
1340		route.hops.iter().any(|hop| self.excluded.contains_key(hop))
1341	}
1342
1343	/// Narrow `candidates` to the routes clean for every peer currently reading the
1344	/// shared front, unless that would leave nothing.
1345	///
1346	/// Keeping the front off a tainted route is what makes the resolve-time
1347	/// split-horizon check hold for the life of a subscription rather than just at
1348	/// request time. Falling back when every route is tainted is deliberate: the
1349	/// alternative is starving readers the route is perfectly good for, and a peer
1350	/// whose only path runs back through itself has nothing to be served from
1351	/// anyway. It re-resolves to [`Error::Unroutable`] on its next request.
1352	fn prefer_untainted<'a>(&self, candidates: &[&'a FrontRoute]) -> Vec<&'a FrontRoute> {
1353		if self.excluded.is_empty() {
1354			return candidates.to_vec();
1355		}
1356		let clean: Vec<&FrontRoute> = candidates
1357			.iter()
1358			.copied()
1359			.filter(|r| !self.taints_a_reader(&r.route))
1360			.collect();
1361		match clean.is_empty() {
1362			true => candidates.to_vec(),
1363			false => clean,
1364		}
1365	}
1366
1367	/// The source one track should be served from: the front's active source
1368	/// unless `skip` rules it out, then the next-best route that survives.
1369	///
1370	/// Whether a source carries a given track is a per-track property (a standby
1371	/// that has not created it yet, a publisher whose encoder is still starting),
1372	/// so a source refusing one track is ruled out of that track only, never out
1373	/// of the front. Preferring `active` keeps a servable track on exactly the
1374	/// route [`Self::reselect`] chose, handover gate included.
1375	fn serve_route(&self, skip: impl Fn(u64) -> bool) -> Option<u64> {
1376		if let Some(active) = self.active
1377			&& !skip(active)
1378			&& let Some(route) = self.routes.iter().find(|r| r.id == active)
1379			&& !self.taints_a_reader(&route.route)
1380		{
1381			return Some(active);
1382		}
1383		self.pick(|r| !skip(r.id), true)
1384	}
1385
1386	/// Re-pick the active source after the table changed. Serve tasks watch
1387	/// `active` and re-splice on their own, so a cheaper route takes over
1388	/// seamlessly at a group boundary.
1389	///
1390	/// The one exception is the simultaneous-activation race: two nodes that
1391	/// each pulled the broadcast before seeing the other both advertise zero
1392	/// cost, so each sees the other as cheaper than its own source, and
1393	/// re-parenting onto each other at once leaves the broadcast with no
1394	/// upstream at all. That hazard only exists when both sides are actively
1395	/// carrying, so the gate is scoped to exactly that: while `carrying` (the
1396	/// front has live demand), a cheaper route whose announcing relay is itself
1397	/// carrying (it advertised zero from a chain of two or more hops; a chain of
1398	/// one is the original publisher, which can never adopt a route to its own
1399	/// broadcast) displaces an announced incumbent only when
1400	/// [`Self::handover_allowed`] says so. Every other cheaper route, e.g. a
1401	/// forwarder path or an upstream that repriced itself down, is taken
1402	/// immediately.
1403	fn reselect(&mut self, carrying: bool) {
1404		let best = self.best_route();
1405		if carrying
1406			&& let (Some(best_id), Some(cur_id)) = (best, self.active)
1407			&& best_id != cur_id
1408			&& let Some(candidate) = self.routes.iter().find(|r| r.id == best_id)
1409			&& let Some(incumbent) = self.routes.iter().find(|r| r.id == cur_id)
1410			&& incumbent.route.announce
1411			&& candidate.route.cost < incumbent.route.cost
1412			&& candidate.route.advertised == 0
1413			&& candidate.route.hops.len() >= 2
1414			&& !self.handover_allowed(&candidate.route)
1415		{
1416			// We won the key comparison: keep our source and let the peer come to us.
1417			return;
1418		}
1419		self.active = best;
1420	}
1421
1422	/// Whether re-parenting onto `route` is allowed while actively carrying: the
1423	/// announcing peer (the chain's last hop) must hash strictly below our own
1424	/// origin for this broadcast name.
1425	///
1426	/// Both sides compute the same two keys (the hash is build-stable and the
1427	/// inputs are shared), so the comparison resolves the same way everywhere:
1428	/// the lower-keyed node keeps its source, the higher-keyed one re-parents.
1429	/// A strict total order has no cycles, so mutual pulls cannot happen. Mixing
1430	/// the broadcast name in spreads ownership across a region's relays instead
1431	/// of funneling every broadcast onto the lowest-keyed one. A route with no
1432	/// hops is a local publish and always allowed.
1433	fn handover_allowed(&self, route: &broadcast::Route) -> bool {
1434		let name = self.path.as_path();
1435		match route.hops.iter().last() {
1436			Some(peer) => fnv_key(&name, [*peer]) < fnv_key(&name, [self.self_origin]),
1437			None => true,
1438		}
1439	}
1440
1441	/// Every attached route in preference order with the active one first,
1442	/// mirrored onto the front's broadcast so sessions can advertise (and be
1443	/// served) a different route per peer. The active route leads even when the
1444	/// handover gate kept it over a lower-ordered candidate, so `routes[0]` is
1445	/// always what this node is actually serving from.
1446	fn routes_snapshot(&self) -> Vec<broadcast::Route> {
1447		let mut routes: Vec<&FrontRoute> = self.routes.iter().collect();
1448		routes.sort_by_key(|r| route_order(&self.path.as_path(), r));
1449		routes.sort_by_key(|r| Some(r.id) != self.active);
1450		routes.into_iter().map(|r| r.route.clone()).collect()
1451	}
1452}
1453
1454/// Refresh the front's public face after a table change: advertise the best
1455/// source's route on the spliced broadcast and gate the path's announcement on
1456/// its `live` flag.
1457///
1458/// Re-reads the table at apply time (rather than applying a value computed under
1459/// an earlier lock) so concurrent attach/detach/update calls converge on the
1460/// latest winner regardless of the order their applies land in. An empty table
1461/// leaves the advert and announce state alone; the front task is closing and
1462/// unannounces on its way out.
1463fn sync_front(state: &kio::Producer<FrontState>, broadcast: &broadcast::Producer, leaf: &Lock<OriginNode>) {
1464	// Snapshot and apply under the leaf lock: two concurrent syncs would
1465	// otherwise race their applies, letting a stale snapshot land last and
1466	// leave the announce flag (or advert) contradicting the current table.
1467	// Lock order (leaf, then table, then broadcast) matches attach_source.
1468	let mut leaf_guard = leaf.lock();
1469	let routes = state.read().routes_snapshot();
1470	if let Some(advert) = routes.first() {
1471		let announce = advert.announce;
1472		broadcast.clone().set_routes(routes);
1473		leaf_guard.set_announced(state, announce);
1474	}
1475}
1476
1477/// Detach source `id`, promoting the next-best source; the tracks it was serving
1478/// re-splice on their own. Idempotent.
1479///
1480/// `graceful` says how the source ended: a deliberate finish (a publisher done
1481/// publishing, a peer's clean unannounce) or an abrupt loss (a session dying).
1482/// Detaching the last source *gracefully* closes the broadcast synchronously,
1483/// which guarantees a following create at the path is a *new* broadcast rather
1484/// than splicing new content into this one. An abrupt last detach instead leaves
1485/// the front open for the configured linger ([`Info::linger`]), so a source
1486/// re-attaching within the window (a reconnect) splices in seamlessly;
1487/// [`run_front`] closes it if the window expires empty. A zero linger closes
1488/// abrupt detaches synchronously too.
1489fn detach_source(
1490	state: &kio::Producer<FrontState>,
1491	broadcast: &broadcast::Producer,
1492	leaf: &Lock<OriginNode>,
1493	id: u64,
1494	graceful: bool,
1495) {
1496	let close = {
1497		// Snapshotted before the state lock (lock order: broadcast, then front).
1498		// A demand flip in between only stales this reselect's handover gate,
1499		// which self-corrects on the next table change.
1500		let carrying = broadcast.demand().is_used();
1501		let Ok(mut s) = state.write() else { return };
1502		let Some(pos) = s.routes.iter().position(|r| r.id == id) else {
1503			return;
1504		};
1505		s.routes.remove(pos);
1506		s.reselect(carrying);
1507		if s.routes.is_empty() && !s.closed && (graceful || s.linger.is_zero()) {
1508			// Last one out: close now. The front task observes `closed` and
1509			// finishes the teardown (unpublish).
1510			s.closed = true;
1511			true
1512		} else {
1513			false
1514		}
1515	};
1516	if close {
1517		broadcast.abort_spliced(Error::Dropped);
1518	}
1519	sync_front(state, broadcast, leaf);
1520}
1521
1522/// Match the ingress announce guard to a route's announce flag: opening bumps
1523/// the announced counters, dropping bumps the closed ones. See [`run_source`].
1524fn sync_announce(guard: &mut Option<stats::Announce>, announced: bool, ingress: &stats::Scope) {
1525	match (announced, guard.is_some()) {
1526		(true, false) => *guard = Some(ingress.announce()),
1527		(false, true) => *guard = None,
1528		_ => {}
1529	}
1530}
1531
1532/// Owns one source's lifecycle: attaches it to the front at its path on the first
1533/// route observation, forwards route updates, and detaches it when the source
1534/// closes. Spawned by [`Producer::create_broadcast`].
1535///
1536/// An announced source whose original publisher (first hop) differs from the
1537/// live front's takes the path over as a fresh broadcast rather than joining; an
1538/// offline one parks until the front closes. A route update that changes the
1539/// source's own first hop likewise detaches it and re-runs the attach, so a
1540/// publisher swap is always a replacement, never a silent splice.
1541async fn run_source(
1542	origin: Info,
1543	node: Lock<OriginNode>,
1544	full: PathOwned,
1545	rest: PathOwned,
1546	mut source: broadcast::Consumer,
1547	ingress: stats::Scope,
1548) {
1549	let ctx = AttachContext {
1550		origin: &origin,
1551		node: &node,
1552		full: &full,
1553		rest: &rest,
1554	};
1555
1556	// The first `route_changed` yields the current route immediately; nothing is
1557	// visible to consumers until this attach, giving the creator a window to set
1558	// up tracks and dynamic handlers.
1559	let Ok(mut route) = source.route_changed().await else {
1560		// Closed before ever attaching; nothing became visible.
1561		return;
1562	};
1563
1564	// Ingress announce guard: held while this source's route is announced. Opening
1565	// bumps `announced` + `announced_bytes`; dropping (route offline, or the source
1566	// closing below) bumps `announced_closed` + `announced_bytes`. Empty scope =
1567	// no-op.
1568	let mut announce = route.announce.then(|| ingress.announce());
1569	// Whether this source still has content the live front has not already beaten.
1570	// Cleared when a rival displaces it, so it stands by instead of evicting the
1571	// winner straight back; only a new publisher re-arms it, since a repricing is
1572	// the same content losing the same argument twice. Whether the route is
1573	// announced is a separate gate, owned by `attach_source`.
1574	let mut may_take_over = true;
1575
1576	'attach: loop {
1577		// Re-resolved every attempt: between attaches the previous front's
1578		// teardown may have pruned the (then-empty) leaf from the tree, and
1579		// attaching to the stale lock would publish into an orphan that lookups
1580		// can no longer reach.
1581		let leaf = if rest.is_empty() {
1582			node.clone()
1583		} else {
1584			node.lock().leaf(&rest)
1585		};
1586
1587		let (state, broadcast, id) = match attach_source(&ctx, &leaf, &source, route.clone(), may_take_over) {
1588			Attach::Ready(state, broadcast, id) => (state, broadcast, id),
1589			Attach::Parked(incumbent) => {
1590				tracing::debug!(
1591					broadcast = %full,
1592					"path already live with a different publisher; parking this source until it ends",
1593				);
1594				// Wait for the incumbent front to close, or for our own route to
1595				// change: a new route observation earns another takeover attempt, and
1596				// our source closing means giving up.
1597				let update = kio::wait(|waiter| {
1598					if let Poll::Ready(update) = source.poll_route_changed(waiter) {
1599						return Poll::Ready(Some(update));
1600					}
1601					// Ready on either the closed flag or the channel itself dying;
1602					// both mean the incumbent is gone.
1603					match incumbent.poll(waiter, |s| if s.closed { Poll::Ready(()) } else { Poll::Pending }) {
1604						Poll::Ready(_) => Poll::Ready(None),
1605						Poll::Pending => Poll::Pending,
1606					}
1607				})
1608				.await;
1609				match update {
1610					// Our route moved; recompute the guard and retry with it.
1611					Some(Ok(update)) => {
1612						sync_announce(&mut announce, update.announce, &ingress);
1613						// Only a new publisher is content the winner has not beaten.
1614						// Plain equality, matching the detach check below: an
1615						// UNKNOWN-to-UNKNOWN repricing from a legacy peer proves no
1616						// new identity, so it must not re-arm either.
1617						if update.hops.iter().next().copied() != route.hops.iter().next().copied() {
1618							may_take_over = true;
1619						}
1620						route = update;
1621					}
1622					// The source closed while parked; it was never visible.
1623					Some(Err(_)) => return,
1624					// The incumbent is gone; retry, creating a fresh front.
1625					None => {}
1626				}
1627				continue 'attach;
1628			}
1629		};
1630		let publisher = route.hops.iter().next().copied();
1631
1632		loop {
1633			let update = kio::wait(|waiter| {
1634				// A takeover closes this front without touching its still-live source.
1635				// Observe that first, ahead of any simultaneous route update: a new
1636				// first hop would otherwise re-arm `may_take_over` and win the path
1637				// straight back, which is the eviction loop the flag exists to stop.
1638				// `Err` here is the state channel dying, which also means displaced.
1639				if state
1640					.poll_ref(waiter, |s| if s.closed { Poll::Ready(()) } else { Poll::Pending })
1641					.is_ready()
1642				{
1643					return Poll::Ready(None);
1644				}
1645				source.poll_route_changed(waiter).map(Some)
1646			})
1647			.await;
1648			match update {
1649				None => {
1650					// The winner owns the path now. Stand by behind it, holding the
1651					// ingress announce guard, until it leaves or our route moves again.
1652					may_take_over = false;
1653					continue 'attach;
1654				}
1655				Some(Ok(update)) => {
1656					let announced = update.announce;
1657					// A different first hop is new content: this source can no
1658					// longer feed the front it attached to. Detach deliberately
1659					// (a linger would only stall the replacement) and re-attach.
1660					//
1661					// Plain equality, not `same_publisher`: within one source
1662					// handle the session layer already guarantees continuity (it
1663					// replaces the handle when identity breaks, UNKNOWN restarts
1664					// included), so this only catches a caller moving a live
1665					// handle to a new publisher. An UNKNOWN-to-UNKNOWN metadata
1666					// update (a legacy peer repricing) must not detach.
1667					if update.hops.iter().next().copied() != publisher {
1668						detach_source(&state, &broadcast, &leaf, id, true);
1669						sync_announce(&mut announce, announced, &ingress);
1670						// A new publisher, so this is content no front has beaten yet. A
1671						// sibling source may still hold the old front open, making the
1672						// re-attach a replacement rather than a create.
1673						may_take_over = true;
1674						route = update;
1675						continue 'attach;
1676					}
1677					{
1678						let carrying = broadcast.demand().is_used();
1679						let Ok(mut s) = state.write() else { return };
1680						let Some(entry) = s.routes.iter_mut().find(|r| r.id == id) else {
1681							return;
1682						};
1683						if entry.route == update {
1684							continue;
1685						}
1686						entry.route = update;
1687						s.reselect(carrying);
1688					}
1689					// Toggle the ingress announce guard on a live/offline transition.
1690					sync_announce(&mut announce, announced, &ingress);
1691					sync_front(&state, &broadcast, &leaf);
1692				}
1693				Some(Err(_)) => {
1694					// A deliberate finish closes the front immediately; an abrupt loss
1695					// (dropped producer, dead session) may linger for a replacement.
1696					detach_source(&state, &broadcast, &leaf, id, source.is_finished());
1697					return;
1698				}
1699			}
1700		}
1701	}
1702}
1703
1704/// The outcome of [`attach_source`].
1705enum Attach {
1706	/// The source joined (or created) the front at its path, yielding the shared
1707	/// source table, the spliced broadcast, and the source's table id.
1708	Ready(kio::Producer<FrontState>, broadcast::Producer, u64),
1709	/// The path's live front belongs to a different original publisher and this
1710	/// source may not take it: either the source is offline (so it would rank below
1711	/// every route the front holds), it already spent its takeover attempt on this
1712	/// route and lost, or its chain leads back through a peer the front is already
1713	/// exposed to, making it a reflection rather than rival content. The caller
1714	/// parks on the returned table until the front closes.
1715	Parked(kio::Producer<FrontState>),
1716}
1717
1718/// Everything about a source's attach that does not change between attempts.
1719struct AttachContext<'a> {
1720	origin: &'a Info,
1721	node: &'a Lock<OriginNode>,
1722	/// Absolute path, for the front's identity and log lines.
1723	full: &'a PathOwned,
1724	/// Path relative to `node`, for locating (and later pruning) the leaf.
1725	rest: &'a PathOwned,
1726}
1727
1728/// Whether two sources carry the same content and may therefore splice.
1729///
1730/// [`Origin::UNKNOWN`] identifies nothing: it is what a peer that declared no
1731/// identity, or that does not speak the hops extension at all, contributes as a
1732/// first hop. Two such sources are not interchangeable even though their first
1733/// hops compare equal, so splicing them would cut one publisher's subscribers
1734/// over to an unrelated publisher's content. Every other id compares normally,
1735/// including `None` for a locally produced broadcast with no hops.
1736fn same_publisher(a: Option<Origin>, b: Option<Origin>) -> bool {
1737	if a == Some(Origin::UNKNOWN) || b == Some(Origin::UNKNOWN) {
1738		return false;
1739	}
1740	a == b
1741}
1742
1743/// Attach a source to the broadcast at `leaf`, creating (and publishing) the
1744/// broadcast if none is live. One lock acquisition covers the whole
1745/// join-or-create decision, so concurrent attaches cannot race each other.
1746///
1747/// Joining requires the same content identity (first hop). A source whose
1748/// original publisher differs takes the path over instead: the incumbent front
1749/// closes and a fresh one is created below, so consumers observe an unannounce
1750/// followed by an announce. That mirrors the session layer's rule that a restart
1751/// with a different first hop is a replacement, never a standby, and it is what
1752/// keeps a reconnect from waiting on the transport to retire the session it
1753/// replaced.
1754///
1755/// Taking over requires announcing, which keeps the rule consistent with
1756/// [`route_order`]: an offline source ranks below every announced route, so it
1757/// waits ([`Attach::Parked`]) rather than unannouncing a live broadcast and
1758/// cutting its subscribers for content nobody has advertised. It also requires a
1759/// chain that does not lead back through a peer this front is already exposed to
1760/// (see [`FrontState::taints_a_reader`]): such a source is our own broadcast
1761/// reflected by a peer that cannot detect the loop itself, and letting it evict the
1762/// front is how a publish direction ends up withdrawing its own announce.
1763///
1764/// `may_take_over` is the caller's third gate: [`run_source`] clears it once this
1765/// source has been displaced, so a route that already lost the path stands by
1766/// instead of winning it straight back. Only a new publisher re-arms it, since a
1767/// repricing carries no content the winner has not already beaten.
1768fn attach_source(
1769	ctx: &AttachContext,
1770	leaf: &Lock<OriginNode>,
1771	source: &broadcast::Consumer,
1772	route: broadcast::Route,
1773	may_take_over: bool,
1774) -> Attach {
1775	let publisher = route.hops.iter().next().copied();
1776	let mut leaf_guard = leaf.lock();
1777
1778	// Join the live broadcast if the leaf already has one. A closed one (torn
1779	// down, awaiting teardown, or evicted just below) is replaced instead.
1780	if let Some(existing) = &leaf_guard.broadcast {
1781		let mut joined = None;
1782		let carrying = existing.broadcast.demand().is_used();
1783		if let Ok(mut s) = existing.state.write()
1784			&& !s.closed
1785		{
1786			if same_publisher(s.publisher, publisher) {
1787				let id = s.next_route;
1788				s.next_route += 1;
1789				s.routes.push(FrontRoute {
1790					id,
1791					route: route.clone(),
1792					source: source.clone(),
1793				});
1794				s.reselect(carrying);
1795				joined = Some(id);
1796			} else if !may_take_over || !route.announce || s.taints_a_reader(&route) {
1797				return Attach::Parked(existing.state.clone());
1798			} else {
1799				// New content at a live path: the newest publisher wins it. Closing
1800				// the incumbent here (rather than letting the newcomer wait it out)
1801				// is what makes a reconnect immediate, and routing the takeover
1802				// through the replacement path below keeps the guarantee that
1803				// unrelated content is never spliced into live subscribers. The
1804				// incumbent's own task observes the flag and finishes its teardown,
1805				// finding the leaf slot already taken.
1806				s.closed = true;
1807				tracing::warn!(broadcast = %ctx.full, "replacing a live broadcast from a different publisher");
1808			}
1809		}
1810		if let Some(id) = joined {
1811			let state = existing.state.clone();
1812			let broadcast = existing.broadcast.clone();
1813			drop(leaf_guard);
1814			sync_front(&state, &broadcast, leaf);
1815			return Attach::Ready(state, broadcast, id);
1816		}
1817	}
1818
1819	// First source: create the broadcast and publish it into the tree.
1820	let announce = route.announce;
1821	let broadcast = broadcast::Producer::new_spliced(broadcast::Info {
1822		origin: ctx.origin.clone(),
1823	});
1824	let _ = broadcast.clone().set_route(route.clone());
1825	let state = kio::Producer::new(FrontState {
1826		path: ctx.full.clone(),
1827		self_origin: ctx.origin.id,
1828		publisher,
1829		next_route: 1,
1830		excluded: HashMap::new(),
1831		routes: vec![FrontRoute {
1832			id: 0,
1833			route,
1834			source: source.clone(),
1835		}],
1836		active: Some(0),
1837		linger: ctx.origin.linger,
1838		closed: false,
1839	});
1840
1841	// Replacing a stale (closed) entry counts as an unannounce, so consumers
1842	// observe the replacement rather than a silent swap; its own teardown task
1843	// then finds the slot already taken and leaves it alone.
1844	if let Some(stale) = leaf_guard.broadcast.take()
1845		&& stale.announced
1846	{
1847		leaf_guard.notify.lock().unannounce(&stale.path);
1848	}
1849	let entry = OriginBroadcast {
1850		path: ctx.full.clone(),
1851		broadcast: broadcast.clone(),
1852		state: state.clone(),
1853		announced: announce,
1854	};
1855	if entry.announced {
1856		leaf_guard
1857			.notify
1858			.lock()
1859			.announce(ctx.full, &broadcast.consume(), &state);
1860	}
1861	leaf_guard.broadcast = Some(entry);
1862	drop(leaf_guard);
1863
1864	web_async::spawn(run_front(
1865		state.clone(),
1866		broadcast.clone(),
1867		ctx.node.clone(),
1868		ctx.rest.clone(),
1869	));
1870
1871	Attach::Ready(state, broadcast, 0)
1872}
1873
1874/// Owns a front's lifecycle: dispatches each requested track to a serve task
1875/// until the last source detaches, then unpublishes the broadcast.
1876async fn run_front(
1877	state: kio::Producer<FrontState>,
1878	mut broadcast: broadcast::Producer,
1879	node: Lock<OriginNode>,
1880	rest: PathOwned,
1881) {
1882	enum Step {
1883		Serve(Arc<str>, super::resume::Producer),
1884		/// The source table emptied or refilled: re-arm the linger countdown.
1885		Changed,
1886		/// The linger window expired: close if the table is still empty.
1887		Expired,
1888		Closed,
1889	}
1890
1891	let linger = state.read().linger;
1892	// Armed while the table is ungracefully empty: the instant the front gives up
1893	// waiting for a replacement source. A graceful close never gets here (the
1894	// detach sets `closed` synchronously), so a running countdown always means a
1895	// reconnect is welcome.
1896	let mut deadline = kio::time::Deadline::new();
1897
1898	loop {
1899		let empty = {
1900			let s = state.read();
1901			!s.closed && s.routes.is_empty()
1902		};
1903		deadline.set(match (empty, deadline.deadline()) {
1904			// An unrepresentable deadline (e.g. `Duration::MAX`) lingers forever:
1905			// no timer, only a re-attach or teardown moves the front on.
1906			(true, None) => web_async::time::Instant::now().checked_add(linger),
1907			(true, at) => at,
1908			(false, _) => None,
1909		});
1910
1911		let step = {
1912			kio::wait(|waiter| {
1913				if let Poll::Ready((name, resume)) = broadcast.poll_spliced_assigned(waiter) {
1914					return Poll::Ready(Step::Serve(name, resume));
1915				}
1916				// Watch for the close, and for the table changing shape so the
1917				// countdown re-arms (a detach emptying it, a reconnect refilling it).
1918				match state.poll(waiter, |s| {
1919					if s.closed || s.routes.is_empty() != empty {
1920						Poll::Ready(())
1921					} else {
1922						Poll::Pending
1923					}
1924				}) {
1925					Poll::Ready(Ok(guard)) => {
1926						return Poll::Ready(if guard.closed { Step::Closed } else { Step::Changed });
1927					}
1928					Poll::Ready(Err(_)) => return Poll::Ready(Step::Closed),
1929					Poll::Pending => {}
1930				}
1931				deadline.poll(waiter).map(|_| Step::Expired)
1932			})
1933			.await
1934		};
1935
1936		match step {
1937			Step::Serve(name, resume) => {
1938				// Serve tasks self-terminate when the track completes or the
1939				// front closes.
1940				web_async::spawn(serve_track(state.clone(), name, resume));
1941			}
1942			Step::Changed => {}
1943			Step::Expired => {
1944				// Close only if the table is still empty: a source that re-attached
1945				// as the window expired wins the write-lock race and keeps the
1946				// broadcast alive.
1947				let close = {
1948					let Ok(mut s) = state.write() else { break };
1949					if !s.closed && s.routes.is_empty() {
1950						s.closed = true;
1951						true
1952					} else {
1953						false
1954					}
1955				};
1956				if close {
1957					break;
1958				}
1959			}
1960			Step::Closed => break,
1961		}
1962	}
1963
1964	// Abort the logical tracks (releasing their subscribers) and unpublish.
1965	broadcast.abort_spliced(Error::Dropped);
1966
1967	// Deliberate end; suppresses the dropped-without-finish warning.
1968	broadcast.finish();
1969
1970	// Remove the broadcast from the tree (identity-checked, so a replacement is
1971	// untouched) and prune empty nodes.
1972	node.lock().remove(&state, &rest);
1973}
1974
1975/// Serves one spliced logical track: splices in the best source's copy of the
1976/// track, re-splicing on handover or failure, until the track completes or the
1977/// front closes. A refusal (a source rejecting the track, or its copy dying
1978/// before delivering anything) is authoritative and never retried: the refuser
1979/// is skipped for this track so a joining standby cannot kill a subscription
1980/// the incumbent is serving, and once every attached source has refused, the
1981/// track aborts with the last refusal's error. The verdict belongs to this
1982/// request; a later consumer request asks afresh (see `track_inner`). Failures
1983/// after delivered progress (a serving session dying mid-stream) are normal
1984/// failover and re-splice from the next source at the first missing group; a
1985/// source that fails while *closing* is a corpse to fail over past (its watcher
1986/// is about to detach it), not a verdict on the track.
1987async fn serve_track(state: kio::Producer<FrontState>, name: Arc<str>, mut resume: super::resume::Producer) {
1988	enum Step {
1989		Closed,
1990		Splice(u64, broadcast::Consumer),
1991		Complete,
1992		Failed(Error),
1993		/// The route we were serving from left the table with nothing servable to
1994		/// replace it: drop our handle; the verdict block at the top of the loop
1995		/// decides between aborting (all refused) and parking (corpses detaching,
1996		/// or an empty table awaiting a reconnect).
1997		NoRoute,
1998		/// The linger expired with the track still unread: release the segment.
1999		Idle,
2000		/// A reader arrived or the last one left: recompute the demand gate.
2001		Demand,
2002	}
2003
2004	// The source whose copy is currently spliced in, and that copy.
2005	let mut serving: Option<(u64, track::Consumer)> = None;
2006	// The delivered edge when that copy spliced in. A copy that dies without
2007	// advancing it never delivered anything, which is what [`Step::Failed`]
2008	// uses to tell a refusal from a mid-stream failover. Snapshotted per splice,
2009	// not per wake: an unrelated wake between the copy's last group and its
2010	// death must not launder its delivered progress away.
2011	let mut spliced_edge: Option<u64> = None;
2012	// Sources that refused this track, and the most recent refusal's error. A
2013	// standby joining a live front wins dispatch the moment it attaches, which is
2014	// before a real publisher has created every track, so its refusal must cost
2015	// the incumbent nothing: we keep serving from a route that has the track.
2016	let mut refused: HashSet<u64> = HashSet::new();
2017	let mut refusal: Option<Error> = None;
2018	// Sources whose splice failed because they had already closed. Their watchers
2019	// are about to detach them, so wait for the table to move on rather than
2020	// treating a corpse's error as a refusal (ids are never reused, so this
2021	// cannot wedge).
2022	let mut dead: HashSet<u64> = HashSet::new();
2023	// When the spliced segment stopped being read, starting the release countdown.
2024	let mut idle_since: Option<web_async::time::Instant> = None;
2025	let mut deadline = kio::time::Deadline::new();
2026
2027	loop {
2028		let serving_id = serving.as_ref().map(|(id, _)| *id);
2029
2030		// The table's verdict: once every attached source has refused, nothing
2031		// will ever serve the track (refusals are never retried) and it aborts
2032		// with the last refusal's error. A detached refuser leaves the set, so a
2033		// source that reattaches (under a fresh id) is asked anew; a table blocked
2034		// only by corpses awaiting detach parks instead, since their replacement
2035		// (a reconnect) deserves the seamless splice.
2036		{
2037			let s = state.read();
2038			refused.retain(|id| s.routes.iter().any(|r| r.id == *id));
2039			dead.retain(|id| s.routes.iter().any(|r| r.id == *id));
2040			let exhausted = !s.routes.is_empty()
2041				&& s.serve_route(|id| refused.contains(&id) || dead.contains(&id))
2042					.is_none();
2043			if exhausted && dead.is_empty() {
2044				drop(s);
2045				let err = refusal.take().unwrap_or(Error::NotFound);
2046				tracing::debug!(name = %name, %err, "every source refused track; aborting");
2047				let _ = resume.abort(err);
2048				return;
2049			}
2050		}
2051
2052		// Demand gates both directions: an unread track never splices a source in,
2053		// and a spliced one is released once the linger expires. Both sides use the
2054		// same signal, so a release can't immediately re-splice and spin.
2055		//
2056		// The countdown keys off the segment, not our handle on the route that
2057		// produced it: a route that leaves (or a copy that dies) drops the handle
2058		// while the segment stays spliced, and that segment is exactly what the
2059		// release exists to reclaim. Keying off the handle strands it until the front
2060		// closes, which pins the departed source's cached groups for a linger that
2061		// may never expire and leaves a dead segment's edge behind for the next
2062		// takeover to splice above.
2063		let used = resume.is_used();
2064		idle_since = match (resume.is_spliced(), used) {
2065			(true, false) => idle_since.or_else(|| Some(web_async::time::Instant::now())),
2066			_ => None,
2067		};
2068		deadline.set(idle_since.and_then(|at| at.checked_add(TRACK_IDLE_LINGER)));
2069
2070		let step = {
2071			let skip = |id: u64| refused.contains(&id) || dead.contains(&id);
2072			kio::wait(|waiter| {
2073				// Watch the source table: the front closing, a better servable
2074				// source than the one spliced in (skipping any we already know
2075				// can't serve this track), or the served route leaving the table,
2076				// which retires the refusals collected against it. Splicing waits
2077				// for a reader.
2078				match state.poll(waiter, |s| {
2079					let gone = serving_id.is_some_and(|id| !s.routes.iter().any(|r| r.id == id));
2080					if s.closed
2081						|| (used && (gone || matches!(s.serve_route(skip), Some(next) if Some(next) != serving_id)))
2082					{
2083						Poll::Ready(())
2084					} else {
2085						Poll::Pending
2086					}
2087				}) {
2088					Poll::Ready(Ok(guard)) => {
2089						if guard.closed {
2090							return Poll::Ready(Step::Closed);
2091						}
2092						let Some(next) = guard.serve_route(skip) else {
2093							return Poll::Ready(Step::NoRoute);
2094						};
2095						let source = guard
2096							.routes
2097							.iter()
2098							.find(|r| r.id == next)
2099							.expect("servable source in table")
2100							.source
2101							.clone();
2102						return Poll::Ready(Step::Splice(next, source));
2103					}
2104					Poll::Ready(Err(_)) => return Poll::Ready(Step::Closed),
2105					Poll::Pending => {}
2106				}
2107
2108				// Watch the demand edge in whichever direction is unmet. This has to end
2109				// the wait, not just wake it: `used` and the countdown are computed by
2110				// the outer loop, so a wake that stayed inside would re-poll with the
2111				// stale value and never arm (or cancel) the linger.
2112				let edge = match used {
2113					true => resume.poll_unused(waiter),
2114					false => resume.poll_used(waiter),
2115				};
2116				if edge.is_ready() {
2117					return Poll::Ready(Step::Demand);
2118				}
2119
2120				// Watch the spliced copy for its end: complete means the logical
2121				// track is over; anything else means the serving copy died.
2122				if let Some((_, track)) = &serving
2123					&& let Poll::Ready(result) = track.poll_complete(waiter)
2124				{
2125					return Poll::Ready(match result {
2126						Ok(()) => Step::Complete,
2127						Err(err) => Step::Failed(err),
2128					});
2129				}
2130
2131				deadline.poll(waiter).map(|_| Step::Idle)
2132			})
2133			.await
2134		};
2135
2136		match step {
2137			// The front's teardown aborts the logical track.
2138			Step::Closed => return,
2139			Step::Complete => {
2140				let _ = resume.finish();
2141				return;
2142			}
2143			Step::Failed(err) => {
2144				// The spliced copy died mid-serve. With delivered progress since
2145				// its splice it's a normal failover: re-splice from the (possibly
2146				// same) active source. A copy that died before producing anything
2147				// is a refusal (a source whose track keeps dying right after
2148				// acceptance must not re-splice forever), unless the source
2149				// itself is closing: that corpse parks for its detach so a
2150				// reconnect gets the seamless splice.
2151				if resume.latest() == spliced_edge
2152					&& let Some(id) = serving_id
2153				{
2154					let closing = state
2155						.read()
2156						.routes
2157						.iter()
2158						.find(|r| r.id == id)
2159						.is_some_and(|r| r.source.is_closing());
2160					if closing {
2161						dead.insert(id);
2162					} else {
2163						refused.insert(id);
2164						refusal = Some(err);
2165					}
2166				}
2167				serving = None;
2168			}
2169			// The outer loop recomputes `used` and the countdown on the next pass.
2170			Step::Demand => {}
2171			// Forget which route we were serving from, or the `gone` edge that woke
2172			// us keeps firing: the id stays absent from the table, the wait returns
2173			// Ready at once, and the loop spins on a full core without ever parking.
2174			// The segment itself stays spliced into `resume` (readers keep whatever
2175			// it delivered) until a replacement is proven servable.
2176			Step::NoRoute => serving = None,
2177			Step::Idle => {
2178				// Nobody has read the track for the linger: drop the source's copy so
2179				// its session can release the track (and the cached `track::Info` that
2180				// came with it). The logical track stays alive and re-splices on the
2181				// next reader, so a returning viewer or a follow-up fetch resumes.
2182				if resume.release().is_err() {
2183					// Finished or aborted meanwhile; the track is over either way.
2184					return;
2185				}
2186				serving = None;
2187			}
2188			Step::Splice(id, source) => {
2189				// Ask the source for its copy and wait for the info to resolve,
2190				// proving it servable, before splicing it in. Bail out early if
2191				// the table moves on while waiting.
2192				let attempt = match source.track(&name) {
2193					Ok(track) => {
2194						// `into_inner` sheds the `Pending` future wrapper so only
2195						// the pollable (which is `Sync`) is held across the await.
2196						let query = track.info().into_inner();
2197						let skip = |id: u64| refused.contains(&id) || dead.contains(&id);
2198						let info = kio::wait(|waiter| {
2199							if let Poll::Ready(result) = query.poll(waiter) {
2200								return Poll::Ready(Some(result));
2201							}
2202							match state.poll(waiter, |s| {
2203								if s.closed || s.serve_route(skip) != Some(id) {
2204									Poll::Ready(())
2205								} else {
2206									Poll::Pending
2207								}
2208							}) {
2209								Poll::Ready(_) => Poll::Ready(None),
2210								Poll::Pending => Poll::Pending,
2211							}
2212						})
2213						.await;
2214						match info {
2215							// The table changed under us; re-pick from the top.
2216							None => continue,
2217							// A copy that is already aborted can't be spliced;
2218							// its error is the source's answer for the track.
2219							Some(Ok(_)) => match track.poll_complete(&kio::Waiter::noop()) {
2220								Poll::Ready(Err(err)) => Err(err),
2221								_ => Ok(track),
2222							},
2223							Some(Err(err)) => Err(err),
2224						}
2225					}
2226					Err(err) => Err(err),
2227				};
2228
2229				match attempt {
2230					Ok(track) => {
2231						if let Err(err) = resume.takeover(&track) {
2232							// Closed means the logical track already ended
2233							// (finished or aborted). Anything else is a boundary
2234							// bug; abort rather than strand subscribers on a
2235							// track no task serves (a no-op after a clean end).
2236							let _ = resume.abort(err);
2237							return;
2238						}
2239						// `dead` must survive the takeover. A dead source can never
2240						// serve again (ids are never reused; `is_closing` is
2241						// terminal), and the retain above reclaims its entry once
2242						// its watcher detaches it. Re-admitting a still-attached
2243						// closing route here would let `serve_route`'s active
2244						// preference re-dispatch it, and because both its instant
2245						// failure and a cached standby splice resolve without
2246						// awaiting, the loop would spin inside a single poll,
2247						// starving the watcher whose detach ends the cycle.
2248						// The new segment has produced nothing yet, so this is
2249						// the edge the copy is asked to advance.
2250						spliced_edge = resume.latest();
2251						serving = Some((id, track));
2252					}
2253					// The source itself closed or deliberately ended: not a
2254					// verdict on the track. Park until its watcher detaches
2255					// it and the table promotes a replacement.
2256					Err(_) if source.is_closing() => {
2257						dead.insert(id);
2258						serving = None;
2259					}
2260					// The dispatched source does not carry the track: a publisher
2261					// announces a broadcast only once its tracks exist, so the
2262					// answer is authoritative and never re-asked. Skip the source
2263					// for this track; the verdict block aborts once every source
2264					// has refused.
2265					Err(err) => {
2266						tracing::debug!(name = %name, source = id, %err, "source refused track");
2267						refused.insert(id);
2268						refusal = Some(err);
2269						serving = None;
2270					}
2271				}
2272			}
2273		}
2274	}
2275}
2276
2277/// Shared fallback request queue for an origin.
2278///
2279/// Lives off to the side of the announce tree because dynamically served broadcasts
2280/// are never announced. Carried in a [`kio::Shared`], so consumers enqueue and handlers
2281/// drain under one lock. Mirrors the fetch state of the track model.
2282#[derive(Default)]
2283struct OriginDynamicState {
2284	// Result channels for pending requests, keyed by absolute path so concurrent
2285	// `request_broadcast` calls for the same path coalesce onto one channel.
2286	requests: Requests<PathOwned, kio::Producer<PendingBroadcast>>,
2287
2288	// Broadcasts a handler has already served, kept weakly so a repeat request for the
2289	// same path resolves to a shared clone instead of re-invoking the handler (which would
2290	// open a duplicate upstream subscription). Weak so a served broadcast still closes once
2291	// its real consumers drop. The cache reclaims closed entries incrementally on insert, so a
2292	// long-lived origin serving many distinct one-shot paths stays bounded by the live count.
2293	served: WeakCache<PathOwned, broadcast::WeakConsumer>,
2294}
2295
2296/// One-shot result of a dynamic broadcast request.
2297///
2298/// Stays `None` until a handler [`accept`](Request::accept)s (yielding the served
2299/// broadcast) or [`reject`](Request::reject)s (yielding an error). The producer is
2300/// dropped right after writing, closing the channel; kio checks the value before the closed
2301/// flag, so an awaiting requester still observes the final result.
2302#[derive(Default)]
2303struct PendingBroadcast {
2304	resolved: Option<Result<broadcast::Consumer, Error>>,
2305}
2306
2307/// Picks up [`Consumer::request_broadcast`] calls for paths that are not announced.
2308///
2309/// The origin-level analogue of [`broadcast::Dynamic`]: where that serves tracks on
2310/// demand within a broadcast, this serves whole broadcasts on demand within an origin. A
2311/// relay uses it as a fallback router, fetching a broadcast from upstream only when a
2312/// downstream consumer asks for an exact path that nobody announced.
2313///
2314/// Served broadcasts are deliberately *not* announced, so they never appear in
2315/// [`Consumer::announced`]. Drop this handle (and every clone) to reject the
2316/// requests still waiting to be served.
2317pub struct Dynamic {
2318	info: Origin,
2319	root: PathOwned,
2320	state: kio::Shared<OriginDynamicState>,
2321}
2322
2323impl Clone for Dynamic {
2324	fn clone(&self) -> Self {
2325		// Mirror `new`: count each live handle. Without this, dropping a clone would
2326		// decrement past `new`'s increment and prematurely flip the handler count to
2327		// zero, making future `request_broadcast` calls return `Unroutable`.
2328		self.state.lock().requests.add_handler();
2329
2330		Self {
2331			info: self.info,
2332			root: self.root.clone(),
2333			state: self.state.clone(),
2334		}
2335	}
2336}
2337
2338impl Dynamic {
2339	fn new(info: Origin, root: PathOwned, state: kio::Shared<OriginDynamicState>) -> Self {
2340		state.lock().requests.add_handler();
2341
2342		Self { info, root, state }
2343	}
2344
2345	/// The origin this handler belongs to.
2346	pub fn info(&self) -> &Origin {
2347		&self.info
2348	}
2349
2350	/// Poll for the next requested broadcast, without blocking.
2351	pub fn poll_requested_broadcast(&mut self, waiter: &kio::Waiter) -> Poll<Result<Request, Error>> {
2352		let mut state = ready!(self.state.poll(waiter, |state| {
2353			if state.requests.has_queued() {
2354				Poll::Ready(())
2355			} else {
2356				Poll::Pending
2357			}
2358		}));
2359
2360		let path = state.requests.pop().expect("predicate guaranteed a request");
2361		// The popped request stays pending, so a repeat request in the window between
2362		// hand-off and accept coalesces onto it instead of re-invoking the handler. The
2363		// producer is a shared clone; `Request::{accept, reject, drop}` removes the
2364		// entry. This mirrors how `poll_requested_track` keeps a served track
2365		// discoverable via the weak cache across the same window.
2366		let producer = state.requests.get(&path).expect("popped key must be pending").clone();
2367		Poll::Ready(Ok(Request {
2368			path,
2369			producer,
2370			state: self.state.clone(),
2371		}))
2372	}
2373
2374	/// Block until a consumer requests an unannounced broadcast, returning a
2375	/// [`Request`] to serve.
2376	pub async fn requested_broadcast(&mut self) -> Result<Request, Error> {
2377		kio::wait(|waiter| self.poll_requested_broadcast(waiter)).await
2378	}
2379
2380	/// Returns the prefix that is automatically stripped from requested paths.
2381	pub fn root(&self) -> &Path<'_> {
2382		&self.root
2383	}
2384}
2385
2386impl Drop for Dynamic {
2387	fn drop(&mut self) {
2388		// Decrement and reject under one lock, so a `request_broadcast` that saw a
2389		// live handler through the same lock can't slip a request past the rejection.
2390		let mut state = self.state.lock();
2391		if state.requests.remove_handler() {
2392			// No handlers left to pop queued requests; drop them, closing their result
2393			// channels so awaiting requesters resolve to `Unroutable`. A request already
2394			// handed to a handler stays, resolved by its `Request` instead.
2395			state.requests.drain_queued();
2396		}
2397	}
2398}
2399
2400/// A pending request for a broadcast that was not announced.
2401///
2402/// Yielded by [`Dynamic::requested_broadcast`]. The requester is awaiting inside
2403/// [`Consumer::request_broadcast`]; [`accept`](Self::accept) resolves it with a live
2404/// broadcast (which the handler keeps producing into) and [`reject`](Self::reject) resolves
2405/// it with an error. Dropping the request without either rejects it.
2406pub struct Request {
2407	// Absolute path that was requested.
2408	path: PathOwned,
2409
2410	// Result channel back to the awaiting requester(s). Writing `resolved` and dropping
2411	// this wakes them with the outcome.
2412	producer: kio::Producer<PendingBroadcast>,
2413
2414	// Shared dynamic state, so `accept` can cache the served broadcast for repeat requests.
2415	state: kio::Shared<OriginDynamicState>,
2416}
2417
2418impl Request {
2419	/// The absolute path that was requested.
2420	pub fn path(&self) -> &Path<'_> {
2421		&self.path
2422	}
2423
2424	/// Accept the request, resolving every awaiting requester with `broadcast`.
2425	///
2426	/// The caller keeps producing into `broadcast` (e.g. a relay proxying tracks from
2427	/// upstream); the requesters receive a consumer for it. The broadcast is *not*
2428	/// announced.
2429	pub fn accept(self, broadcast: impl Consume<broadcast::Consumer>) {
2430		let broadcast = broadcast.consume();
2431
2432		// Move the entry out of the in-flight queue and into the weak `served` cache, so repeat
2433		// requests for this path share the same broadcast instead of asking the handler to serve
2434		// (and subscribe upstream) again. Re-check under the lock: if a live broadcast was already
2435		// served for this path while we were fetching upstream, dedup onto it and drop ours rather
2436		// than replace a good entry with a duplicate subscription.
2437		let resolved = {
2438			let mut state = self.state.lock();
2439			let existing = state.served.insert(self.path.clone(), broadcast.weak());
2440			state
2441				.requests
2442				.remove_if(&self.path, |producer| producer.same_channel(&self.producer));
2443			existing.map(|weak| weak.consume()).unwrap_or(broadcast)
2444		};
2445
2446		if let Ok(mut pending) = self.producer.write() {
2447			pending.resolved = Some(Ok(resolved));
2448		}
2449		// `self.producer` drops here, closing the channel; the value is still observable.
2450	}
2451
2452	/// Reject the request, resolving every awaiting requester with `err`.
2453	pub fn reject(self, err: Error) {
2454		self.state
2455			.lock()
2456			.requests
2457			.remove_if(&self.path, |producer| producer.same_channel(&self.producer));
2458		if let Ok(mut state) = self.producer.write() {
2459			state.resolved = Some(Err(err));
2460		}
2461	}
2462}
2463
2464impl Drop for Request {
2465	fn drop(&mut self) {
2466		// Handed off but neither accepted nor rejected: drop the still-pending entry so its
2467		// producer clone (plus this one) closes the channel, resolving coalesced requesters to
2468		// `Unroutable` rather than hanging.
2469		//
2470		// The identity guard matters: `accept`/`reject` already removed our entry and released
2471		// the lock before we run, so a concurrent request for the same path may have registered
2472		// a *new* one here. Removing unconditionally would clobber it, stranding its requesters.
2473		self.state
2474			.lock()
2475			.requests
2476			.remove_if(&self.path, |producer| producer.same_channel(&self.producer));
2477	}
2478}
2479
2480/// The pollable result of [`Consumer::request_broadcast`].
2481///
2482/// Awaited via the [`kio::Pending`] wrapper; resolves to the [`broadcast::Consumer`]
2483/// immediately when the broadcast was already announced, or once an [`Dynamic`]
2484/// handler serves the request. Resolves to an error if the request is rejected or every
2485/// handler drops before serving it.
2486pub struct Requesting {
2487	inner: RequestState,
2488	// Egress scope applied to the resolved broadcast, so its reads are attributed.
2489	// Empty (no-op) for an untagged consumer.
2490	stats: stats::Scope,
2491}
2492
2493enum RequestState {
2494	// Already announced: resolves immediately with a clone of this broadcast.
2495	Ready(broadcast::Consumer),
2496	// Unroutable at request time: resolves immediately with this error. Baked in so
2497	// `request_broadcast` itself stays infallible.
2498	Failed(Error),
2499	// Awaiting a handler: resolves when the request's result channel is written.
2500	Pending(kio::Consumer<PendingBroadcast>),
2501}
2502
2503impl Requesting {
2504	fn ready(broadcast: broadcast::Consumer) -> Self {
2505		Self {
2506			inner: RequestState::Ready(broadcast),
2507			stats: stats::Scope::default(),
2508		}
2509	}
2510
2511	fn failed(error: Error) -> Self {
2512		Self {
2513			inner: RequestState::Failed(error),
2514			stats: stats::Scope::default(),
2515		}
2516	}
2517
2518	fn pending(consumer: kio::Consumer<PendingBroadcast>) -> Self {
2519		Self {
2520			inner: RequestState::Pending(consumer),
2521			stats: stats::Scope::default(),
2522		}
2523	}
2524
2525	fn with_stats(mut self, scope: stats::Scope) -> Self {
2526		self.stats = scope;
2527		self
2528	}
2529
2530	/// Poll for the requested broadcast without blocking.
2531	pub fn poll_ok(&self, waiter: &kio::Waiter) -> Poll<Result<broadcast::Consumer, Error>> {
2532		match &self.inner {
2533			RequestState::Ready(broadcast) => Poll::Ready(Ok(broadcast.clone().with_stats(self.stats.clone()))),
2534			RequestState::Failed(error) => Poll::Ready(Err(error.clone())),
2535			RequestState::Pending(consumer) => Poll::Ready(
2536				match ready!(consumer.poll(waiter, |state| match &state.resolved {
2537					Some(result) => Poll::Ready(result.clone()),
2538					None => Poll::Pending,
2539				})) {
2540					Ok(result) => result.map(|broadcast| broadcast.with_stats(self.stats.clone())),
2541					// Every handler dropped without resolving: nobody could route it.
2542					Err(_closed) => Err(Error::Unroutable),
2543				},
2544			),
2545		}
2546	}
2547}
2548
2549impl kio::Pollable for Requesting {
2550	type Output = Result<broadcast::Consumer, Error>;
2551
2552	fn poll(&self, waiter: &kio::Waiter) -> Poll<Self::Output> {
2553		self.poll_ok(waiter)
2554	}
2555}
2556
2557/// Derive a read view from a handle.
2558///
2559/// Lets APIs accept either a producer or a consumer (e.g.
2560/// [`Client::with_publisher`](crate::Client::with_publisher),
2561/// [`Request::accept`]). The blanket `&T` impl means you can
2562/// pass by value (`foo(x)`) to hand off ownership, or by reference (`foo(&x)`)
2563/// to keep it, without spelling out `.consume()`.
2564pub trait Consume<T> {
2565	/// Derive a read view (a consumer) from this handle.
2566	fn consume(&self) -> T;
2567}
2568
2569impl<T, U: Consume<T>> Consume<T> for &U {
2570	fn consume(&self) -> T {
2571		(**self).consume()
2572	}
2573}
2574
2575impl Consume<Consumer> for Producer {
2576	fn consume(&self) -> Consumer {
2577		// Mirrors the inherent `Producer::consume`; inlined to avoid the
2578		// inherent-vs-trait `consume` ambiguity. Untagged: egress is tagged
2579		// separately from ingress.
2580		Consumer::new(
2581			self.info,
2582			self.root.clone(),
2583			self.nodes.clone(),
2584			self.dynamic.clone(),
2585			stats::Session::default(),
2586		)
2587	}
2588}
2589
2590impl Consume<Consumer> for Consumer {
2591	fn consume(&self) -> Consumer {
2592		self.clone()
2593	}
2594}
2595
2596impl Consume<broadcast::Consumer> for broadcast::Producer {
2597	fn consume(&self) -> broadcast::Consumer {
2598		// The inherent `consume` shadows this trait method, so this delegates.
2599		self.consume()
2600	}
2601}
2602
2603impl Consume<broadcast::Consumer> for broadcast::Consumer {
2604	fn consume(&self) -> broadcast::Consumer {
2605		self.clone()
2606	}
2607}
2608
2609impl Consume<track::Consumer> for track::Producer {
2610	fn consume(&self) -> track::Consumer {
2611		self.consume()
2612	}
2613}
2614
2615impl Consume<track::Consumer> for track::Consumer {
2616	fn consume(&self) -> track::Consumer {
2617		self.clone()
2618	}
2619}
2620
2621/// Cheap read handle over an origin's broadcast tree.
2622///
2623/// Clones share the underlying tree state without allocating any per-cursor
2624/// resources. To actually receive announce / unannounce events, call
2625/// [`Self::announced`] to obtain an [`AnnounceConsumer`].
2626#[derive(Clone)]
2627pub struct Consumer {
2628	// Identity of the origin this consumer was derived from.
2629	info: Origin,
2630	nodes: OriginNodes,
2631
2632	// A prefix that is automatically stripped from all paths.
2633	root: PathOwned,
2634
2635	// Shared fallback request queue, fed to any `Dynamic` handler on the
2636	// producer side. Used only by `request_broadcast`; announced lookups ignore it.
2637	dynamic: kio::Shared<OriginDynamicState>,
2638
2639	// Egress stats context. Broadcasts handed out through this consumer (and any
2640	// handle derived from them) are attributed to it (reads counted on the
2641	// publisher/egress side). Empty (no-op) unless a session tagged this handle.
2642	stats: stats::Session,
2643
2644	// Data-plane split horizon: broadcasts resolved through this handle are
2645	// served from a source whose hop chain excludes this origin (the requesting
2646	// peer). `None` (the default) serves from the active source as usual.
2647	exclude: Option<Origin>,
2648}
2649
2650impl std::ops::Deref for Consumer {
2651	type Target = Origin;
2652
2653	fn deref(&self) -> &Self::Target {
2654		&self.info
2655	}
2656}
2657
2658impl Consumer {
2659	fn new(
2660		info: Origin,
2661		root: PathOwned,
2662		nodes: OriginNodes,
2663		dynamic: kio::Shared<OriginDynamicState>,
2664		stats: stats::Session,
2665	) -> Self {
2666		Self {
2667			info,
2668			nodes,
2669			root,
2670			dynamic,
2671			stats,
2672			exclude: None,
2673		}
2674	}
2675
2676	/// A clone that never serves the given peer its own data: broadcasts resolve
2677	/// to a source whose hop chain excludes `peer`, matching what the announce
2678	/// loop advertises to them. Sessions apply this once they learn the peer's
2679	/// origin id.
2680	pub(crate) fn excluding(mut self, peer: Origin) -> Self {
2681		self.exclude = Some(peer);
2682		self
2683	}
2684
2685	/// Attach an egress stats context: broadcasts handed out through this handle (and
2686	/// any handle derived from it) are attributed to `session` on the publisher
2687	/// (egress) side. Pass [`stats::Session::default`] to opt out.
2688	pub fn with_stats(mut self, session: stats::Session) -> Self {
2689		self.stats = session;
2690		self
2691	}
2692
2693	/// A clone of this consumer with its stats context cleared, so an internal
2694	/// lookup stream (e.g. [`Self::announced_broadcast`]) doesn't drive the egress
2695	/// announce guards; the caller re-attributes the result itself.
2696	fn untagged(&self) -> Self {
2697		Self {
2698			stats: stats::Session::default(),
2699			..self.clone()
2700		}
2701	}
2702
2703	/// A view with this consumer's identity and root but no broadcasts:
2704	/// [`announced`](Self::announced) yields nothing. Used to answer a peer's
2705	/// announce-interest for a prefix outside our scope by announcing nothing,
2706	/// rather than tearing the stream down.
2707	pub(crate) fn empty(&self) -> Self {
2708		Self {
2709			info: self.info,
2710			nodes: OriginNodes { nodes: Vec::new() },
2711			root: self.root.clone(),
2712			dynamic: self.dynamic.clone(),
2713			stats: self.stats.clone(),
2714			exclude: self.exclude,
2715		}
2716	}
2717
2718	/// Subscribe to announce / unannounce events for this consumer's subtree.
2719	///
2720	/// Allocates a per-cursor coalescing buffer, registers it with each root
2721	/// in this consumer's scope, and replays the currently active broadcast
2722	/// set as initial announcements. Drop the returned [`AnnounceConsumer`]
2723	/// to unregister.
2724	pub fn announced(&self) -> AnnounceConsumer {
2725		AnnounceConsumer::new(self.root.clone(), self.nodes.clone(), self.stats.clone(), self.exclude)
2726	}
2727
2728	/// Returns a cheap duplicate of this read handle.
2729	pub fn consume(&self) -> Self {
2730		self.clone()
2731	}
2732
2733	/// Internal synchronous lookup: how the broadcast at `path` resolves for this
2734	/// consumer, telling "announced but every route loops back through you" apart
2735	/// from "nothing here".
2736	///
2737	/// Races announcement gossip (a freshly-connected consumer sees `Missing` even when
2738	/// the broadcast is about to arrive), so it is not public. [`Self::request_broadcast`]
2739	/// is the public lookup: it builds on this for the announced case, then falls back to
2740	/// a dynamic handler. [`Self::announced_broadcast`] waits for a future announcement.
2741	fn resolve(&self, path: impl AsPath) -> Resolved {
2742		let path = path.as_path();
2743		let Some((root, rest)) = self.nodes.get(&path) else {
2744			return Resolved::Missing;
2745		};
2746		let state = root.lock();
2747		state.resolve_broadcast(&rest, self.exclude)
2748	}
2749
2750	/// [`Self::resolve`] reduced to "can I read it": the peek the tests assert on.
2751	#[cfg(test)]
2752	pub(crate) fn get_broadcast(&self, path: impl AsPath) -> Option<broadcast::Consumer> {
2753		match self.resolve(path) {
2754			Resolved::Found(broadcast) => Some(broadcast),
2755			Resolved::Excluded | Resolved::Missing => None,
2756		}
2757	}
2758
2759	/// Block until a broadcast with the given path is announced and return it.
2760	///
2761	/// Returns `None` if the path is outside this consumer's allowed prefixes or if the consumer
2762	/// is closed before the broadcast is announced. The returned broadcast may itself be closed
2763	/// later. Subscribers should watch [`broadcast::Consumer::closed`] to react to that.
2764	///
2765	/// Prefer this over [`Self::request_broadcast`] when you know the exact path you want but
2766	/// cannot guarantee the announcement has already been received. With moq-lite-05 (and
2767	/// the older Lite01/02) `connect()` already blocks until the initial announce set lands,
2768	/// so [`Self::request_broadcast`] is race-free for broadcasts that were live at connect time;
2769	/// this method is still needed to wait for a broadcast that comes online *after* connect.
2770	pub async fn announced_broadcast(&self, path: impl AsPath) -> Option<broadcast::Consumer> {
2771		let path = path.as_path();
2772
2773		// Scope a fresh consumer down to this path so we only wake up for relevant announcements.
2774		let consumer = self.scope(std::slice::from_ref(&path))?;
2775
2776		// `scope` keeps narrower permissions intact: if we ask for `foo` on a consumer limited
2777		// to `foo/specific`, `scope` returns a consumer scoped to `foo/specific`. No
2778		// announcement at the exact path `foo` can ever arrive. Bail rather than loop forever.
2779		if !consumer.allowed().any(|allowed| path.has_prefix(allowed)) {
2780			return None;
2781		}
2782
2783		// Use an untagged stream: this is a lookup, not egress announce forwarding, so
2784		// it must not drive the announce guards. The matched result is attributed
2785		// with the egress scope instead.
2786		let mut announced = consumer.untagged().announced();
2787		let scope = self.stats.egress(self.root.join(&path).to_owned());
2788		loop {
2789			let OriginAnnounce {
2790				path: announced_path,
2791				broadcast,
2792			} = announced.next().await?;
2793			// `scope` narrows by prefix, but we only want an exact-path match.
2794			if announced_path.as_path() == path
2795				&& let Some(broadcast) = broadcast
2796			{
2797				return Some(broadcast.with_stats(scope));
2798			}
2799		}
2800	}
2801
2802	/// Returns a new Consumer restricted to broadcasts under one of `prefixes`.
2803	///
2804	/// Returns None if there are no legal prefixes (the requested prefixes are
2805	/// disjoint from this consumer's current scope, so it would always return None).
2806	// TODO accept PathPrefixes instead of &[Path]
2807	pub fn scope(&self, prefixes: &[Path]) -> Option<Consumer> {
2808		let prefixes = PathPrefixes::new(prefixes);
2809		Some(Consumer {
2810			info: self.info,
2811			root: self.root.clone(),
2812			nodes: self.nodes.select(&prefixes)?,
2813			dynamic: self.dynamic.clone(),
2814			stats: self.stats.clone(),
2815			exclude: self.exclude,
2816		})
2817	}
2818
2819	/// Get a broadcast by path, falling back to a dynamic request when it is not announced.
2820	///
2821	/// Returns a [`kio::Pending`] future (resolved synchronously for an announced broadcast,
2822	/// otherwise once a handler serves it), mirroring [`track::Consumer::fetch_group`](track::Consumer::fetch_group).
2823	/// The lookup order is: an already-announced broadcast resolves
2824	/// immediately; otherwise, if an [`Dynamic`] handler is live (see
2825	/// [`Producer::dynamic`]), a fallback request is registered and the future resolves
2826	/// when the handler [`accept`](Request::accept)s it (or errors if it
2827	/// [`reject`](Request::reject)s or every handler drops). Concurrent requests for
2828	/// the same unannounced path coalesce onto one handler request, and once served the
2829	/// broadcast is cached weakly so *later* requests for that path also share it (rather
2830	/// than re-invoking the handler and opening a duplicate upstream subscription) for as
2831	/// long as it stays live; a closed one is re-served on the next request.
2832	///
2833	/// The returned future resolves to [`Error::Unroutable`] when the path is not announced and no
2834	/// dynamic handler exists. A request that is registered while a handler is live but then loses
2835	/// every handler before being served also resolves to [`Error::Unroutable`]. Unlike an announced
2836	/// broadcast, a dynamically served one is never visible to [`Self::announced`].
2837	pub fn request_broadcast(&self, path: impl AsPath) -> kio::Pending<Requesting> {
2838		let path = path.as_path();
2839
2840		// Key requests by absolute path so a scoped/rooted consumer and the handler
2841		// (which may have a different root) agree on the same entry, and so the egress
2842		// counters resolve against the same broadcast the ingress side wrote.
2843		let absolute = self.root.join(&path).to_owned();
2844		let scope = self.stats.egress(&absolute);
2845
2846		// Prefer a live announcement when one is present; the dynamic queue is only a
2847		// fallback for a path we hold nothing for. A broadcast we do hold but cannot
2848		// serve this requester is unroutable, not missing: a handler resolves paths
2849		// with no route chain to check, so falling through would let it route around
2850		// the split horizon and rebuild the loop.
2851		match self.resolve(&path) {
2852			Resolved::Found(broadcast) => return kio::Pending::new(Requesting::ready(broadcast).with_stats(scope)),
2853			Resolved::Excluded => return kio::Pending::new(Requesting::failed(Error::Unroutable)),
2854			Resolved::Missing => {}
2855		}
2856
2857		let mut state = self.dynamic.lock();
2858
2859		// Reuse a still-live broadcast a handler already served for this path, so repeat
2860		// requests share one upstream subscription. A closed entry is stale; `get` drops it
2861		// and returns `None`, so we fall through and re-serve below.
2862		if let Some(weak) = state.served.get(&absolute) {
2863			return kio::Pending::new(Requesting::ready(weak.consume()).with_stats(scope));
2864		}
2865
2866		// Coalesce onto a pending request for the same path; otherwise register a new
2867		// one, unless there is no handler alive to serve it.
2868		let consumer = if let Some(producer) = state.requests.join(&absolute) {
2869			producer.consume()
2870		} else {
2871			let producer = kio::Producer::<PendingBroadcast>::default();
2872			let consumer = producer.consume();
2873			if state.requests.insert(absolute, producer).is_err() {
2874				return kio::Pending::new(Requesting::failed(Error::Unroutable));
2875			}
2876			consumer
2877		};
2878
2879		kio::Pending::new(Requesting::pending(consumer).with_stats(scope))
2880	}
2881
2882	/// Returns a new Consumer that automatically strips out the provided prefix.
2883	///
2884	/// Returns None if the provided root is not authorized; when [`Self::scope`] was
2885	/// already used without a wildcard.
2886	pub fn with_root(&self, prefix: impl AsPath) -> Option<Self> {
2887		let prefix = prefix.as_path();
2888
2889		Some(Self {
2890			info: self.info,
2891			root: self.root.join(&prefix).to_owned(),
2892			nodes: self.nodes.root(&prefix)?,
2893			dynamic: self.dynamic.clone(),
2894			stats: self.stats.clone(),
2895			exclude: self.exclude,
2896		})
2897	}
2898
2899	/// Returns the prefix that is automatically stripped from all paths.
2900	pub fn root(&self) -> &Path<'_> {
2901		&self.root
2902	}
2903
2904	/// Iterate over the path prefixes this handle is permitted to publish or subscribe under.
2905	// TODO return PathPrefixes
2906	pub fn allowed(&self) -> impl Iterator<Item = &Path<'_>> {
2907		self.nodes.nodes.iter().map(|(root, _)| root)
2908	}
2909
2910	/// Converts a relative path to an absolute path.
2911	pub fn absolute(&self, path: impl AsPath) -> Path<'_> {
2912		self.root.join(path)
2913	}
2914}
2915
2916/// Handle to the announcement stream for a subtree.
2917///
2918/// Symmetric counterpart of [`AnnounceConsumer`]. Cheap to clone; call
2919/// [`Self::consume`] to obtain an [`AnnounceConsumer`] that receives events.
2920#[derive(Clone)]
2921pub struct AnnounceProducer {
2922	nodes: OriginNodes,
2923	root: PathOwned,
2924}
2925
2926impl AnnounceProducer {
2927	fn new(root: PathOwned, nodes: OriginNodes) -> Self {
2928		Self { nodes, root }
2929	}
2930
2931	/// Subscribe to announce / unannounce events for this subtree.
2932	///
2933	/// Allocates a per-cursor coalescing buffer and replays the currently active broadcast set
2934	/// as initial announcements. Drop the returned [`AnnounceConsumer`] to
2935	/// unregister.
2936	pub fn consume(&self) -> AnnounceConsumer {
2937		// Untagged: `AnnounceProducer` is used for internal announce plumbing, not
2938		// egress attribution (which flows through `origin::Consumer::announced`).
2939		AnnounceConsumer::new(self.root.clone(), self.nodes.clone(), stats::Session::default(), None)
2940	}
2941
2942	/// Returns the prefix that is automatically stripped from announced paths.
2943	pub fn root(&self) -> &Path<'_> {
2944		&self.root
2945	}
2946}
2947
2948/// Receives announce / unannounce events for a subtree.
2949///
2950/// Created by [`Consumer::announced`] or [`AnnounceProducer::consume`].
2951/// Drop to unregister.
2952pub struct AnnounceConsumer {
2953	id: ConsumerId,
2954	nodes: OriginNodes,
2955	root: PathOwned,
2956
2957	// Pending updates queued for this cursor. Coalesced so a slow consumer
2958	// can't accumulate redundant announce/unannounce pairs.
2959	state: kio::Producer<OriginConsumerState>,
2960
2961	// Egress stats context (empty for an untagged stream). Announce events drive the
2962	// per-broadcast announce guards below and tag the broadcasts handed out.
2963	stats: stats::Session,
2964
2965	// Live egress announce guards, keyed by absolute broadcast path. An announce
2966	// opens one (bumping `announced` + `announced_bytes`); the matching unannounce
2967	// drops it (bumping `announced_closed` + `announced_bytes`).
2968	guards: HashMap<PathOwned, stats::Announce>,
2969}
2970
2971impl AnnounceConsumer {
2972	fn new(root: PathOwned, nodes: OriginNodes, stats: stats::Session, exclude: Option<Origin>) -> Self {
2973		let state = kio::Producer::<OriginConsumerState>::default();
2974		let id = ConsumerId::new();
2975
2976		for (_, node) in &nodes.nodes {
2977			let notify = AnnounceConsumerNotify {
2978				root: root.clone(),
2979				state: state.clone(),
2980				exclude,
2981			};
2982			node.lock().consume(id, notify);
2983		}
2984
2985		Self {
2986			id,
2987			nodes,
2988			root,
2989			state,
2990			stats,
2991			guards: HashMap::new(),
2992		}
2993	}
2994
2995	/// Drive the egress announce guards and tag the broadcast for one update.
2996	///
2997	/// An announce opens a guard (keyed by absolute path) and tags the yielded
2998	/// broadcast with the egress scope; an unannounce drops the guard. A no-op for
2999	/// an untagged stream.
3000	fn attribute(&mut self, update: OriginAnnounce) -> OriginAnnounce {
3001		let OriginAnnounce { path, broadcast } = update;
3002		let absolute = self.root.join(&path).to_owned();
3003		match broadcast {
3004			Some(broadcast) => {
3005				let scope = self.stats.egress(&absolute);
3006				self.guards.entry(absolute).or_insert_with(|| scope.announce());
3007				OriginAnnounce {
3008					path,
3009					broadcast: Some(broadcast.with_stats(scope)),
3010				}
3011			}
3012			None => {
3013				self.guards.remove(&absolute);
3014				OriginAnnounce { path, broadcast: None }
3015			}
3016		}
3017	}
3018
3019	/// Returns the next (un)announced broadcast and its path relative to this
3020	/// cursor's root.
3021	///
3022	/// The broadcast will only be announced if it was previously unannounced.
3023	/// The same path won't be announced/unannounced twice in a row; instead it
3024	/// toggles. Returns None if the cursor is closed.
3025	pub async fn next(&mut self) -> Option<OriginAnnounce> {
3026		kio::wait(|waiter| self.poll_next(waiter)).await
3027	}
3028
3029	/// Poll for the next (un)announced broadcast, without blocking.
3030	///
3031	/// Returns `Poll::Ready(Some(_))` for an update, `Poll::Ready(None)` if the
3032	/// cursor is closed, or `Poll::Pending` after registering `waiter` to be
3033	/// notified when the next update arrives.
3034	pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<Option<OriginAnnounce>> {
3035		let update = {
3036			let mut state = match ready!(self.state.poll(waiter, |state| {
3037				if state.pending.is_empty() {
3038					Poll::Pending
3039				} else {
3040					Poll::Ready(())
3041				}
3042			})) {
3043				Ok(state) => state,
3044				// Closed: discard the Ref so its MutexGuard doesn't escape this call.
3045				Err(_) => return Poll::Ready(None),
3046			};
3047			state.take().expect("predicate guaranteed an update")
3048		};
3049		Poll::Ready(Some(self.attribute(update)))
3050	}
3051
3052	/// Returns the next (un)announced broadcast without blocking.
3053	///
3054	/// Returns None if there is no update available; NOT because the cursor is closed.
3055	/// Use [`Self::is_closed`] to check if the cursor is closed.
3056	pub fn try_next(&mut self) -> Option<OriginAnnounce> {
3057		let update = self.state.write().ok()?.take()?;
3058		Some(self.attribute(update))
3059	}
3060
3061	/// Returns true if the cursor is closed (no more updates will arrive).
3062	pub fn is_closed(&self) -> bool {
3063		self.state.write().is_err()
3064	}
3065
3066	/// Returns the prefix that is automatically stripped from emitted paths.
3067	pub fn root(&self) -> &Path<'_> {
3068		&self.root
3069	}
3070
3071	/// Converts a relative path to an absolute path.
3072	pub fn absolute(&self, path: impl AsPath) -> Path<'_> {
3073		self.root.join(path)
3074	}
3075}
3076
3077impl Drop for AnnounceConsumer {
3078	fn drop(&mut self) {
3079		for (_, root) in &self.nodes.nodes {
3080			root.lock().unconsume(self.id);
3081		}
3082	}
3083}
3084
3085#[cfg(test)]
3086use futures::FutureExt;
3087
3088#[cfg(test)]
3089#[allow(missing_docs)] // test-only assertion helpers
3090impl AnnounceConsumer {
3091	pub fn assert_next(&mut self, expected: impl AsPath, broadcast: &broadcast::Consumer) {
3092		let expected = expected.as_path();
3093		let announce = self.next().now_or_never().expect("next blocked").expect("no next");
3094		assert_eq!(announce.path, expected, "wrong path");
3095		let announced = announce.broadcast.expect("should be an active announce");
3096		assert!(announced.is_clone(broadcast), "should be the same broadcast");
3097	}
3098
3099	/// An announce for `expected`, without asserting which broadcast backs it
3100	/// (the origin owns the announced broadcast, not the publisher). Returns the
3101	/// announced consumer.
3102	pub fn assert_next_some(&mut self, expected: impl AsPath) -> broadcast::Consumer {
3103		let expected = expected.as_path();
3104		let announce = self.next().now_or_never().expect("next blocked").expect("no next");
3105		assert_eq!(announce.path, expected, "wrong path");
3106		announce.broadcast.expect("should be an active announce")
3107	}
3108
3109	pub fn assert_try_next(&mut self, expected: impl AsPath, broadcast: &broadcast::Consumer) {
3110		let expected = expected.as_path();
3111		let announce = self.try_next().expect("no next");
3112		assert_eq!(announce.path, expected, "wrong path");
3113		let announced = announce.broadcast.expect("should be an active announce");
3114		assert!(announced.is_clone(broadcast), "should be the same broadcast");
3115	}
3116
3117	/// The `try_next` counterpart of [`Self::assert_next_some`].
3118	pub fn assert_try_next_some(&mut self, expected: impl AsPath) -> broadcast::Consumer {
3119		let expected = expected.as_path();
3120		let announce = self.try_next().expect("no next");
3121		assert_eq!(announce.path, expected, "wrong path");
3122		announce.broadcast.expect("should be an active announce")
3123	}
3124
3125	pub fn assert_next_none(&mut self, expected: impl AsPath) {
3126		let expected = expected.as_path();
3127		let announce = self.next().now_or_never().expect("next blocked").expect("no next");
3128		assert_eq!(announce.path, expected, "wrong path");
3129		assert!(announce.broadcast.is_none(), "should be unannounced");
3130	}
3131
3132	pub fn assert_next_wait(&mut self) {
3133		if let Some(res) = self.next().now_or_never() {
3134			panic!("next should block: got {:?}", res.map(|a| a.path));
3135		}
3136	}
3137
3138	/*
3139	pub fn assert_next_closed(&mut self) {
3140		assert!(
3141			self.next().now_or_never().expect("next blocked").is_none(),
3142			"next should be closed"
3143		);
3144	}
3145	*/
3146}
3147
3148#[cfg(test)]
3149mod tests {
3150	use crate::coding::Decode;
3151	use crate::group;
3152
3153	use super::*;
3154
3155	/// An announced direct route.
3156	fn announce() -> broadcast::Route {
3157		broadcast::Route::new().with_announce(true)
3158	}
3159
3160	/// The first origin whose handover key for `name` sits above (`true`) or below
3161	/// (`false`) the peer's, so tests exercising the carrying gate are
3162	/// deterministic instead of hinging on a random id winning a hash comparison.
3163	/// Starts searching above the small ids the tests use in hop chains, so the
3164	/// result never collides with a hop (a looping chain trips a debug_assert).
3165	fn origin_keyed(name: &str, peer: Origin, above: bool) -> Origin {
3166		let name = Path::new(name);
3167		let peer_key = fnv_key(&name, [peer]);
3168		(100u64..)
3169			.map(|id| Origin::new(id).unwrap())
3170			.find(|origin| (fnv_key(&name, [*origin]) > peer_key) == above)
3171			.unwrap()
3172	}
3173
3174	/// A front table for reselect tests: routes get ids in order, the first is
3175	/// the incumbent.
3176	fn front_state(self_origin: Origin, routes: Vec<broadcast::Route>) -> FrontState {
3177		let source = broadcast::Info::new().produce().consume();
3178		FrontState {
3179			path: Path::new("test").to_owned(),
3180			self_origin,
3181			publisher: routes.first().and_then(|r| r.hops.iter().next().copied()),
3182			next_route: routes.len() as u64,
3183			excluded: HashMap::new(),
3184			routes: routes
3185				.into_iter()
3186				.enumerate()
3187				.map(|(id, route)| FrontRoute {
3188					id: id as u64,
3189					route,
3190					source: source.clone(),
3191				})
3192				.collect(),
3193			active: Some(0),
3194			linger: Duration::ZERO,
3195			closed: false,
3196		}
3197	}
3198
3199	/// A route as a warm sibling would announce it: zero cost, chain ending at
3200	/// the announcing peer.
3201	fn sibling_route(peer: Origin) -> broadcast::Route {
3202		let hops = OriginList::try_from(vec![Origin::new(90).unwrap(), peer]).unwrap();
3203		announce().with_hops(hops)
3204	}
3205
3206	/// A route as the upstream announces it: priced, one hop.
3207	fn upstream_route(cost: u64) -> broadcast::Route {
3208		let hops = OriginList::try_from(vec![Origin::new(90).unwrap()]).unwrap();
3209		announce().with_hops(hops).with_cost(cost)
3210	}
3211
3212	/// While carrying, a strictly cheaper route from a peer that hashes above us
3213	/// must not displace the incumbent; the same table re-parents freely once
3214	/// idle, or when the peer hashes below us.
3215	#[test]
3216	fn test_carrying_gate_keys() {
3217		let peer = Origin::new(3).unwrap();
3218
3219		// We lose the key comparison: stay put while carrying, migrate when idle.
3220		let mut lost = front_state(
3221			origin_keyed("test", peer, false),
3222			vec![upstream_route(10), sibling_route(peer)],
3223		);
3224		lost.reselect(true);
3225		assert_eq!(
3226			lost.active,
3227			Some(0),
3228			"carrying front re-parented onto a higher-keyed peer"
3229		);
3230		lost.reselect(false);
3231		assert_eq!(lost.active, Some(1), "idle front must take the cheaper route");
3232
3233		// We win the key comparison: re-parent even while carrying.
3234		let mut won = front_state(
3235			origin_keyed("test", peer, true),
3236			vec![upstream_route(10), sibling_route(peer)],
3237		);
3238		won.reselect(true);
3239		assert_eq!(won.active, Some(1), "carrying front must follow a lower-keyed peer");
3240	}
3241
3242	/// The simultaneous-activation race: two relays that each pulled the same
3243	/// broadcast independently see each other's zero-cost route. Exactly one of
3244	/// them re-parents; the other keeps its upstream, so the broadcast is never
3245	/// left without a source.
3246	#[test]
3247	fn test_carrying_gate_symmetric_race() {
3248		let a = Origin::new(1).unwrap();
3249		let b = Origin::new(2).unwrap();
3250
3251		let mut a_view = front_state(a, vec![upstream_route(10), sibling_route(b)]);
3252		let mut b_view = front_state(b, vec![upstream_route(10), sibling_route(a)]);
3253		a_view.reselect(true);
3254		b_view.reselect(true);
3255
3256		let a_moved = a_view.active == Some(1);
3257		let b_moved = b_view.active == Some(1);
3258		assert!(
3259			a_moved != b_moved,
3260			"exactly one side must re-parent (a: {a_moved}, b: {b_moved})"
3261		);
3262	}
3263
3264	/// The gate is scoped to warm siblings: a cheaper route via a relay that is
3265	/// not itself carrying (advertised nonzero), or directly from the original
3266	/// publisher (single-hop chain), is taken immediately even while carrying
3267	/// and even when we would lose the key comparison.
3268	#[test]
3269	fn test_carrying_switches_to_benign_routes() {
3270		let peer = Origin::new(3).unwrap();
3271		let lost = origin_keyed("test", peer, false);
3272
3273		// A cheaper forwarder path: the relay advertised its accumulated cost.
3274		let mut forwarder = sibling_route(peer).with_cost(4);
3275		forwarder.advertised = 4;
3276		let mut state = front_state(lost, vec![upstream_route(10), forwarder]);
3277		state.reselect(true);
3278		assert_eq!(
3279			state.active,
3280			Some(1),
3281			"a cheaper forwarder path must win while carrying"
3282		);
3283
3284		// Directly from the original publisher: single-hop chain, advertised zero.
3285		let direct = announce().with_hops(OriginList::try_from(vec![peer]).unwrap());
3286		let mut state = front_state(lost, vec![upstream_route(10), direct]);
3287		state.reselect(true);
3288		assert_eq!(
3289			state.active,
3290			Some(1),
3291			"a direct publisher route must win while carrying"
3292		);
3293
3294		// The peer's session reconnecting: same chain, same cost, so the gate's
3295		// strictly-cheaper test does not apply and recency decides. Loosening that
3296		// test to `<=` would hold a carrying front on the dead session until the
3297		// transport timed it out, which is the whole point of the recency order.
3298		let mut state = front_state(lost, vec![sibling_route(peer), sibling_route(peer)]);
3299		state.reselect(true);
3300		assert_eq!(
3301			state.active,
3302			Some(1),
3303			"a reconnect on an identical chain must win while carrying"
3304		);
3305	}
3306
3307	/// The gate only protects an announced incumbent: one that lost its announce
3308	/// (the upstream retracted) is displaced regardless of the key comparison.
3309	#[test]
3310	fn test_carrying_gate_ignores_unannounced_incumbent() {
3311		let peer = Origin::new(3).unwrap();
3312		let unannounced = upstream_route(10).with_announce(false);
3313		let mut state = front_state(
3314			origin_keyed("test", peer, false),
3315			vec![unannounced, sibling_route(peer)],
3316		);
3317		state.reselect(true);
3318		assert_eq!(
3319			state.active,
3320			Some(1),
3321			"an unannounced incumbent must always be displaced"
3322		);
3323	}
3324
3325	/// A route arriving through a peer the front is already exposed to is our own
3326	/// broadcast reflected back, whatever its chain claims, so it must not evict the
3327	/// front. This is the shape a relay with no hop ids produces when both directions
3328	/// of a sync target point at it.
3329	#[test]
3330	fn test_reflection_through_an_exposed_peer_cannot_take_over() {
3331		let peer = Origin::new(42).unwrap();
3332		let upstream = OriginList::try_from(vec![Origin::new(7).unwrap()]).unwrap();
3333		let reflected = announce().with_hops(OriginList::try_from(vec![peer]).unwrap());
3334
3335		let mut state = front_state(Origin::new(1).unwrap(), vec![announce().with_hops(upstream)]);
3336
3337		// Nobody is exposed yet: a different publisher is free to take the path.
3338		assert!(!state.taints_a_reader(&reflected));
3339
3340		// Advertising the path to `peer` registers them, and the same route is now
3341		// recognizable as a reflection.
3342		*state.excluded.entry(peer).or_default() += 1;
3343		assert!(state.taints_a_reader(&reflected));
3344	}
3345
3346	/// The exposure is what discriminates, not the shape of the chain: an unrelated
3347	/// publisher reaching us through some *other* peer still takes the path over.
3348	#[test]
3349	fn test_rival_publisher_through_another_peer_still_takes_over() {
3350		let peer = Origin::new(42).unwrap();
3351		let elsewhere = Origin::new(43).unwrap();
3352		let upstream = OriginList::try_from(vec![Origin::new(7).unwrap()]).unwrap();
3353		let rival = announce().with_hops(OriginList::try_from(vec![Origin::UNKNOWN, elsewhere]).unwrap());
3354
3355		let mut state = front_state(Origin::new(1).unwrap(), vec![announce().with_hops(upstream)]);
3356		*state.excluded.entry(peer).or_default() += 1;
3357
3358		assert!(!state.taints_a_reader(&rival), "only the peer we feed is a reflection");
3359	}
3360
3361	/// A peer that carries no hop ids hides its depth: everything behind it collapses
3362	/// into one entry, so its route understates its true length and wins the cost
3363	/// comparison against a longer-looking but genuinely shorter path. Pricing the
3364	/// opaque link (`Client::with_cost`) is what restores the intended order.
3365	#[test]
3366	fn test_opaque_peer_understates_its_depth() {
3367		let us = Origin::new(1).unwrap();
3368
3369		// Our own upstream, honestly described: two hops, charged per link.
3370		let direct = || {
3371			announce()
3372				.with_hops(OriginList::try_from(vec![Origin::new(7).unwrap(), Origin::new(8).unwrap()]).unwrap())
3373				.with_cost(2)
3374		};
3375		// The same content reached through an opaque relay, which is actually further
3376		// away but advertises no chain at all, so it lands as one unpriced hop.
3377		let opaque = |cost| {
3378			announce()
3379				.with_hops(OriginList::try_from(vec![Origin::new(42).unwrap()]).unwrap())
3380				.with_cost(cost)
3381		};
3382
3383		// Unpriced, the opaque route wins on cost even though it is the longer path.
3384		let mut state = front_state(us, vec![direct(), opaque(1)]);
3385		state.reselect(false);
3386		assert_eq!(
3387			state.active,
3388			Some(1),
3389			"an unpriced opaque link out-ranks a shorter real path"
3390		);
3391
3392		// Priced to reflect what it hides, the real path wins again.
3393		let mut state = front_state(us, vec![direct(), opaque(16)]);
3394		state.reselect(false);
3395		assert_eq!(
3396			state.active,
3397			Some(0),
3398			"pricing the opaque link restores the intended order"
3399		);
3400	}
3401
3402	/// Let the spawned origin tasks (source watchers, front dispatch) run. The
3403	/// tests pause tokio time, so this advances the clock instantly.
3404	async fn settle() {
3405		tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
3406	}
3407
3408	/// Serve one requested track from a source like a session would: wait for the
3409	/// origin to dispatch it, then accept with default info.
3410	async fn accept_track(dynamic: &mut broadcast::Dynamic, name: &str) -> track::Producer {
3411		let request = tokio::time::timeout(std::time::Duration::from_secs(1), dynamic.requested_track())
3412			.await
3413			.expect("timed out waiting for a track request")
3414			.expect("source closed");
3415		assert_eq!(request.name(), name, "unexpected track dispatched");
3416		request.accept(None)
3417	}
3418
3419	/// Serve `count` requested tracks, keyed by name. Dispatch order across
3420	/// tracks is not guaranteed, which [`accept_track`]'s exact-name assert
3421	/// cannot express.
3422	async fn accept_tracks(dynamic: &mut broadcast::Dynamic, count: usize) -> HashMap<String, track::Producer> {
3423		let mut accepted = HashMap::new();
3424		for _ in 0..count {
3425			let request = tokio::time::timeout(std::time::Duration::from_secs(1), dynamic.requested_track())
3426				.await
3427				.expect("timed out waiting for a track request")
3428				.expect("source closed");
3429			let name = request.name().to_string();
3430			accepted.insert(name, request.accept(None));
3431		}
3432		accepted
3433	}
3434
3435	/// Tagging both origin handles with one context attributes the full model path:
3436	/// ingress writes on the subscriber side, egress reads on the publisher side,
3437	/// each counter landing exactly once (the model-layer silent-zero guard).
3438	#[tokio::test]
3439	async fn test_stats_tagged_end_to_end() {
3440		use crate::Timestamp;
3441		use crate::stats::{Config, Registry, Tier};
3442		use bytes::Bytes;
3443
3444		tokio::time::pause();
3445
3446		let registry = Registry::new(Config::new());
3447		let ctx = registry.tier(Tier::default()).session("acme");
3448
3449		let origin = Origin::random().produce();
3450		let ingress = origin.clone().with_stats(ctx.clone());
3451		let egress = origin.consume().with_stats(ctx.clone());
3452
3453		// Egress announce stream: this is the tagged stream that drives the egress
3454		// announce guard.
3455		let mut announced = egress.announced();
3456
3457		// Ingress publishes an announced broadcast.
3458		let source = ingress.create_broadcast("demo", announce()).unwrap();
3459		let mut dynamic = source.dynamic();
3460		settle().await;
3461		settle().await;
3462
3463		// Egress observes the announce and gets the tagged broadcast.
3464		let update = announced.next().await.unwrap();
3465		assert_eq!(update.path.as_str(), "demo");
3466		let broadcast = update.broadcast.unwrap();
3467
3468		// Egress subscribes; the ingress side serves the track on demand.
3469		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3470		let mut producer = accept_track(&mut dynamic, "video").await;
3471		settle().await;
3472		let mut sub = subscribing.await.unwrap();
3473
3474		// Ingress writes one group with two 5-byte frames.
3475		let mut group = producer.append_group().unwrap();
3476		group
3477			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
3478			.unwrap();
3479		group
3480			.write_frame(Timestamp::ZERO, Bytes::from_static(b"world"))
3481			.unwrap();
3482		group.finish().unwrap();
3483
3484		// Egress reads the group and both frames.
3485		let mut group_c = sub.recv_group().await.unwrap().unwrap();
3486		let mut frames = 0;
3487		while let Some(frame) = group_c.read_frame().await.unwrap() {
3488			assert_eq!(frame.payload.len(), 5);
3489			frames += 1;
3490		}
3491		assert_eq!(frames, 2);
3492		settle().await;
3493
3494		let report = registry.report();
3495		let entry = report
3496			.traffic
3497			.iter()
3498			.find(|e| e.path.as_str() == "demo")
3499			.expect("demo tracked");
3500		let path_len = "demo".len() as u64;
3501
3502		// Egress (publisher side): reads out of the model.
3503		let egress = &entry.publisher;
3504		assert_eq!(egress.announced, 1, "one egress announce");
3505		assert_eq!(egress.announced_bytes, path_len);
3506		assert_eq!(egress.subscriptions, 1, "one egress subscription");
3507		assert_eq!(egress.broadcasts, 1, "one viewer");
3508		assert_eq!(egress.groups, 1);
3509		assert_eq!(egress.frames, 2);
3510		assert_eq!(egress.bytes, 10);
3511		assert_eq!(egress.fetches, 0);
3512
3513		// Ingress (subscriber side): writes into the model.
3514		let ingress = &entry.subscriber;
3515		assert_eq!(ingress.announced, 1, "one ingress announce");
3516		assert_eq!(ingress.announced_bytes, path_len);
3517		assert_eq!(ingress.subscriptions, 1, "one ingress track");
3518		assert_eq!(ingress.broadcasts, 0, "ingress has no viewer refcount");
3519		assert_eq!(ingress.groups, 1);
3520		assert_eq!(ingress.frames, 2);
3521		assert_eq!(ingress.bytes, 10);
3522
3523		// A fetch bumps only `fetches` on the egress side, plus the delivered group.
3524		let fetched = broadcast.track("video").unwrap().fetch_group(0, None).await.unwrap();
3525		let _ = fetched;
3526		settle().await;
3527		let report = registry.report();
3528		let entry = report.traffic.iter().find(|e| e.path.as_str() == "demo").unwrap();
3529		assert_eq!(entry.publisher.fetches, 1, "one fetch");
3530		assert_eq!(entry.publisher.subscriptions, 1, "fetch does not bump subscriptions");
3531		assert_eq!(entry.publisher.broadcasts, 1, "fetch does not bump the viewer refcount");
3532		// `fetches` is egress-only for the same structural reason as `broadcasts`:
3533		// only a `track::Consumer` can fetch, and the ingress scope never reaches one
3534		// (`broadcast::Producer::consume` hands out an untagged consumer).
3535		assert_eq!(entry.subscriber.fetches, 0, "ingress cannot fetch");
3536	}
3537
3538	/// `Subscriber::read_frame` collapses a group to its first frame. The paths it
3539	/// delegates to (plain and spliced) build their own *unmetered* group consumers,
3540	/// so the wrapper is the only place that can attribute the read: exactly one
3541	/// group, one frame, and the payload bytes, counted once each.
3542	#[tokio::test]
3543	async fn test_stats_read_frame_counts_once() {
3544		use crate::Timestamp;
3545		use crate::stats::{Config, Registry, Tier};
3546		use bytes::Bytes;
3547
3548		tokio::time::pause();
3549
3550		let registry = Registry::new(Config::new());
3551		let ctx = registry.tier(Tier::default()).session("acme");
3552
3553		let origin = Origin::random().produce();
3554		let ingress = origin.clone().with_stats(ctx.clone());
3555		let egress = origin.consume().with_stats(ctx.clone());
3556
3557		let mut announced = egress.announced();
3558		let source = ingress.create_broadcast("demo", announce()).unwrap();
3559		let mut dynamic = source.dynamic();
3560		settle().await;
3561		settle().await;
3562
3563		let broadcast = announced.next().await.unwrap().broadcast.unwrap();
3564		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3565		let mut producer = accept_track(&mut dynamic, "video").await;
3566		settle().await;
3567		let mut sub = subscribing.await.unwrap();
3568
3569		// A single-frame group, read back through the collapsing helper.
3570		producer
3571			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
3572			.unwrap();
3573
3574		let frame = sub.read_frame().await.unwrap().expect("frame");
3575		assert_eq!(frame.payload.len(), 5);
3576		settle().await;
3577
3578		let report = registry.report();
3579		let entry = report
3580			.traffic
3581			.iter()
3582			.find(|e| e.path.as_str() == "demo")
3583			.expect("demo tracked");
3584		assert_eq!(entry.publisher.groups, 1, "one group, counted once");
3585		assert_eq!(entry.publisher.frames, 1, "one frame, counted once");
3586		assert_eq!(
3587			entry.publisher.bytes, 5,
3588			"payload counted once, not zero and not doubled"
3589		);
3590	}
3591
3592	/// Datagrams bypass the group/frame handles entirely, so they're metered at the
3593	/// producer (ingress write) and the subscriber (egress read). Each one counts as
3594	/// the single-frame group it stands in for, plus the `datagrams` breakout.
3595	#[tokio::test]
3596	async fn test_stats_datagrams_counted_both_sides() {
3597		use crate::Timestamp;
3598		use crate::stats::{Config, Registry, Tier};
3599
3600		tokio::time::pause();
3601
3602		let registry = Registry::new(Config::new());
3603		let ctx = registry.tier(Tier::default()).session("acme");
3604
3605		let origin = Origin::random().produce();
3606		let ingress = origin.clone().with_stats(ctx.clone());
3607		let egress = origin.consume().with_stats(ctx.clone());
3608
3609		let mut announced = egress.announced();
3610		let source = ingress.create_broadcast("demo", announce()).unwrap();
3611		let mut dynamic = source.dynamic();
3612		settle().await;
3613		settle().await;
3614
3615		let broadcast = announced.next().await.unwrap().broadcast.unwrap();
3616		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3617		let mut producer = accept_track(&mut dynamic, "video").await;
3618		settle().await;
3619		let mut sub = subscribing.await.unwrap();
3620
3621		producer.append_datagram(Timestamp::ZERO, &b"hello"[..]).unwrap();
3622		let datagram = sub.recv_datagram().await.unwrap().expect("datagram");
3623		assert_eq!(&datagram.payload[..], b"hello");
3624		settle().await;
3625
3626		let report = registry.report();
3627		let entry = report
3628			.traffic
3629			.iter()
3630			.find(|e| e.path.as_str() == "demo")
3631			.expect("demo tracked");
3632
3633		for (side, traffic) in [("egress", &entry.publisher), ("ingress", &entry.subscriber)] {
3634			assert_eq!(traffic.datagrams, 1, "{side}: one datagram");
3635			assert_eq!(traffic.groups, 1, "{side}: counted as its single-frame group");
3636			assert_eq!(traffic.frames, 1, "{side}: one frame");
3637			assert_eq!(traffic.bytes, 5, "{side}: payload counted once");
3638		}
3639	}
3640
3641	#[test]
3642	fn origin_rejects_reserved_ids() {
3643		assert!(Origin::new(0).is_err());
3644		assert!(Origin::new(1u64 << 62).is_err());
3645		assert_eq!(Origin::new(1).unwrap().id(), 1);
3646
3647		let mut zero = [0u8].as_slice();
3648		assert_eq!(
3649			Origin::decode(&mut zero, crate::lite::Version::Lite05).unwrap(),
3650			Origin::UNKNOWN
3651		);
3652	}
3653
3654	#[test]
3655	fn origin_list_push_fails_at_limit() {
3656		let mut list = OriginList::new();
3657		for _ in 0..MAX_HOPS {
3658			list.push(Origin::random()).unwrap();
3659		}
3660		assert_eq!(list.len(), MAX_HOPS);
3661		assert_eq!(list.push(Origin::random()), Err(TooManyOrigins));
3662	}
3663
3664	#[test]
3665	fn origin_list_replace_first() {
3666		let mut list = OriginList::new();
3667		for _ in 0..3 {
3668			list.push(Origin::UNKNOWN).unwrap();
3669		}
3670
3671		// Rewrites only the first placeholder, keeping the length the same.
3672		assert!(list.replace_first(Origin::UNKNOWN, Origin::new(7).unwrap()));
3673		assert_eq!(
3674			list.as_slice(),
3675			&[Origin::new(7).unwrap(), Origin::UNKNOWN, Origin::UNKNOWN]
3676		);
3677
3678		// No match leaves the list untouched.
3679		assert!(!list.replace_first(Origin::new(99).unwrap(), Origin::new(8).unwrap()));
3680		assert_eq!(list.len(), 3);
3681	}
3682
3683	#[test]
3684	fn origin_list_try_from_vec_enforces_limit() {
3685		let under: Vec<Origin> = (0..MAX_HOPS).map(|_| Origin::random()).collect();
3686		assert!(OriginList::try_from(under).is_ok());
3687
3688		let over: Vec<Origin> = (0..MAX_HOPS + 1).map(|_| Origin::random()).collect();
3689		assert_eq!(OriginList::try_from(over), Err(TooManyOrigins));
3690	}
3691
3692	#[tokio::test]
3693	async fn test_announce() {
3694		tokio::time::pause();
3695
3696		let origin = Origin::random().produce();
3697
3698		let mut consumer1 = origin.consume().announced();
3699		consumer1.assert_next_wait();
3700
3701		// Publish the first broadcast; it becomes visible asynchronously.
3702		let mut broadcast1 = origin.create_broadcast("test1", announce()).unwrap();
3703		settle().await;
3704
3705		consumer1.assert_next_some("test1");
3706		consumer1.assert_next_wait();
3707
3708		// Make a new consumer that should get the existing broadcast.
3709		// But we don't consume it yet.
3710		let mut consumer2 = origin.consume().announced();
3711
3712		// Publish the second broadcast.
3713		let mut broadcast2 = origin.create_broadcast("test2", announce()).unwrap();
3714		settle().await;
3715
3716		consumer1.assert_next_some("test2");
3717		consumer1.assert_next_wait();
3718
3719		consumer2.assert_next_some("test1");
3720		consumer2.assert_next_some("test2");
3721		consumer2.assert_next_wait();
3722
3723		// Finish the first broadcast: a graceful end unannounces immediately.
3724		broadcast1.finish();
3725		settle().await;
3726
3727		// All consumers should get a None now.
3728		consumer1.assert_next_none("test1");
3729		consumer2.assert_next_none("test1");
3730		consumer1.assert_next_wait();
3731		consumer2.assert_next_wait();
3732
3733		// And a new consumer only gets the last broadcast.
3734		let mut consumer3 = origin.consume().announced();
3735		consumer3.assert_next_some("test2");
3736		consumer3.assert_next_wait();
3737
3738		broadcast2.finish();
3739		settle().await;
3740
3741		consumer1.assert_next_none("test2");
3742		consumer2.assert_next_none("test2");
3743		consumer3.assert_next_none("test2");
3744	}
3745
3746	/// Multiple sources created at one path feed a single origin-owned broadcast:
3747	/// one announce, no churn as sources come and go, and an unannounce only when
3748	/// the last source leaves.
3749	#[tokio::test]
3750	async fn test_duplicate() {
3751		tokio::time::pause();
3752
3753		let origin = Origin::random().produce();
3754		let consumer = origin.consume();
3755		let mut announced = consumer.announced();
3756
3757		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
3758		let mut broadcast2 = origin.create_broadcast("test", announce()).unwrap();
3759		let mut broadcast3 = origin.create_broadcast("test", announce()).unwrap();
3760		settle().await;
3761		assert!(consumer.get_broadcast("test").is_some());
3762
3763		announced.assert_next_some("test");
3764		announced.assert_next_wait();
3765
3766		// A standby source finishing changes nothing.
3767		broadcast2.finish();
3768		settle().await;
3769		assert!(consumer.get_broadcast("test").is_some());
3770		announced.assert_next_wait();
3771
3772		// The active source finishing hands over to a survivor, invisibly.
3773		broadcast1.finish();
3774		settle().await;
3775		assert!(consumer.get_broadcast("test").is_some());
3776		announced.assert_next_wait();
3777
3778		// The last source finishing unannounces and removes the broadcast.
3779		broadcast3.finish();
3780		settle().await;
3781		assert!(consumer.get_broadcast("test").is_none());
3782
3783		announced.assert_next_none("test");
3784		announced.assert_next_wait();
3785	}
3786
3787	/// A source dying mid-serve fails over: the track re-splices from the standby
3788	/// source and resumes exactly at the first missing group.
3789	#[tokio::test]
3790	async fn test_route_failover() {
3791		tokio::time::pause();
3792
3793		let origin = Origin::random().produce();
3794		let consumer = origin.consume();
3795		let mut announced = consumer.announced();
3796
3797		// Both routes share the first hop (the original publisher): only
3798		// interchangeable content may join as a standby.
3799		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3800		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
3801
3802		// The first source announces the broadcast.
3803		let source_a = origin.create_broadcast("test", announce().with_hops(hops_a)).unwrap();
3804		let mut dynamic_a = source_a.dynamic();
3805		settle().await;
3806		settle().await;
3807		let broadcast = consumer.request_broadcast("test").await.unwrap();
3808		announced.assert_next_some("test");
3809
3810		// A second (longer) source joins silently as a standby.
3811		let source_b = origin.create_broadcast("test", announce().with_hops(hops_b)).unwrap();
3812		let mut dynamic_b = source_b.dynamic();
3813		settle().await;
3814		settle().await;
3815		announced.assert_next_wait();
3816
3817		// Subscribing dispatches the track to the best source (A).
3818		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3819		let mut producer = accept_track(&mut dynamic_a, "video").await;
3820		settle().await;
3821		dynamic_b.assert_no_request();
3822
3823		let mut sub = subscribing.await.unwrap();
3824		// Demand registers as the subscriber polls; a fresh segment carries no
3825		// boundary, so the demand is the subscriber's own.
3826		sub.assert_no_group();
3827		assert_eq!(producer.subscription().unwrap().group_start, None);
3828
3829		producer.append_group().unwrap();
3830		producer.append_group().unwrap();
3831		assert_eq!(sub.assert_group().sequence, 0);
3832		assert_eq!(sub.assert_group().sequence, 1);
3833
3834		// Source A dies (session loss): the track re-splices from B and nothing
3835		// is announced.
3836		// abort() consumes the producer, so this both aborts and drops it.
3837		producer.abort(Error::Dropped).unwrap();
3838		source_a.abort(Error::Dropped).unwrap();
3839		drop(dynamic_a);
3840		settle().await;
3841		announced.assert_next_wait();
3842
3843		// The new copy keeps the subscriber's live-edge demand; the splice
3844		// boundary bounds the range, so groups the old source already delivered
3845		// are filtered rather than re-demanded.
3846		let mut producer = accept_track(&mut dynamic_b, "video").await;
3847		settle().await;
3848		sub.assert_no_group();
3849		assert_eq!(producer.subscription().unwrap().group_start, None);
3850		producer.create_group(group::Info { sequence: 1 }).unwrap();
3851		producer.create_group(group::Info { sequence: 2 }).unwrap();
3852		assert_eq!(sub.assert_group().sequence, 2, "groups below the boundary are filtered");
3853		sub.assert_not_closed();
3854	}
3855
3856	/// Failover restores *every* subscribed track, not just one. A single-track
3857	/// takeover leaves a partial recovery indistinguishable from a whole one:
3858	/// the subscriber survives and keeps reading, but a track that is never
3859	/// re-dispatched to the standby stalls silently. Real broadcasts are
3860	/// multi-track (an MPEG-TS feed carries video, audio and data together), so
3861	/// each track's boundary has to be computed and served independently.
3862	#[tokio::test]
3863	async fn test_route_failover_restores_every_track() {
3864		tokio::time::pause();
3865
3866		let origin = Origin::random().produce();
3867		let consumer = origin.consume();
3868
3869		// Both routes share the first hop: interchangeable content.
3870		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3871		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
3872
3873		let source_a = origin.create_broadcast("test", announce().with_hops(hops_a)).unwrap();
3874		let mut dynamic_a = source_a.dynamic();
3875		settle().await;
3876		settle().await;
3877		let broadcast = consumer.request_broadcast("test").await.unwrap();
3878
3879		// The standby joins silently.
3880		let source_b = origin.create_broadcast("test", announce().with_hops(hops_b)).unwrap();
3881		let mut dynamic_b = source_b.dynamic();
3882		settle().await;
3883		settle().await;
3884
3885		// Deliberately unequal group counts, so every track's resume boundary is a
3886		// different number. Equal counts would let a boundary computed per
3887		// broadcast rather than per track pass unnoticed.
3888		const TRACKS: [(&str, u64); 3] = [("video", 3), ("audio", 1), ("data", 2)];
3889
3890		let subscribing: Vec<_> = TRACKS
3891			.iter()
3892			.map(|(name, _)| broadcast.track(name).unwrap().subscribe(None))
3893			.collect();
3894		let mut producers_a = accept_tracks(&mut dynamic_a, TRACKS.len()).await;
3895		settle().await;
3896
3897		let mut subs = Vec::new();
3898		for ((name, groups), subscribing) in TRACKS.iter().zip(subscribing) {
3899			let mut sub = subscribing.await.unwrap();
3900			let producer = producers_a
3901				.get_mut(*name)
3902				.unwrap_or_else(|| panic!("{name} was never dispatched"));
3903			for expected in 0..*groups {
3904				producer.append_group().unwrap();
3905				assert_eq!(sub.assert_group().sequence, expected, "{name} did not start");
3906			}
3907			subs.push((*name, *groups, sub));
3908		}
3909
3910		// Source A dies mid-stream.
3911		for (_, producer) in producers_a.drain() {
3912			producer.abort(Error::Dropped).unwrap();
3913		}
3914		source_a.abort(Error::Dropped).unwrap();
3915		drop(dynamic_a);
3916		settle().await;
3917
3918		// The standby must be asked for all of them, and each must resume at its
3919		// own boundary.
3920		let mut producers_b = accept_tracks(&mut dynamic_b, TRACKS.len()).await;
3921		settle().await;
3922
3923		// Demand registers as each subscriber polls, so poll them all before
3924		// reading the boundaries back.
3925		for (_, _, sub) in subs.iter_mut() {
3926			sub.assert_no_group();
3927		}
3928		settle().await;
3929
3930		for (name, groups, sub) in subs.iter_mut() {
3931			let producer = producers_b
3932				.get_mut(*name)
3933				.unwrap_or_else(|| panic!("{name} was never re-dispatched to the standby"));
3934			// The live-edge demand survives the failover; each track's own
3935			// boundary lives in its segment range, checked through the filter.
3936			assert_eq!(
3937				producer
3938					.subscription()
3939					.unwrap_or_else(|| panic!("{name} resumed without a subscription"))
3940					.group_start,
3941				None,
3942				"{name} must keep the subscriber's live-edge demand"
3943			);
3944			let boundary = *groups;
3945
3946			// A group below the boundary is filtered out; one at it is delivered.
3947			producer.create_group(group::Info { sequence: boundary - 1 }).unwrap();
3948			producer.create_group(group::Info { sequence: boundary }).unwrap();
3949			assert_eq!(sub.assert_group().sequence, boundary, "{name} did not resume");
3950			sub.assert_not_closed();
3951		}
3952	}
3953
3954	/// `route_changed` yields the current route first, then each change; equal
3955	/// updates coalesce, and the watch errors once every producer is gone.
3956	#[tokio::test]
3957	async fn test_broadcast_route_watch() {
3958		let mut producer = broadcast::Info::new().produce();
3959		let mut consumer = producer.consume();
3960
3961		// Initial value: the default route.
3962		assert_eq!(consumer.route_changed().await.unwrap(), broadcast::Route::default());
3963
3964		// An equal update is a no-op.
3965		producer.set_route(broadcast::Route::default()).unwrap();
3966		assert!(consumer.route_changed().now_or_never().is_none());
3967
3968		let mut hops = OriginList::new();
3969		hops.push(Origin::new(7).unwrap()).unwrap();
3970		let route = broadcast::Route::new().with_hops(hops).with_cost(3);
3971		producer.set_route(route.clone()).unwrap();
3972		assert_eq!(consumer.route_changed().await.unwrap(), route);
3973
3974		// A fresh consumer sees the current value immediately.
3975		let mut fresh = producer.consume();
3976		assert_eq!(fresh.route_changed().await.unwrap(), route);
3977
3978		drop(producer);
3979		assert!(matches!(consumer.route_changed().await.unwrap_err(), Error::Dropped));
3980	}
3981
3982	/// A cost update that flips the winning source hands live tracks over at a
3983	/// group boundary and re-advertises the broadcast's route, without announce
3984	/// churn.
3985	#[tokio::test]
3986	async fn test_route_cost_update() {
3987		tokio::time::pause();
3988
3989		// The takeover happens while a subscriber is live (carrying), so the local
3990		// origin must win the handover key comparison against B's announcing hop
3991		// (origin 3); a random id would flake on the hash.
3992		let origin = Info::new(origin_keyed("test", Origin::new(3).unwrap(), true)).produce();
3993		let consumer = origin.consume();
3994		let mut announced = consumer.announced();
3995
3996		// Both routes share the first hop (the original publisher): only
3997		// interchangeable content may join as a standby.
3998		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3999		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
4000
4001		// A (shorter chain) wins at equal cost.
4002		let mut source_a = origin
4003			.create_broadcast("test", announce().with_hops(hops_a.clone()))
4004			.unwrap();
4005		let mut dynamic_a = source_a.dynamic();
4006		settle().await;
4007		let broadcast = consumer.request_broadcast("test").await.unwrap();
4008		announced.assert_next_some("test");
4009
4010		let mut watch = broadcast.clone();
4011		assert_eq!(watch.route_changed().await.unwrap().hops, hops_a);
4012
4013		let mut source_b = origin
4014			.create_broadcast("test", announce().with_hops(hops_b.clone()))
4015			.unwrap();
4016		let mut dynamic_b = source_b.dynamic();
4017		settle().await;
4018		assert!(
4019			watch.route_changed().now_or_never().is_none(),
4020			"a losing standby must not change the advertised route"
4021		);
4022
4023		// Dispatch the track to A and deliver a group.
4024		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4025		let mut producer = accept_track(&mut dynamic_a, "video").await;
4026		settle().await;
4027		let mut sub = subscribing.await.unwrap();
4028		producer.append_group().unwrap();
4029		assert_eq!(sub.assert_group().sequence, 0);
4030
4031		// A's cost rises above B's: B takes over at the boundary and the
4032		// broadcast re-advertises B's route. No announce events.
4033		source_a
4034			.set_route(announce().with_hops(hops_a.clone()).with_cost(10))
4035			.unwrap();
4036		settle().await;
4037		assert_eq!(watch.route_changed().await.unwrap().hops, hops_b);
4038		announced.assert_next_wait();
4039
4040		let mut producer_b = accept_track(&mut dynamic_b, "video").await;
4041		settle().await;
4042		// Demand registers as the subscriber polls; the splice boundary bounds
4043		// the range while a live-edge subscriber's demand stays unbounded.
4044		sub.assert_no_group();
4045		assert_eq!(producer_b.subscription().unwrap().group_start, None);
4046		producer_b.create_group(group::Info { sequence: 1 }).unwrap();
4047		assert_eq!(sub.assert_group().sequence, 1);
4048		sub.assert_not_closed();
4049
4050		// The active source updating its own metadata re-advertises in place.
4051		source_b
4052			.set_route(announce().with_hops(hops_b.clone()).with_cost(5))
4053			.unwrap();
4054		settle().await;
4055		let advertised = watch.route_changed().await.unwrap();
4056		assert_eq!(advertised.hops, hops_b);
4057		assert_eq!(advertised.cost, 5);
4058		announced.assert_next_wait();
4059	}
4060
4061	/// A track completed for good must survive later source churn: it is never
4062	/// re-dispatched, and late subscribers still see a clean end.
4063	#[tokio::test]
4064	async fn test_completed_track_survives_route_churn() {
4065		tokio::time::pause();
4066
4067		let origin = Origin::random().produce();
4068		let consumer = origin.consume();
4069
4070		// Shared first hop, so B is a standby rather than a parked replacement.
4071		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4072		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
4073
4074		let source_a = origin.create_broadcast("test", announce().with_hops(hops_a)).unwrap();
4075		let mut dynamic_a = source_a.dynamic();
4076		settle().await;
4077		let source_b = origin.create_broadcast("test", announce().with_hops(hops_b)).unwrap();
4078		let mut dynamic_b = source_b.dynamic();
4079		settle().await;
4080		settle().await;
4081		let broadcast = consumer.request_broadcast("test").await.unwrap();
4082
4083		// Serve the track via A and end it for good.
4084		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4085		let mut producer = accept_track(&mut dynamic_a, "video").await;
4086		settle().await;
4087		let mut sub = subscribing.await.unwrap();
4088		producer.append_group().unwrap();
4089		assert_eq!(sub.assert_group().sequence, 0);
4090		producer.finish().unwrap();
4091		drop(producer);
4092		settle().await;
4093		sub.assert_closed();
4094
4095		// A detaching must not re-dispatch the finished track to B.
4096		source_a.abort(Error::Dropped).unwrap();
4097		drop(dynamic_a);
4098		settle().await;
4099		dynamic_b.assert_no_request();
4100
4101		// A late subscriber sees the same clean end, not an abort.
4102		let mut late = broadcast.track("video").unwrap().subscribe(None).await.unwrap();
4103		late.assert_closed();
4104	}
4105
4106	/// A source rejecting a track ends the logical track immediately, with the
4107	/// source's own error: which tracks a broadcast carries is the publisher's
4108	/// contract, so there is no sweep of other routes and no retry budget.
4109	#[tokio::test]
4110	async fn test_refused_track_aborts_instantly() {
4111		tokio::time::pause();
4112
4113		let origin = Origin::random().produce();
4114		let consumer = origin.consume();
4115
4116		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4117		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4118		let mut dynamic = source.dynamic();
4119		settle().await;
4120		settle().await;
4121		let broadcast = consumer.request_broadcast("test").await.unwrap();
4122
4123		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4124		let request = dynamic.requested_track().await.unwrap();
4125		request.reject(Error::NotFound);
4126		settle().await;
4127
4128		// One refusal is the verdict; the source is never re-asked.
4129		assert!(matches!(subscribing.await, Err(Error::NotFound)));
4130		dynamic.assert_no_request();
4131	}
4132
4133	/// A rejection only rules its source out of the track, so a better route
4134	/// taking over while the original source's info request is pending (and the
4135	/// stale source rejecting at the same moment) rides the handover instead of
4136	/// aborting the subscription.
4137	#[tokio::test]
4138	async fn test_stale_rejection_does_not_abort_a_handover() {
4139		tokio::time::pause();
4140
4141		let origin = Origin::random().produce();
4142		let consumer = origin.consume();
4143
4144		let publisher = Origin::new(1).unwrap();
4145		let peer = Origin::new(5).unwrap();
4146		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
4147		let local = OriginList::try_from(vec![publisher]).unwrap();
4148
4149		// The only route: dispatch parks on its pending info request.
4150		let source_remote = origin
4151			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
4152			.unwrap();
4153		let mut dynamic_remote = source_remote.dynamic();
4154		settle().await;
4155		settle().await;
4156		let broadcast = consumer.request_broadcast("test").await.unwrap();
4157		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4158		let request_remote = dynamic_remote.requested_track().await.unwrap();
4159
4160		// A better route attaches (taking dispatch) while the old source's
4161		// request is still pending; the old source rejects in the same window.
4162		let source_local = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
4163		let mut dynamic_local = source_local.dynamic();
4164		request_remote.reject(Error::NotFound);
4165		settle().await;
4166
4167		// The subscription rides the handover onto the new route.
4168		let mut producer_local = accept_track(&mut dynamic_local, "video").await;
4169		settle().await;
4170		let mut sub = subscribing
4171			.await
4172			.expect("the handover must win over the stale rejection");
4173		producer_local.append_group().unwrap();
4174		assert_eq!(sub.assert_group().sequence, 0);
4175		sub.assert_not_closed();
4176	}
4177
4178	/// A better source attaching mid-subscription takes the track over at an
4179	/// explicit group boundary: the old copy's demand is capped, the new copy
4180	/// starts at the boundary, and the subscriber reads a seamless sequence.
4181	#[tokio::test]
4182	async fn test_route_handover() {
4183		tokio::time::pause();
4184
4185		let origin = Origin::random().produce();
4186		let consumer = origin.consume();
4187		let mut announced = consumer.announced();
4188
4189		// Shared first hop, so the short route joins as an interchangeable source.
4190		let hops_long = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
4191		let hops_short = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4192
4193		let source_a = origin
4194			.create_broadcast("test", announce().with_hops(hops_long))
4195			.unwrap();
4196		let mut dynamic_a = source_a.dynamic();
4197		settle().await;
4198		settle().await;
4199		let broadcast = consumer.request_broadcast("test").await.unwrap();
4200		announced.assert_next_some("test");
4201
4202		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4203		let mut producer_a = accept_track(&mut dynamic_a, "video").await;
4204		settle().await;
4205		let mut sub = subscribing.await.unwrap();
4206		producer_a.append_group().unwrap();
4207		producer_a.append_group().unwrap();
4208		assert_eq!(sub.assert_group().sequence, 0);
4209		assert_eq!(sub.assert_group().sequence, 1);
4210
4211		// A strictly shorter source attaches: the live track is handed over with
4212		// no announce churn.
4213		let source_b = origin
4214			.create_broadcast("test", announce().with_hops(hops_short))
4215			.unwrap();
4216		let mut dynamic_b = source_b.dynamic();
4217		settle().await;
4218		settle().await;
4219		announced.assert_next_wait();
4220
4221		let mut producer_b = accept_track(&mut dynamic_b, "video").await;
4222		settle().await;
4223
4224		// The old copy's demand is capped at the boundary; the new copy keeps
4225		// the subscriber's live-edge demand. Both propagate as the subscriber
4226		// polls.
4227		sub.assert_no_group();
4228		assert_eq!(producer_a.subscription().unwrap().group_end, Some(1));
4229		assert_eq!(producer_b.subscription().unwrap().group_start, None);
4230
4231		// The old copy racing past its cap is filtered; the new copy serves on.
4232		producer_a.create_group(group::Info { sequence: 2 }).unwrap();
4233		producer_b.create_group(group::Info { sequence: 2 }).unwrap();
4234		producer_b.create_group(group::Info { sequence: 3 }).unwrap();
4235		assert_eq!(sub.assert_group().sequence, 2);
4236		assert_eq!(sub.assert_group().sequence, 3);
4237		sub.assert_no_group();
4238		sub.assert_not_closed();
4239	}
4240
4241	/// A graceful detach (deliberate unannounce) closes immediately: no linger, so
4242	/// the unannounce propagates promptly and a re-create is a fresh broadcast.
4243	#[tokio::test(start_paused = true)]
4244	async fn test_route_unannounce_immediate() {
4245		let origin = Origin::random().produce();
4246		let consumer = origin.consume();
4247		let mut announced = consumer.announced();
4248
4249		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4250		let mut source = origin
4251			.create_broadcast("test", announce().with_hops(hops.clone()))
4252			.unwrap();
4253		settle().await;
4254		let broadcast = consumer.request_broadcast("test").await.unwrap();
4255		announced.assert_next_some("test");
4256
4257		// The peer deliberately unannounced: no reconnect window, the broadcast is
4258		// gone as soon as the teardown task observes the close.
4259		source.finish();
4260		settle().await;
4261		announced.assert_next_none("test");
4262
4263		// A re-create at the same path is a brand-new broadcast.
4264		let _source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4265		settle().await;
4266		let fresh = consumer.request_broadcast("test").await.unwrap();
4267		announced.assert_next_some("test");
4268		assert!(
4269			!fresh.is_clone(&broadcast),
4270			"re-create must not splice the old broadcast"
4271		);
4272	}
4273
4274	/// With the default zero linger, a dying source (a session drop, not a
4275	/// deliberate unannounce) closes the broadcast just as promptly as a graceful
4276	/// one: no reconnect window, the tracks abort, and a re-create is a fresh
4277	/// broadcast rather than a splice.
4278	#[tokio::test(start_paused = true)]
4279	async fn test_route_detach_immediate() {
4280		let origin = Origin::random().produce();
4281		let consumer = origin.consume();
4282		let mut announced = consumer.announced();
4283
4284		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4285		let source = origin
4286			.create_broadcast("test", announce().with_hops(hops.clone()))
4287			.unwrap();
4288		let mut dynamic = source.dynamic();
4289		settle().await;
4290		settle().await;
4291		let broadcast = consumer.request_broadcast("test").await.unwrap();
4292		announced.assert_next_some("test");
4293
4294		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4295		let producer = accept_track(&mut dynamic, "video").await;
4296		settle().await;
4297		let mut sub = subscribing.await.unwrap();
4298
4299		// The session dies without unannouncing.
4300		drop(producer);
4301		source.abort(Error::Dropped).unwrap();
4302		drop(dynamic);
4303
4304		settle().await;
4305		announced.assert_next_none("test");
4306		sub.assert_error();
4307
4308		// A reconnecting session gets a brand-new broadcast, not a splice into
4309		// the old one.
4310		let _source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4311		settle().await;
4312		settle().await;
4313		let fresh = consumer.request_broadcast("test").await.unwrap();
4314		announced.assert_next_some("test");
4315		assert!(
4316			!fresh.is_clone(&broadcast),
4317			"re-create must not splice the old broadcast"
4318		);
4319	}
4320
4321	/// A track nobody reads keeps the source's copy for [`TRACK_IDLE_LINGER`], then
4322	/// releases it. Crucially, the release must not immediately re-splice: the same
4323	/// demand signal gates both directions, so an idle track settles instead of
4324	/// re-requesting the track (and its info) every linger.
4325	#[tokio::test(start_paused = true)]
4326	async fn test_idle_track_releases_without_respinning() {
4327		let origin = Info::new(Origin::random()).produce();
4328		let consumer = origin.consume();
4329
4330		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4331		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4332		let mut dynamic = source.dynamic();
4333		settle().await;
4334		let broadcast = consumer.request_broadcast("test").await.unwrap();
4335
4336		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4337		let producer = accept_track(&mut dynamic, "video").await;
4338		settle().await;
4339		let sub = subscribing.await.unwrap();
4340
4341		// The reader leaves, but the copy stays warm inside the window so a viewer
4342		// coming back (or a follow-up fetch) reuses it.
4343		drop(sub);
4344		tokio::time::sleep(TRACK_IDLE_LINGER / 2).await;
4345		settle().await;
4346		assert!(
4347			producer.poll_unused(&kio::Waiter::noop()).is_pending(),
4348			"the copy must stay spliced inside the linger",
4349		);
4350
4351		// Past the window the segment is released, so the serving session sees its
4352		// copy go unused and can drop it (along with the track info).
4353		tokio::time::sleep(TRACK_IDLE_LINGER).await;
4354		settle().await;
4355		assert!(
4356			producer.poll_unused(&kio::Waiter::noop()).is_ready(),
4357			"an idle copy must be released after the linger",
4358		);
4359
4360		// The anti-spin property: the release must not re-arm the splice. Ungated,
4361		// the loop re-attaches the copy immediately and drops it again every linger,
4362		// re-requesting the track (and its info) from the session each time it dies.
4363		for _ in 0..3 {
4364			tokio::time::sleep(TRACK_IDLE_LINGER).await;
4365			settle().await;
4366			assert!(
4367				producer.poll_unused(&kio::Waiter::noop()).is_ready(),
4368				"an unread copy must stay released, not be re-spliced",
4369			);
4370		}
4371		assert!(
4372			dynamic.requested_track().now_or_never().is_none(),
4373			"an unread track must not be re-requested",
4374		);
4375		drop(producer);
4376
4377		// A returning reader re-splices: the origin asks the source for a fresh copy.
4378		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4379		let mut producer = accept_track(&mut dynamic, "video").await;
4380		settle().await;
4381		let mut sub = subscribing.await.unwrap();
4382		producer.append_group().unwrap();
4383		assert_eq!(sub.assert_group().sequence, 0);
4384	}
4385
4386	/// Back-to-back fetches reuse the source's copy: only the first asks the source
4387	/// for the track, so a fetch-driven consumer (HLS pulling segment after segment)
4388	/// doesn't re-request the track, and its `TRACK_INFO`, for every group.
4389	#[tokio::test(start_paused = true)]
4390	async fn test_back_to_back_fetches_reuse_the_track() {
4391		let origin = Info::new(Origin::random()).produce();
4392		let consumer = origin.consume();
4393
4394		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4395		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4396		let mut dynamic = source.dynamic();
4397		settle().await;
4398		let broadcast = consumer.request_broadcast("test").await.unwrap();
4399
4400		// The first fetch has to ask the source for the track.
4401		let fetching = broadcast.track("video").unwrap().fetch_group(0, None);
4402		let mut producer = accept_track(&mut dynamic, "video").await;
4403		producer.append_group().unwrap().finish().unwrap();
4404		settle().await;
4405		let first = fetching.await.expect("first fetch");
4406		drop(first);
4407
4408		// A second fetch inside the linger reuses the copy already spliced in.
4409		settle().await;
4410		let fetching = broadcast.track("video").unwrap().fetch_group(0, None);
4411		settle().await;
4412		assert!(
4413			dynamic.requested_track().now_or_never().is_none(),
4414			"a fetch inside the linger must reuse the track, not re-request it",
4415		);
4416		drop(fetching.await.expect("second fetch"));
4417
4418		// Once the fetches stop, the copy is released like any other idle track.
4419		tokio::time::sleep(TRACK_IDLE_LINGER * 2).await;
4420		settle().await;
4421		assert!(
4422			producer.poll_unused(&kio::Waiter::noop()).is_ready(),
4423			"the copy must be released once the fetches stop",
4424		);
4425		drop(producer);
4426
4427		// And a later fetch re-requests it: `accept_track` times out if it doesn't.
4428		settle().await;
4429		let fetching = broadcast.track("video").unwrap().fetch_group(0, None);
4430		let mut producer = accept_track(&mut dynamic, "video").await;
4431		producer.append_group().unwrap().finish().unwrap();
4432		settle().await;
4433		fetching.await.expect("fetch after the linger");
4434	}
4435
4436	/// With a linger configured, an ungraceful source loss keeps the broadcast
4437	/// alive and announced; a source re-attaching within the window splices in and
4438	/// consumers resume at the group boundary, never observing the outage.
4439	#[tokio::test(start_paused = true)]
4440	async fn test_linger_reconnect_splices() {
4441		let origin = Info::new(Origin::random())
4442			.with_linger(Duration::from_secs(5))
4443			.produce();
4444		let consumer = origin.consume();
4445		let mut announced = consumer.announced();
4446
4447		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4448		let source = origin
4449			.create_broadcast("test", announce().with_hops(hops.clone()))
4450			.unwrap();
4451		let mut dynamic = source.dynamic();
4452		settle().await;
4453		settle().await;
4454		let broadcast = consumer.request_broadcast("test").await.unwrap();
4455		announced.assert_next_some("test");
4456
4457		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4458		let mut producer = accept_track(&mut dynamic, "video").await;
4459		settle().await;
4460		let mut sub = subscribing.await.unwrap();
4461
4462		producer.append_group().unwrap();
4463		producer.append_group().unwrap();
4464		assert_eq!(sub.assert_group().sequence, 0);
4465		assert_eq!(sub.assert_group().sequence, 1);
4466
4467		// The session dies without unannouncing: the broadcast enters the linger
4468		// window instead of closing.
4469		drop(producer);
4470		source.abort(Error::Dropped).unwrap();
4471		drop(dynamic);
4472		settle().await;
4473
4474		// No unannounce, no track error: the outage is invisible so far.
4475		announced.assert_next_wait();
4476		sub.assert_no_group();
4477		sub.assert_not_closed();
4478
4479		// A consumer arriving mid-outage still resolves the lingering broadcast.
4480		let during = consumer.request_broadcast("test").await.unwrap();
4481		assert!(during.is_clone(&broadcast), "the lingering broadcast still resolves");
4482
4483		// The session reconnects within the window: the new source splices into
4484		// the same broadcast.
4485		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4486		let mut dynamic = source.dynamic();
4487		settle().await;
4488		settle().await;
4489		announced.assert_next_wait();
4490		let again = consumer.request_broadcast("test").await.unwrap();
4491		assert!(again.is_clone(&broadcast), "the reconnect must splice, not replace");
4492
4493		// The pending track re-splices from the new source; the boundary keeps
4494		// already-delivered groups filtered while the subscriber's live-edge
4495		// demand survives the splice. Demand registers as the subscriber polls.
4496		let mut producer = accept_track(&mut dynamic, "video").await;
4497		settle().await;
4498		sub.assert_no_group();
4499		assert_eq!(producer.subscription().unwrap().group_start, None);
4500		producer.create_group(group::Info { sequence: 2 }).unwrap();
4501		assert_eq!(sub.assert_group().sequence, 2);
4502		sub.assert_not_closed();
4503	}
4504
4505	/// The linger window expiring without a replacement closes the broadcast: the
4506	/// path unannounces, tracks abort, and a later re-create is a fresh broadcast.
4507	#[tokio::test(start_paused = true)]
4508	async fn test_linger_expiry_closes() {
4509		let origin = Info::new(Origin::random())
4510			.with_linger(Duration::from_secs(5))
4511			.produce();
4512		let consumer = origin.consume();
4513		let mut announced = consumer.announced();
4514
4515		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4516		let source = origin
4517			.create_broadcast("test", announce().with_hops(hops.clone()))
4518			.unwrap();
4519		let mut dynamic = source.dynamic();
4520		settle().await;
4521		settle().await;
4522		let broadcast = consumer.request_broadcast("test").await.unwrap();
4523		announced.assert_next_some("test");
4524
4525		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4526		let producer = accept_track(&mut dynamic, "video").await;
4527		settle().await;
4528		let mut sub = subscribing.await.unwrap();
4529
4530		drop(producer);
4531		source.abort(Error::Dropped).unwrap();
4532		drop(dynamic);
4533		settle().await;
4534		announced.assert_next_wait();
4535
4536		// Nobody comes back: the window expires and the broadcast closes.
4537		tokio::time::sleep(std::time::Duration::from_secs(6)).await;
4538		settle().await;
4539		announced.assert_next_none("test");
4540		sub.assert_error();
4541
4542		// A session reconnecting after the window gets a brand-new broadcast.
4543		let _source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4544		settle().await;
4545		settle().await;
4546		let fresh = consumer.request_broadcast("test").await.unwrap();
4547		announced.assert_next_some("test");
4548		assert!(
4549			!fresh.is_clone(&broadcast),
4550			"a late re-create must not splice the expired broadcast"
4551		);
4552	}
4553
4554	/// A linger too large to represent as a deadline never expires: the broadcast
4555	/// outlives an arbitrarily long outage (a reconnect loop with no give-up
4556	/// timeout promises to retry forever).
4557	#[tokio::test(start_paused = true)]
4558	async fn test_linger_forever() {
4559		let origin = Info::new(Origin::random()).with_linger(Duration::MAX).produce();
4560		let consumer = origin.consume();
4561		let mut announced = consumer.announced();
4562
4563		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4564		let source = origin
4565			.create_broadcast("test", announce().with_hops(hops.clone()))
4566			.unwrap();
4567		settle().await;
4568		let broadcast = consumer.request_broadcast("test").await.unwrap();
4569		announced.assert_next_some("test");
4570
4571		source.abort(Error::Dropped).unwrap();
4572		settle().await;
4573
4574		// Days later the broadcast is still announced and still splices a reconnect.
4575		tokio::time::sleep(std::time::Duration::from_secs(60 * 60 * 24 * 3)).await;
4576		announced.assert_next_wait();
4577		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4578		settle().await;
4579		settle().await;
4580		let again = consumer.request_broadcast("test").await.unwrap();
4581		assert!(again.is_clone(&broadcast), "the reconnect must splice, not replace");
4582		drop(source);
4583	}
4584
4585	/// A lingering broadcast with a live subscription parks, then resumes on the
4586	/// reconnect.
4587	///
4588	/// An ungraceful loss empties the route table while the front waits out its
4589	/// linger, so the route a track is spliced from is gone with no replacement to
4590	/// serve it. The task has to park for the whole window on the strength of
4591	/// dropping the departed route, because nothing else bounds it: a "route gone"
4592	/// edge that keeps firing yields a wait that is Ready on every poll, a full
4593	/// core per subscribed track, and the runtime that has to deliver the
4594	/// reconnect starved along with it.
4595	///
4596	/// Under a paused clock that spin never yields, so a regression hangs here
4597	/// rather than failing.
4598	#[tokio::test(start_paused = true)]
4599	async fn test_linger_parks_a_live_subscription() {
4600		let origin = Info::new(Origin::random())
4601			.with_linger(Duration::from_secs(5))
4602			.produce();
4603		let consumer = origin.consume();
4604
4605		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4606		let source = origin
4607			.create_broadcast("test", announce().with_hops(hops.clone()))
4608			.unwrap();
4609		let mut dynamic = source.dynamic();
4610		settle().await;
4611		settle().await;
4612		let broadcast = consumer.request_broadcast("test").await.unwrap();
4613
4614		// A viewer reading a track that is written once and then stays quiet, like a
4615		// catalog.
4616		let subscribing = broadcast.track("catalog.json").unwrap().subscribe(None);
4617		let mut producer = accept_track(&mut dynamic, "catalog.json").await;
4618		settle().await;
4619		let mut sub = subscribing.await.unwrap();
4620		producer.append_group().unwrap();
4621		assert_eq!(sub.assert_group().sequence, 0);
4622
4623		// The publisher's session dies abruptly: the table empties and the front
4624		// lingers, holding the subscription open for a reconnect. The track copy
4625		// outlives the broadcast (closes don't cascade), so nothing reports the
4626		// spliced segment as ended: the departed route is the only edge left.
4627		source.abort(Error::Dropped).unwrap();
4628		settle().await;
4629		settle().await;
4630		sub.assert_not_closed();
4631
4632		// Most of the window with no source at all: the subscription stays parked
4633		// rather than being cut, which is what the linger promises a reconnect.
4634		tokio::time::sleep(Duration::from_secs(4)).await;
4635		settle().await;
4636		sub.assert_not_closed();
4637
4638		// The reconnect inside the window resumes the same subscription.
4639		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4640		let mut dynamic = source.dynamic();
4641		settle().await;
4642		let mut producer = accept_track(&mut dynamic, "catalog.json").await;
4643		settle().await;
4644		producer.create_group(group::Info { sequence: 1 }).unwrap();
4645		assert_eq!(sub.assert_group().sequence, 1);
4646		sub.assert_not_closed();
4647	}
4648
4649	/// An idle segment is released even once the route that produced it is gone.
4650	///
4651	/// A departed route drops the loop's handle on it, so the idle countdown has to
4652	/// key off the segment itself. Otherwise the segment is stranded for the life of
4653	/// the front, pinning the dead source's cached groups.
4654	///
4655	/// Asserted through the boundary a stranded segment would leave behind, since
4656	/// `resume` is private and delivery is what a viewer actually feels: a released
4657	/// segment set splices the next source unbounded, so its first group arrives,
4658	/// while a retained one caps it below that edge.
4659	#[tokio::test(start_paused = true)]
4660	async fn test_idle_release_survives_the_route_leaving() {
4661		let origin = Info::new(Origin::random())
4662			.with_linger(Duration::from_secs(600))
4663			.produce();
4664		let consumer = origin.consume();
4665
4666		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4667		let source = origin
4668			.create_broadcast("test", announce().with_hops(hops.clone()))
4669			.unwrap();
4670		let mut dynamic = source.dynamic();
4671		settle().await;
4672		settle().await;
4673		let broadcast = consumer.request_broadcast("test").await.unwrap();
4674
4675		let subscribing = broadcast.track("catalog.json").unwrap().subscribe(None);
4676		let mut producer = accept_track(&mut dynamic, "catalog.json").await;
4677		settle().await;
4678		let mut sub = subscribing.await.unwrap();
4679		producer.append_group().unwrap();
4680		assert_eq!(sub.assert_group().sequence, 0);
4681
4682		// The publisher's session dies, then the viewer gives up: nothing holds the
4683		// segment, and nothing is left to serve the track.
4684		source.abort(Error::Dropped).unwrap();
4685		settle().await;
4686		settle().await;
4687		drop(sub);
4688		drop(producer);
4689		drop(dynamic);
4690		settle().await;
4691		tokio::time::sleep(TRACK_IDLE_LINGER + Duration::from_secs(1)).await;
4692		settle().await;
4693
4694		// The publisher reconnects under the same identity, so it joins the front as
4695		// a route rather than replacing it, and restarts its group numbering.
4696		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4697		let mut dynamic = source.dynamic();
4698		settle().await;
4699		settle().await;
4700		let broadcast = consumer.request_broadcast("test").await.unwrap();
4701		let subscribing = broadcast.track("catalog.json").unwrap().subscribe(None);
4702		let mut producer = accept_track(&mut dynamic, "catalog.json").await;
4703		settle().await;
4704		let mut sub = subscribing.await.unwrap();
4705		producer.append_group().unwrap();
4706		assert_eq!(
4707			sub.assert_group().sequence,
4708			0,
4709			"the reconnect's first group must not be filtered by a stale boundary"
4710		);
4711	}
4712
4713	/// A deliberate finish never lingers, even with a linger configured: a clean
4714	/// unannounce from a peer must propagate immediately.
4715	#[tokio::test(start_paused = true)]
4716	async fn test_linger_skipped_on_finish() {
4717		let origin = Info::new(Origin::random())
4718			.with_linger(Duration::from_secs(5))
4719			.produce();
4720		let consumer = origin.consume();
4721		let mut announced = consumer.announced();
4722
4723		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4724		let mut source = origin
4725			.create_broadcast("test", announce().with_hops(hops.clone()))
4726			.unwrap();
4727		settle().await;
4728		let broadcast = consumer.request_broadcast("test").await.unwrap();
4729		announced.assert_next_some("test");
4730
4731		// A clean unannounce: the broadcast is gone as soon as the teardown task
4732		// observes the close, with no reconnect window.
4733		source.finish();
4734		settle().await;
4735		announced.assert_next_none("test");
4736
4737		// A re-create at the same path is a brand-new broadcast.
4738		let _source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4739		settle().await;
4740		let fresh = consumer.request_broadcast("test").await.unwrap();
4741		announced.assert_next_some("test");
4742		assert!(
4743			!fresh.is_clone(&broadcast),
4744			"a finish must not leave a lingering broadcast to splice into"
4745		);
4746	}
4747
4748	/// A non-live broadcast is reachable by exact path but never announced;
4749	/// toggling `live` announces and unannounces without touching the broadcast.
4750	#[tokio::test]
4751	async fn test_announce_toggle() {
4752		tokio::time::pause();
4753
4754		let origin = Origin::random().produce();
4755		let consumer = origin.consume();
4756		let mut announced = consumer.announced();
4757
4758		let mut source = origin.create_broadcast("test", broadcast::Route::new()).unwrap();
4759		settle().await;
4760
4761		// Routable but not announced.
4762		announced.assert_next_wait();
4763		let broadcast = consumer
4764			.get_broadcast("test")
4765			.expect("offline broadcast is still routable");
4766		assert!(!broadcast.route().announce);
4767
4768		// request_broadcast resolves the offline broadcast too.
4769		let requested = consumer.request_broadcast("test").await.unwrap();
4770		assert!(requested.is_clone(&broadcast));
4771
4772		// Going live announces.
4773		source.set_route(announce()).unwrap();
4774		settle().await;
4775		let face = announced.assert_next_some("test");
4776		assert!(face.is_clone(&broadcast));
4777
4778		// A fresh consumer replays only announced broadcasts.
4779		let mut fresh = origin.consume().announced();
4780		fresh.assert_next_some("test");
4781		fresh.assert_next_wait();
4782
4783		// Going offline unannounces but stays routable.
4784		source.set_route(broadcast::Route::new()).unwrap();
4785		settle().await;
4786		announced.assert_next_none("test");
4787		assert!(consumer.get_broadcast("test").is_some());
4788		let mut fresh = origin.consume().announced();
4789		fresh.assert_next_wait();
4790
4791		source.finish();
4792		settle().await;
4793		assert!(consumer.get_broadcast("test").is_none());
4794	}
4795
4796	/// An announced source outranks a cheaper offline one, so the broadcast
4797	/// stays announced and serves from it.
4798	#[tokio::test]
4799	async fn test_announce_beats_offline() {
4800		tokio::time::pause();
4801
4802		let origin = Origin::random().produce();
4803		let consumer = origin.consume();
4804		let mut announced = consumer.announced();
4805
4806		// An unannounced source with the best cost.
4807		let _offline = origin.create_broadcast("test", broadcast::Route::new()).unwrap();
4808		settle().await;
4809		announced.assert_next_wait();
4810
4811		// An announced source with a worse cost still wins: the path announces
4812		// and advertises its route.
4813		let mut announced_source = origin.create_broadcast("test", announce().with_cost(10)).unwrap();
4814		settle().await;
4815		announced.assert_next_some("test");
4816		let face = consumer.get_broadcast("test").unwrap();
4817		assert!(face.route().announce);
4818		assert_eq!(face.route().cost, 10);
4819
4820		// The announced source leaving falls back to the offline one: the path
4821		// unannounces but stays routable.
4822		announced_source.finish();
4823		settle().await;
4824		announced.assert_next_none("test");
4825		assert!(consumer.get_broadcast("test").is_some());
4826	}
4827
4828	/// A better source attaching does not churn announces: the broadcast identity
4829	/// is origin-owned, so the swap is invisible to consumers.
4830	#[tokio::test]
4831	async fn test_better_source_no_churn() {
4832		tokio::time::pause();
4833
4834		let origin = Origin::random().produce();
4835		let mut announced = origin.consume().announced();
4836
4837		// `a` carries two hops; `b` reaches the same publisher in one, so `b`
4838		// wins dispatch when it joins.
4839		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
4840		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4841		let _a = origin.create_broadcast("test", announce().with_hops(hops_a)).unwrap();
4842		settle().await;
4843		let face = announced.assert_next_some("test");
4844
4845		let _b = origin
4846			.create_broadcast("test", announce().with_hops(hops_b.clone()))
4847			.unwrap();
4848		settle().await;
4849		announced.assert_next_wait();
4850		let current = origin.consume().get_broadcast("test").unwrap();
4851		assert!(current.is_clone(&face), "the broadcast identity must not change");
4852		// The face now advertises the winning (shorter) route.
4853		assert_eq!(current.route().hops, hops_b);
4854	}
4855
4856	/// A second source with a different original publisher (first hop) is new
4857	/// content, not a standby: it must not splice into the incumbent's
4858	/// subscribers. It takes the path over immediately, as a real unannounce +
4859	/// announce, rather than waiting out an incumbent whose session may only be
4860	/// alive because the transport has not timed it out yet.
4861	#[tokio::test]
4862	async fn test_publisher_mismatch_replaces() {
4863		tokio::time::pause();
4864
4865		let origin = Origin::random().produce();
4866		let consumer = origin.consume();
4867		let mut announced = consumer.announced();
4868
4869		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4870		let hops_b = OriginList::try_from(vec![Origin::new(2).unwrap()]).unwrap();
4871
4872		let mut source_a = origin
4873			.create_broadcast("test", announce().with_hops(hops_a.clone()))
4874			.unwrap();
4875		settle().await;
4876		let face_a = announced.assert_next_some("test");
4877
4878		// A different publisher at the same path: the newcomer wins it now, while
4879		// the incumbent is still attached and still believes it is publishing.
4880		let _source_b = origin
4881			.create_broadcast("test", announce().with_hops(hops_b.clone()))
4882			.unwrap();
4883		settle().await;
4884		settle().await;
4885		announced.assert_next_none("test");
4886		let face_b = announced.assert_next_some("test");
4887		assert!(!face_b.is_clone(&face_a), "a replacement, never a splice");
4888		assert_eq!(consumer.get_broadcast("test").unwrap().route().hops, hops_b);
4889		// The displaced front is torn down, not merely unpublished: leaving it
4890		// running would strand its subscribers and its source watchers on a face
4891		// nothing can reach.
4892		assert!(face_a.is_closed(), "the displaced front must close");
4893
4894		// The displaced incumbent ending is invisible: it no longer owns the path.
4895		source_a.finish();
4896		settle().await;
4897		settle().await;
4898		announced.assert_next_wait();
4899		assert_eq!(consumer.get_broadcast("test").unwrap().route().hops, hops_b);
4900	}
4901
4902	/// A displaced live publisher stands by rather than fighting for the path back,
4903	/// then reclaims it once its replacement leaves.
4904	#[tokio::test]
4905	async fn test_displaced_publisher_reclaims_path_when_replacement_leaves() {
4906		tokio::time::pause();
4907
4908		let origin = Origin::random().produce();
4909		let consumer = origin.consume();
4910		let mut announced = consumer.announced();
4911
4912		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4913		let hops_b = OriginList::try_from(vec![Origin::new(2).unwrap()]).unwrap();
4914
4915		// A announces and is live.
4916		let mut source_a = origin
4917			.create_broadcast("test", announce().with_hops(hops_a.clone()))
4918			.unwrap();
4919		settle().await;
4920		announced.assert_next_some("test");
4921
4922		// A different publisher announces the same path, displacing A even though
4923		// A's session is still open.
4924		let mut source_b = origin.create_broadcast("test", announce().with_hops(hops_b)).unwrap();
4925		settle().await;
4926		settle().await;
4927		announced.assert_next_none("test");
4928		announced.assert_next_some("test");
4929		// Exactly one handover: A stands by on its unchanged route instead of
4930		// taking the path straight back, which would trade announces forever.
4931		announced.assert_next_wait();
4932
4933		// B disconnects. A is still an open, live broadcast producer.
4934		source_b.finish();
4935		settle().await;
4936		settle().await;
4937
4938		assert!(
4939			consumer.get_broadcast("test").is_some(),
4940			"the still-live publisher A should reclaim the path once its replacement leaves"
4941		);
4942		let recovered = consumer.request_broadcast("test").await.unwrap();
4943		assert_eq!(recovered.route().hops, hops_a);
4944
4945		announced.assert_next_none("test");
4946		announced.assert_next_some("test");
4947		announced.assert_next_wait();
4948
4949		source_a.finish();
4950	}
4951
4952	/// A source that was displaced and later reclaimed the path must still be able
4953	/// to take the path over when its own route moves to a new publisher, even
4954	/// though a sibling source keeps the old front alive.
4955	#[tokio::test]
4956	async fn test_reclaimed_publisher_can_still_replace_itself() {
4957		tokio::time::pause();
4958
4959		let origin = Origin::random().produce();
4960		let consumer = origin.consume();
4961
4962		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4963		let hops_b = OriginList::try_from(vec![Origin::new(2).unwrap()]).unwrap();
4964		let hops_c = OriginList::try_from(vec![Origin::new(3).unwrap()]).unwrap();
4965
4966		// Two sources sharing a publisher splice into one front.
4967		let mut source_a1 = origin
4968			.create_broadcast("test", announce().with_hops(hops_a.clone()))
4969			.unwrap();
4970		let mut source_a2 = origin
4971			.create_broadcast("test", announce().with_hops(hops_a.clone()))
4972			.unwrap();
4973		settle().await;
4974		settle().await;
4975		assert_eq!(consumer.get_broadcast("test").unwrap().route().hops, hops_a);
4976
4977		// A different publisher takes the path; both A sources stand by behind it.
4978		let mut source_b = origin.create_broadcast("test", announce().with_hops(hops_b)).unwrap();
4979		settle().await;
4980		settle().await;
4981
4982		// B leaves and the A sources reclaim the path, having spent their attempt.
4983		source_b.finish();
4984		settle().await;
4985		settle().await;
4986		assert_eq!(consumer.get_broadcast("test").unwrap().route().hops, hops_a);
4987
4988		// A1 now moves to a new publisher, which is a fresh route observation. Its
4989		// sibling A2 keeps the old front open, so this attach is a replacement, and
4990		// a publisher swap is always a replacement rather than a standby.
4991		source_a1.set_route(announce().with_hops(hops_c.clone())).unwrap();
4992		settle().await;
4993		settle().await;
4994		assert_eq!(
4995			consumer.get_broadcast("test").unwrap().route().hops,
4996			hops_c,
4997			"the new publisher must take the path over, not stand by behind the old front"
4998		);
4999
5000		source_a1.finish();
5001		source_a2.finish();
5002	}
5003
5004	/// A repricing is not new content: a standby source must not use a cost-only
5005	/// route update to evict the live front it already lost to.
5006	#[tokio::test]
5007	async fn test_repricing_does_not_earn_a_takeover() {
5008		tokio::time::pause();
5009
5010		let origin = Origin::random().produce();
5011		let consumer = origin.consume();
5012		let mut announced = consumer.announced();
5013
5014		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
5015		let hops_b = OriginList::try_from(vec![Origin::new(2).unwrap()]).unwrap();
5016
5017		let mut source_a = origin
5018			.create_broadcast("test", announce().with_hops(hops_a.clone()).with_cost(5))
5019			.unwrap();
5020		settle().await;
5021		announced.assert_next_some("test");
5022
5023		// B displaces A; A stands by.
5024		let mut source_b = origin
5025			.create_broadcast("test", announce().with_hops(hops_b.clone()))
5026			.unwrap();
5027		settle().await;
5028		settle().await;
5029		announced.assert_next_none("test");
5030		announced.assert_next_some("test");
5031		announced.assert_next_wait();
5032
5033		// A's peer re-announces it at a new cost. Same publisher, same content:
5034		// nothing about this says A should own the path again.
5035		source_a
5036			.set_route(announce().with_hops(hops_a.clone()).with_cost(9))
5037			.unwrap();
5038		settle().await;
5039		settle().await;
5040		assert_eq!(
5041			consumer.get_broadcast("test").unwrap().route().hops,
5042			hops_b,
5043			"a repricing must not take the path back from the live front"
5044		);
5045		announced.assert_next_wait();
5046
5047		source_a.finish();
5048		source_b.finish();
5049	}
5050
5051	/// A publisher reconnecting under the same identity attaches as a second
5052	/// route with an identical hop chain and cost. The new session is the one
5053	/// actually carrying frames, so it must win selection immediately rather than
5054	/// waiting for the transport to retire the old one.
5055	#[tokio::test]
5056	async fn test_reconnect_wins_over_stale_route() {
5057		tokio::time::pause();
5058
5059		let origin = Origin::random().produce();
5060		let consumer = origin.consume();
5061
5062		let publisher = Origin::new(1).unwrap();
5063		let hops = OriginList::try_from(vec![publisher]).unwrap();
5064
5065		// The original session, still attached: its QUIC connection has not been
5066		// declared dead yet.
5067		let stale = origin
5068			.create_broadcast("test", announce().with_hops(hops.clone()))
5069			.unwrap();
5070		let mut stale_dynamic = stale.dynamic();
5071		settle().await;
5072
5073		// The same publisher reconnecting over a fresh session.
5074		let fresh = origin
5075			.create_broadcast("test", announce().with_hops(hops.clone()))
5076			.unwrap();
5077		let mut fresh_dynamic = fresh.dynamic();
5078		settle().await;
5079		settle().await;
5080
5081		// Track requests dispatch to the reconnect, not the corpse.
5082		let broadcast = consumer.request_broadcast("test").await.unwrap();
5083		let subscribing = broadcast.track("video").unwrap().subscribe(None);
5084		settle().await;
5085		let _producer = accept_track(&mut fresh_dynamic, "video").await;
5086		settle().await;
5087		subscribing.await.unwrap();
5088		stale_dynamic.assert_no_request();
5089	}
5090
5091	/// The same reconnect, but arriving while a subscription is already spliced
5092	/// onto the stale route: the live track must re-splice onto the new session
5093	/// rather than ride the dead one until the transport gives up. The gate
5094	/// [`FrontState::reselect`] applies while carrying is pinned separately, in
5095	/// [`test_carrying_switches_to_benign_routes`].
5096	#[tokio::test]
5097	async fn test_carrying_reconnect_switches_immediately() {
5098		tokio::time::pause();
5099
5100		let origin = Origin::random().produce();
5101		let consumer = origin.consume();
5102
5103		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
5104
5105		let stale = origin
5106			.create_broadcast("test", announce().with_hops(hops.clone()))
5107			.unwrap();
5108		let mut stale_dynamic = stale.dynamic();
5109		settle().await;
5110
5111		// A live subscription riding the original session.
5112		let broadcast = consumer.request_broadcast("test").await.unwrap();
5113		let subscribing = broadcast.track("video").unwrap().subscribe(None);
5114		settle().await;
5115		let _stale_producer = accept_track(&mut stale_dynamic, "video").await;
5116		settle().await;
5117		// Held: the splice only follows the active route while the track is read.
5118		let _subscription = subscribing.await.unwrap();
5119
5120		// The reconnect arrives with the front already carrying.
5121		let fresh = origin
5122			.create_broadcast("test", announce().with_hops(hops.clone()))
5123			.unwrap();
5124		let mut fresh_dynamic = fresh.dynamic();
5125		settle().await;
5126		settle().await;
5127
5128		// The carrying front re-splices onto the reconnect rather than waiting for
5129		// the stale session to die.
5130		let _fresh_producer = accept_track(&mut fresh_dynamic, "video").await;
5131	}
5132
5133	/// Taking the path over is scoped to a newcomer that would actually outrank
5134	/// the incumbent. An offline source (a cache, or an on-demand handler) is
5135	/// ranked below every announced route, so arriving under a different
5136	/// publisher must not unannounce a live broadcast and cut its subscribers.
5137	///
5138	/// The tail of this test covers the park's *exit*: the parked source has to
5139	/// wake and attach once the incumbent ends. Nothing else drives that wait, so
5140	/// without this a lost wakeup would strand the source invisibly forever
5141	/// rather than failing anything.
5142	#[tokio::test]
5143	async fn test_offline_mismatch_never_evicts_a_live_front() {
5144		tokio::time::pause();
5145
5146		let origin = Origin::random().produce();
5147		let consumer = origin.consume();
5148		let mut announced = consumer.announced();
5149
5150		let hops_live = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
5151		let hops_cache = OriginList::try_from(vec![Origin::new(2).unwrap()]).unwrap();
5152
5153		let mut live = origin
5154			.create_broadcast("test", announce().with_hops(hops_live.clone()))
5155			.unwrap();
5156		let mut live_dynamic = live.dynamic();
5157		settle().await;
5158		let face = announced.assert_next_some("test");
5159
5160		// A subscriber riding the live broadcast, which the eviction would cut.
5161		let broadcast = consumer.request_broadcast("test").await.unwrap();
5162		let subscribing = broadcast.track("video").unwrap().subscribe(None);
5163		settle().await;
5164		let _producer = accept_track(&mut live_dynamic, "video").await;
5165		settle().await;
5166		subscribing.await.unwrap();
5167
5168		// An offline source for different content at the same path: it stays
5169		// invisible rather than displacing the live one.
5170		let cache = origin
5171			.create_broadcast("test", broadcast::Route::new().with_hops(hops_cache.clone()))
5172			.unwrap();
5173		settle().await;
5174		settle().await;
5175		announced.assert_next_wait();
5176		assert!(!face.is_closed(), "the live front must survive");
5177		assert_eq!(consumer.get_broadcast("test").unwrap().route().hops, hops_live);
5178
5179		// The incumbent ending hands the path to the parked source, which is the
5180		// only thing that ever wakes that wait.
5181		live.finish();
5182		settle().await;
5183		settle().await;
5184		announced.assert_next_none("test");
5185		let taken = consumer
5186			.get_broadcast("test")
5187			.expect("the parked source must take over");
5188		assert_eq!(taken.route().hops, hops_cache);
5189		// Offline, so it holds the path without being advertised.
5190		announced.assert_next_wait();
5191		drop(cache);
5192	}
5193
5194	/// A subscription from a peer the active chain flows through is served from
5195	/// the best clean source directly (data-plane split horizon), while every
5196	/// other consumer keeps the shared spliced broadcast fed by the active
5197	/// source. The two selections match what the announce loop advertises, so
5198	/// the data plane keeps the control plane's promise.
5199	#[tokio::test]
5200	async fn test_dispatch_excludes_requester() {
5201		tokio::time::pause();
5202
5203		let origin = Origin::random().produce();
5204		let consumer = origin.consume();
5205
5206		let peer = Origin::new(5).unwrap();
5207		let publisher = Origin::new(1).unwrap();
5208		// The route through the peer is cheaper, so it is the active source.
5209		let tainted = OriginList::try_from(vec![publisher, peer]).unwrap();
5210		let clean = OriginList::try_from(vec![publisher]).unwrap();
5211
5212		let source_a = origin.create_broadcast("test", announce().with_hops(tainted)).unwrap();
5213		let mut dynamic_a = source_a.dynamic();
5214		settle().await;
5215		let source_b = origin
5216			.create_broadcast("test", announce().with_hops(clean).with_cost(5))
5217			.unwrap();
5218		let mut dynamic_b = source_b.dynamic();
5219		settle().await;
5220		settle().await;
5221
5222		// An ordinary consumer rides the shared front, dispatched to the active
5223		// (peer-tainted) source.
5224		let shared = consumer.request_broadcast("test").await.unwrap();
5225		let subscribing = shared.track("video").unwrap().subscribe(None);
5226		let _producer_a = accept_track(&mut dynamic_a, "video").await;
5227		settle().await;
5228		subscribing.await.unwrap();
5229
5230		// The peer is pinned to the clean source. Crucially, nothing reaches the
5231		// via-peer source: a track request on it is what a session would forward
5232		// upstream as a SUBSCRIBE, and forwarding this one would send the peer's
5233		// own subscription back to them.
5234		let scoped = consumer.clone().excluding(peer);
5235		let pinned = scoped.request_broadcast("test").await.unwrap();
5236		let subscribing = pinned.track("video").unwrap().subscribe(None);
5237		let _producer_b = accept_track(&mut dynamic_b, "video").await;
5238		settle().await;
5239		subscribing.await.unwrap();
5240		dynamic_a.assert_no_request();
5241	}
5242
5243	/// Two publishers that never declared an identity both arrive with a first
5244	/// hop of UNKNOWN. They are unrelated content, so the second MUST replace the
5245	/// first rather than joining it as an interchangeable standby: splicing them
5246	/// would cut one publisher's subscribers over to the other's stream.
5247	#[tokio::test]
5248	async fn test_unknown_publishers_do_not_splice() {
5249		tokio::time::pause();
5250
5251		let origin = Origin::random().produce();
5252		let consumer = origin.consume();
5253		let mut announced = consumer.announced();
5254
5255		let unknown_a = OriginList::try_from(vec![Origin::UNKNOWN]).unwrap();
5256		let unknown_b = OriginList::try_from(vec![Origin::UNKNOWN]).unwrap();
5257
5258		let mut source_a = origin
5259			.create_broadcast("test", announce().with_hops(unknown_a.clone()))
5260			.unwrap();
5261		settle().await;
5262		settle().await;
5263		announced.assert_next_some("test");
5264
5265		// Same first hop by value, but it identifies nothing, so this is a
5266		// replacement: the path is unannounced and re-announced rather than
5267		// silently gaining a standby.
5268		let source_b = origin
5269			.create_broadcast("test", announce().with_hops(unknown_b))
5270			.unwrap();
5271		settle().await;
5272		settle().await;
5273		announced.assert_next_none("test");
5274		let live = announced.assert_next_some("test");
5275
5276		// Repricing the displaced UNKNOWN source is still not evidence of a new
5277		// publisher. It remains parked behind the replacement.
5278		source_a
5279			.set_route(announce().with_hops(unknown_a).with_cost(9))
5280			.unwrap();
5281		settle().await;
5282		settle().await;
5283		assert!(
5284			consumer.get_broadcast("test").unwrap().is_clone(&live),
5285			"UNKNOWN-to-UNKNOWN repricing must not replace the live front"
5286		);
5287		announced.assert_next_wait();
5288
5289		drop(source_a);
5290		drop(source_b);
5291	}
5292
5293	/// The same two sources under a real shared publisher id do splice, which is
5294	/// what keeps the rule above about UNKNOWN rather than about hop chains.
5295	#[tokio::test]
5296	async fn test_known_publishers_still_splice() {
5297		tokio::time::pause();
5298
5299		let origin = Origin::random().produce();
5300		let consumer = origin.consume();
5301		let mut announced = consumer.announced();
5302
5303		let publisher = Origin::new(1).unwrap();
5304		let hops_a = OriginList::try_from(vec![publisher]).unwrap();
5305		let hops_b = OriginList::try_from(vec![publisher, Origin::new(3).unwrap()]).unwrap();
5306
5307		let source_a = origin.create_broadcast("test", announce().with_hops(hops_a)).unwrap();
5308		settle().await;
5309		settle().await;
5310		announced.assert_next_some("test");
5311
5312		// Shares the publisher, so it joins silently as a standby: no churn.
5313		let source_b = origin.create_broadcast("test", announce().with_hops(hops_b)).unwrap();
5314		settle().await;
5315		settle().await;
5316		announced.assert_next_wait();
5317
5318		drop(source_a);
5319		drop(source_b);
5320	}
5321
5322	/// A local standby with the same original publisher joining a front that is
5323	/// carrying the broadcast from a peer must splice the live subscription onto
5324	/// the new source, never tear it down (#2473, e2e finding 2). Redundant
5325	/// publishers sharing an origin id MUST produce the same tracks, so the
5326	/// splice resumes seamlessly at the group boundary.
5327	#[tokio::test]
5328	async fn test_standby_join_splices_live_subscriber() {
5329		tokio::time::pause();
5330
5331		let origin = Origin::random().produce();
5332		let consumer = origin.consume();
5333
5334		let publisher = Origin::new(1).unwrap();
5335		let peer = Origin::new(5).unwrap();
5336		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
5337		let local = OriginList::try_from(vec![publisher]).unwrap();
5338
5339		// Carrying via the peer, with a live subscriber mid-stream.
5340		let source_remote = origin
5341			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
5342			.unwrap();
5343		let mut dynamic_remote = source_remote.dynamic();
5344		settle().await;
5345		settle().await;
5346		let broadcast = consumer.request_broadcast("test").await.unwrap();
5347		let subscribing = broadcast.track("video").unwrap().subscribe(None);
5348		let mut producer_remote = accept_track(&mut dynamic_remote, "video").await;
5349		settle().await;
5350		let mut sub = subscribing.await.unwrap();
5351		producer_remote.append_group().unwrap();
5352		assert_eq!(sub.assert_group().sequence, 0);
5353
5354		// The local standby joins with the same first hop and a cheaper route:
5355		// it wins dispatch and the live track re-splices at the boundary,
5356		// keeping the subscriber's live-edge demand.
5357		let source_local = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
5358		let mut dynamic_local = source_local.dynamic();
5359		settle().await;
5360		let mut producer_local = accept_track(&mut dynamic_local, "video").await;
5361		settle().await;
5362		sub.assert_no_group();
5363		assert_eq!(producer_local.subscription().unwrap().group_start, None);
5364		producer_local.create_group(group::Info { sequence: 1 }).unwrap();
5365		assert_eq!(sub.assert_group().sequence, 1);
5366		sub.assert_not_closed();
5367	}
5368
5369	/// Reselect is decided per track, so a standby carrying only some of the
5370	/// broadcast's tracks splices the ones it has and leaves the rest on the
5371	/// incumbent. This is the divergent-layout case for a 1+1 pair: two sources
5372	/// that are meant to be interchangeable but do not agree on the track list
5373	/// must degrade to a per-track split rather than taking the whole broadcast
5374	/// with them.
5375	#[tokio::test]
5376	async fn test_standby_with_a_partial_track_list_splits_per_track() {
5377		tokio::time::pause();
5378
5379		let origin = Origin::random().produce();
5380		let consumer = origin.consume();
5381
5382		let publisher = Origin::new(1).unwrap();
5383		let peer = Origin::new(5).unwrap();
5384		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
5385		let local = OriginList::try_from(vec![publisher]).unwrap();
5386
5387		// The incumbent carries both tracks, with live subscribers mid-stream.
5388		let source_remote = origin
5389			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
5390			.unwrap();
5391		let mut dynamic_remote = source_remote.dynamic();
5392		settle().await;
5393		settle().await;
5394		let broadcast = consumer.request_broadcast("test").await.unwrap();
5395
5396		let subscribing_video = broadcast.track("video").unwrap().subscribe(None);
5397		let subscribing_audio = broadcast.track("audio").unwrap().subscribe(None);
5398		let mut producers_remote = accept_tracks(&mut dynamic_remote, 2).await;
5399		settle().await;
5400
5401		let mut sub_video = subscribing_video.await.unwrap();
5402		let mut sub_audio = subscribing_audio.await.unwrap();
5403		for name in ["video", "audio"] {
5404			producers_remote.get_mut(name).unwrap().append_group().unwrap();
5405		}
5406		assert_eq!(sub_video.assert_group().sequence, 0);
5407		assert_eq!(sub_audio.assert_group().sequence, 0);
5408
5409		// The cheaper standby joins and wins dispatch, but only has "video".
5410		let source_local = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
5411		let mut dynamic_local = source_local.dynamic();
5412		settle().await;
5413
5414		let mut producer_local = None;
5415		for _ in 0..2 {
5416			let request = tokio::time::timeout(std::time::Duration::from_secs(1), dynamic_local.requested_track())
5417				.await
5418				.expect("timed out waiting for a track request")
5419				.expect("source closed");
5420			match request.name() {
5421				"video" => producer_local = Some(request.accept(None)),
5422				"audio" => request.reject(Error::NotFound),
5423				other => panic!("unexpected track dispatched: {other}"),
5424			}
5425		}
5426		settle().await;
5427		let mut producer_local = producer_local.expect("the standby was never asked for video");
5428
5429		// Video re-splices onto the standby at the boundary, keeping the
5430		// subscriber's live-edge demand.
5431		sub_video.assert_no_group();
5432		assert_eq!(producer_local.subscription().unwrap().group_start, None);
5433		producer_local.create_group(group::Info { sequence: 1 }).unwrap();
5434		assert_eq!(
5435			sub_video.assert_group().sequence,
5436			1,
5437			"video did not move to the standby"
5438		);
5439
5440		// Audio is untouched: the refusal costs the incumbent nothing.
5441		producers_remote.get_mut("audio").unwrap().append_group().unwrap();
5442		assert_eq!(
5443			sub_audio.assert_group().sequence,
5444			1,
5445			"audio did not stay on the incumbent"
5446		);
5447		sub_video.assert_not_closed();
5448		sub_audio.assert_not_closed();
5449	}
5450
5451	/// A standby that wins dispatch before creating a track must not kill a
5452	/// subscription the incumbent is serving: its refusal only rules it out of
5453	/// this track, and the incumbent keeps delivering. The refusal is still
5454	/// never retried: once the incumbent goes away every remaining source has
5455	/// refused, so the subscription aborts and the consumer's next request asks
5456	/// afresh.
5457	#[tokio::test]
5458	async fn test_standby_missing_track_keeps_incumbent() {
5459		tokio::time::pause();
5460
5461		let origin = Origin::random().produce();
5462		let consumer = origin.consume();
5463
5464		let publisher = Origin::new(1).unwrap();
5465		let peer = Origin::new(5).unwrap();
5466		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
5467		let local = OriginList::try_from(vec![publisher]).unwrap();
5468
5469		// Carrying via the peer, with a live subscriber mid-stream.
5470		let source_remote = origin
5471			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
5472			.unwrap();
5473		let mut dynamic_remote = source_remote.dynamic();
5474		settle().await;
5475		settle().await;
5476		let broadcast = consumer.request_broadcast("test").await.unwrap();
5477		let subscribing = broadcast.track("audio").unwrap().subscribe(None);
5478		let mut producer_remote = accept_track(&mut dynamic_remote, "audio").await;
5479		settle().await;
5480		let mut sub = subscribing.await.unwrap();
5481		producer_remote.append_group().unwrap();
5482		assert_eq!(sub.assert_group().sequence, 0);
5483
5484		// The standby joins and wins dispatch, but has not created "audio" yet.
5485		// Its refusal must cost the incumbent nothing.
5486		let source_local = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
5487		let mut dynamic_local = source_local.dynamic();
5488		settle().await;
5489		let request = dynamic_local.requested_track().await.unwrap();
5490		assert_eq!(request.name(), "audio");
5491		request.reject(Error::NotFound);
5492		settle().await;
5493
5494		// Still spliced to the incumbent, still delivering.
5495		producer_remote.append_group().unwrap();
5496		assert_eq!(sub.assert_group().sequence, 1);
5497		sub.assert_not_closed();
5498
5499		// The incumbent leaving exhausts the table (the standby's refusal is
5500		// never retried): the subscription aborts.
5501		source_remote.abort(Error::Dropped).unwrap();
5502		settle().await;
5503		settle().await;
5504		sub.assert_closed();
5505		dynamic_local.assert_no_request();
5506
5507		// A fresh consumer request asks the standby anew, which has the track now.
5508		let retry = broadcast.track("audio").unwrap().subscribe(None);
5509		let mut producer_local = accept_track(&mut dynamic_local, "audio").await;
5510		settle().await;
5511		let mut sub = retry.await.expect("a fresh request must reach the standby");
5512		producer_local.create_group(group::Info { sequence: 2 }).unwrap();
5513		assert_eq!(sub.assert_group().sequence, 2);
5514	}
5515
5516	/// A refused track aborts, but the verdict is not cached: it belongs to the
5517	/// request that received it, so a later request re-asks the source. Otherwise
5518	/// one early request for a track the publisher had not created yet leaves the
5519	/// name dead for the life of the front.
5520	#[tokio::test]
5521	async fn test_unservable_track_retried_by_a_later_request() {
5522		tokio::time::pause();
5523
5524		let origin = Origin::random().produce();
5525		let consumer = origin.consume();
5526
5527		let source = origin.create_broadcast("test", announce()).unwrap();
5528		let mut dynamic = source.dynamic();
5529		settle().await;
5530		settle().await;
5531		let broadcast = consumer.request_broadcast("test").await.unwrap();
5532
5533		// Nothing serves it: the refusal aborts the track with the source's error.
5534		let subscribing = broadcast.track("audio").unwrap().subscribe(None);
5535		let request = dynamic.requested_track().await.unwrap();
5536		request.reject(Error::NotFound);
5537		settle().await;
5538		assert!(matches!(subscribing.await, Err(Error::NotFound)));
5539
5540		// The publisher has the track now; a fresh request must reach it.
5541		let retry = broadcast.track("audio").unwrap().subscribe(None);
5542		let mut producer = accept_track(&mut dynamic, "audio").await;
5543		settle().await;
5544		let mut sub = retry.await.expect("a fresh request must reach the source");
5545		producer.append_group().unwrap();
5546		assert_eq!(sub.assert_group().sequence, 0);
5547	}
5548
5549	/// A copy that dies before delivering anything is a refusal, not a failover:
5550	/// a source whose track keeps dying right after acceptance must not
5551	/// re-splice forever. With every attached source refused, the track aborts
5552	/// with the copy's error, and the verdict belongs to that request: a fresh
5553	/// consumer request asks again.
5554	#[tokio::test]
5555	async fn test_track_dying_without_progress_aborts() {
5556		tokio::time::pause();
5557
5558		let origin = Origin::random().produce();
5559		let consumer = origin.consume();
5560
5561		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
5562		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
5563		let mut dynamic = source.dynamic();
5564		settle().await;
5565		settle().await;
5566		let broadcast = consumer.request_broadcast("test").await.unwrap();
5567
5568		let subscribing = broadcast.track("video").unwrap().subscribe(None);
5569		let producer = accept_track(&mut dynamic, "video").await;
5570		settle().await;
5571		let mut sub = subscribing.await.unwrap();
5572
5573		// The copy dies without delivering a single group, from a source that is
5574		// alive and well: that's its answer for the track, not a failover.
5575		drop(producer);
5576		settle().await;
5577		sub.assert_closed();
5578		dynamic.assert_no_request();
5579
5580		// A fresh request asks the (still attached) source anew.
5581		let retry = broadcast.track("video").unwrap().subscribe(None);
5582		let mut producer = accept_track(&mut dynamic, "video").await;
5583		settle().await;
5584		let mut sub = retry.await.expect("a fresh request must reach the source");
5585		producer.append_group().unwrap();
5586		assert_eq!(sub.assert_group().sequence, 0);
5587	}
5588
5589	/// A copy that delivered and later died is a failover even when unrelated
5590	/// wakes happened between its last group and its death: progress is tracked
5591	/// per splice, so a demand edge must not launder it into a zero-progress
5592	/// refusal that aborts the only route.
5593	#[tokio::test]
5594	async fn test_delivered_copy_death_survives_unrelated_wakes() {
5595		tokio::time::pause();
5596
5597		let origin = Origin::random().produce();
5598		let consumer = origin.consume();
5599
5600		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
5601		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
5602		let mut dynamic = source.dynamic();
5603		settle().await;
5604		settle().await;
5605		let broadcast = consumer.request_broadcast("test").await.unwrap();
5606
5607		let subscribing = broadcast.track("video").unwrap().subscribe(None);
5608		let mut producer = accept_track(&mut dynamic, "video").await;
5609		settle().await;
5610		let mut sub = subscribing.await.unwrap();
5611		producer.append_group().unwrap();
5612		assert_eq!(sub.assert_group().sequence, 0);
5613
5614		// Unrelated demand-edge wakes after the copy's last group: the reader
5615		// leaves and a new one arrives.
5616		drop(sub);
5617		settle().await;
5618		let resubscribing = broadcast.track("video").unwrap().subscribe(None);
5619		settle().await;
5620		let mut sub = resubscribing.await.unwrap();
5621		assert_eq!(sub.assert_group().sequence, 0, "cached group re-served");
5622
5623		// The copy then dies having delivered: failover, so the source is
5624		// re-asked and the subscription resumes.
5625		drop(producer);
5626		let mut producer = accept_track(&mut dynamic, "video").await;
5627		settle().await;
5628		producer.create_group(group::Info { sequence: 1 }).unwrap();
5629		assert_eq!(sub.assert_group().sequence, 1);
5630		sub.assert_not_closed();
5631	}
5632
5633	/// The front picks a source per track and re-picks on failover, so a route
5634	/// tainted for a peer is one the front may serve them from even when the
5635	/// active route is clean. Sharing the front therefore has to check the whole
5636	/// table, not just the active route: here the clean route is active but cannot
5637	/// carry the track, and the fallback is the peer's own route.
5638	#[tokio::test]
5639	async fn test_per_track_fallback_respects_exclusion() {
5640		tokio::time::pause();
5641
5642		let origin = Origin::random().produce();
5643		let consumer = origin.consume();
5644
5645		let publisher = Origin::new(1).unwrap();
5646		let peer = Origin::new(5).unwrap();
5647		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
5648		let local = OriginList::try_from(vec![publisher]).unwrap();
5649
5650		// The route through the peer has the track.
5651		let source_tainted = origin
5652			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
5653			.unwrap();
5654		let mut dynamic_tainted = source_tainted.dynamic();
5655		settle().await;
5656		settle().await;
5657
5658		// The clean route is cheaper, so it is active, but its publisher has not
5659		// created the track yet.
5660		let source_clean = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
5661		let mut dynamic_clean = source_clean.dynamic();
5662		settle().await;
5663
5664		let scoped = consumer.clone().excluding(peer);
5665		let broadcast = scoped.request_broadcast("test").await.unwrap();
5666		let _subscribing = broadcast.track("video").unwrap().subscribe(None);
5667		settle().await;
5668
5669		// The peer is pinned to the clean source, so the fallback the front would
5670		// take for everyone else is not reachable from their subscription: the
5671		// refusal aborts the track rather than asking the tainted route.
5672		let request = dynamic_clean.requested_track().await.unwrap();
5673		request.reject(Error::NotFound);
5674		settle().await;
5675		dynamic_tainted.assert_no_request();
5676	}
5677
5678	/// The same guarantee under failover: the peer resolves while a clean route is
5679	/// active, then that route dies. Its subscription must not migrate onto the
5680	/// route that flows back through it.
5681	#[tokio::test]
5682	async fn test_exclusion_survives_failover_onto_a_tainted_route() {
5683		tokio::time::pause();
5684
5685		let origin = Origin::random().produce();
5686		let consumer = origin.consume();
5687
5688		let publisher = Origin::new(1).unwrap();
5689		let peer = Origin::new(5).unwrap();
5690		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
5691		let local = OriginList::try_from(vec![publisher]).unwrap();
5692
5693		let source_tainted = origin
5694			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
5695			.unwrap();
5696		let mut dynamic_tainted = source_tainted.dynamic();
5697		settle().await;
5698		settle().await;
5699		let source_clean = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
5700		let mut dynamic_clean = source_clean.dynamic();
5701		settle().await;
5702
5703		let scoped = consumer.clone().excluding(peer);
5704		let broadcast = scoped.request_broadcast("test").await.unwrap();
5705		let _subscribing = broadcast.track("video").unwrap().subscribe(None);
5706		let _clean = accept_track(&mut dynamic_clean, "video").await;
5707		settle().await;
5708
5709		// The clean route dies, so the front's only remaining route is the peer's.
5710		source_clean.abort(Error::Dropped).unwrap();
5711		settle().await;
5712		settle().await;
5713		dynamic_tainted.assert_no_request();
5714
5715		// A fresh request now has nowhere clean to go, and says so rather than
5716		// silently serving the peer their own route.
5717		assert!(matches!(scoped.request_broadcast("test").await, Err(Error::Unroutable)));
5718	}
5719
5720	/// The resolve-time check only proves the table is clean for a peer at that
5721	/// instant. A route through them attaching *afterwards* must not be adopted
5722	/// underneath their live subscription: they hold the shared front, so the front
5723	/// stays off that route while a clean one remains.
5724	#[tokio::test]
5725	async fn test_exclusion_holds_when_a_tainted_route_attaches_later() {
5726		tokio::time::pause();
5727
5728		let origin = Origin::random().produce();
5729		let consumer = origin.consume();
5730
5731		let publisher = Origin::new(1).unwrap();
5732		let peer = Origin::new(5).unwrap();
5733		let local = OriginList::try_from(vec![publisher]).unwrap();
5734		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
5735
5736		// Only a clean route exists, so the peer legitimately gets the shared front.
5737		// Priced above the route that arrives later, so the front genuinely prefers
5738		// that one and staying put is the guard's doing, not the tie-break's.
5739		let source_clean = origin
5740			.create_broadcast("test", announce().with_hops(local).with_cost(5))
5741			.unwrap();
5742		let mut dynamic_clean = source_clean.dynamic();
5743		settle().await;
5744		settle().await;
5745		let scoped = consumer.clone().excluding(peer);
5746		let broadcast = scoped.request_broadcast("test").await.unwrap();
5747		let subscribing = broadcast.track("video").unwrap().subscribe(None);
5748		let mut producer_clean = accept_track(&mut dynamic_clean, "video").await;
5749		settle().await;
5750		let mut sub = subscribing.await.unwrap();
5751		producer_clean.append_group().unwrap();
5752		assert_eq!(sub.assert_group().sequence, 0);
5753
5754		// A cheaper route back through the peer attaches. Without the registration
5755		// the front would re-splice onto it and hand the peer its own bytes.
5756		// `advertised` non-zero says the announcing relay is not itself carrying, which
5757		// keeps the simultaneous-activation handover gate (whose key comparison is
5758		// hash-random) out of this test: the front takes the cheaper route outright
5759		// unless the exclusion stops it.
5760		let mut tainted = announce().with_hops(via_peer.clone()).with_cost(0);
5761		tainted.advertised = 1;
5762		let mut source_tainted = origin.create_broadcast("test", tainted).unwrap();
5763		let mut dynamic_tainted = source_tainted.dynamic();
5764		settle().await;
5765		settle().await;
5766		dynamic_tainted.assert_no_request();
5767		producer_clean.append_group().unwrap();
5768		assert_eq!(sub.assert_group().sequence, 1);
5769		sub.assert_not_closed();
5770
5771		// The registration ends with the last handle. The next table change is free
5772		// to take the cheaper route, which is what proves it was released.
5773		drop(sub);
5774		drop(broadcast);
5775		drop(scoped);
5776		settle().await;
5777		let mut bumped = announce().with_hops(via_peer).with_cost(1);
5778		bumped.advertised = 1;
5779		source_tainted.set_route(bumped).unwrap();
5780		settle().await;
5781		let plain = consumer.request_broadcast("test").await.unwrap();
5782		let _plain_track = plain.track("video").unwrap().subscribe(None);
5783		settle().await;
5784		settle().await;
5785		assert!(
5786			dynamic_tainted.requested_track().now_or_never().is_some(),
5787			"the front must be free to use the route again once the peer is gone"
5788		);
5789	}
5790
5791	/// An announced path every route of which loops through the requester is
5792	/// unroutable, not missing: the dynamic handler resolves paths with no route
5793	/// chain to check, so consulting it would route around the split horizon.
5794	#[tokio::test]
5795	async fn test_excluded_path_never_reaches_the_dynamic_handler() {
5796		tokio::time::pause();
5797
5798		let origin = Origin::random().produce();
5799		let consumer = origin.consume();
5800		let mut dynamic = origin.dynamic();
5801
5802		let peer = Origin::new(5).unwrap();
5803		let tainted = OriginList::try_from(vec![Origin::new(1).unwrap(), peer]).unwrap();
5804		let _source = origin.create_broadcast("test", announce().with_hops(tainted)).unwrap();
5805		settle().await;
5806		settle().await;
5807
5808		let scoped = consumer.clone().excluding(peer);
5809		assert!(matches!(scoped.request_broadcast("test").await, Err(Error::Unroutable)));
5810		assert!(
5811			dynamic.requested_broadcast().now_or_never().is_none(),
5812			"the dynamic handler was asked to route around the exclusion"
5813		);
5814
5815		// An unannounced path still reaches the handler, exclusion or not.
5816		let _pending = scoped.request_broadcast("other");
5817		settle().await;
5818		assert!(
5819			dynamic.requested_broadcast().now_or_never().is_some(),
5820			"a genuinely missing path must still fall back"
5821		);
5822	}
5823
5824	/// When every route flows through the requester, the path is unroutable for
5825	/// them: serving it would hand them their own bytes back.
5826	#[tokio::test]
5827	async fn test_dispatch_all_tainted_unroutable() {
5828		tokio::time::pause();
5829
5830		let origin = Origin::random().produce();
5831		let consumer = origin.consume();
5832
5833		let peer = Origin::new(5).unwrap();
5834		let tainted = OriginList::try_from(vec![Origin::new(1).unwrap(), peer]).unwrap();
5835		let _source = origin.create_broadcast("test", announce().with_hops(tainted)).unwrap();
5836		settle().await;
5837		settle().await;
5838
5839		let scoped = consumer.clone().excluding(peer);
5840		match scoped.request_broadcast("test").await {
5841			Err(Error::Unroutable) => {}
5842			Err(err) => panic!("expected Unroutable, got {err:?}"),
5843			Ok(_) => panic!("expected Unroutable, got a broadcast"),
5844		}
5845
5846		// Everyone else still resolves the shared front.
5847		consumer.request_broadcast("test").await.unwrap();
5848	}
5849
5850	#[tokio::test]
5851	async fn test_duplicate_reverse() {
5852		tokio::time::pause();
5853
5854		let origin = Origin::random().produce();
5855
5856		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
5857		let mut broadcast2 = origin.create_broadcast("test", announce()).unwrap();
5858		settle().await;
5859		assert!(origin.consume().get_broadcast("test").is_some());
5860
5861		// This is harder, finishing the newer source first.
5862		broadcast2.finish();
5863		settle().await;
5864		assert!(origin.consume().get_broadcast("test").is_some());
5865
5866		broadcast1.finish();
5867		settle().await;
5868		assert!(origin.consume().get_broadcast("test").is_none());
5869	}
5870
5871	#[tokio::test]
5872	async fn test_deterministic_tiebreak() {
5873		tokio::time::pause();
5874
5875		fn hops(ids: &[u64]) -> OriginList {
5876			OriginList::try_from(
5877				ids.iter()
5878					.copied()
5879					.map(|id| Origin::new(id).unwrap())
5880					.collect::<Vec<_>>(),
5881			)
5882			.unwrap()
5883		}
5884
5885		// Resolve the advertised route for "test" after creating both sources in
5886		// the given order.
5887		async fn winner(first: &[u64], second: &[u64]) -> OriginList {
5888			let origin = Origin::random().produce();
5889			let _a = origin
5890				.create_broadcast("test", announce().with_hops(hops(first)))
5891				.unwrap();
5892			let _b = origin
5893				.create_broadcast("test", announce().with_hops(hops(second)))
5894				.unwrap();
5895			settle().await;
5896			origin.consume().get_broadcast("test").unwrap().route().hops
5897		}
5898
5899		// Two routes with equal hop counts but distinct chains (sharing the first
5900		// hop, so both may attach). The winner is decided by the deterministic
5901		// key, not arrival order, so both publish orders converge.
5902		let forward = winner(&[5, 20], &[5, 40]).await;
5903		let reverse = winner(&[5, 40], &[5, 20]).await;
5904		assert_eq!(forward, reverse, "tie-break must not depend on publish order");
5905
5906		// A strictly shorter chain always wins regardless of the hash.
5907		assert_eq!(winner(&[5, 20], &[5]).await.len(), 1);
5908		assert_eq!(winner(&[5], &[5, 20]).await.len(), 1);
5909	}
5910
5911	// A previous mpsc-based implementation could only deliver the first 127 broadcasts
5912	// instantly via `assert_next` (which uses `now_or_never`). The kio-backed
5913	// implementation polls synchronously and can deliver all of them without yielding.
5914	// Names are zero-padded so lexicographic delivery order matches the loop index.
5915	#[tokio::test]
5916	async fn test_many_announces() {
5917		let origin = Origin::random().produce();
5918
5919		let mut consumer = origin.consume().announced();
5920		// Held for the duration: a dropped source unannounces immediately.
5921		let mut broadcasts = Vec::new();
5922		for i in 0..256 {
5923			broadcasts.push(origin.create_broadcast(format!("test{i:03}"), announce()).unwrap());
5924			settle().await;
5925		}
5926
5927		for i in 0..256 {
5928			consumer.assert_next_some(format!("test{i:03}"));
5929		}
5930		consumer.assert_next_wait();
5931	}
5932
5933	#[tokio::test]
5934	async fn test_many_announces_try() {
5935		let origin = Origin::random().produce();
5936
5937		let mut consumer = origin.consume().announced();
5938		// Held for the duration: a dropped source unannounces immediately.
5939		let mut broadcasts = Vec::new();
5940		for i in 0..256 {
5941			broadcasts.push(origin.create_broadcast(format!("test{i:03}"), announce()).unwrap());
5942			settle().await;
5943		}
5944
5945		for i in 0..256 {
5946			consumer.assert_try_next_some(format!("test{i:03}"));
5947		}
5948	}
5949
5950	#[tokio::test]
5951	async fn test_with_root_basic() {
5952		let origin = Origin::random().produce();
5953
5954		// Create a producer with root "/foo"
5955		let foo_producer = origin.with_root("foo").expect("should create root");
5956		assert_eq!(foo_producer.root().as_str(), "foo");
5957
5958		let mut consumer = origin.consume().announced();
5959
5960		// When publishing to "bar/baz", it should actually publish to "foo/bar/baz"
5961		let _broadcast = foo_producer
5962			.create_broadcast("bar/baz", announce())
5963			.expect("publish allowed");
5964		settle().await;
5965		// The original consumer should see the full path
5966		consumer.assert_next_some("foo/bar/baz");
5967
5968		// A consumer created from the rooted producer should see the stripped path
5969		let mut foo_consumer = foo_producer.consume().announced();
5970		foo_consumer.assert_next_some("bar/baz");
5971	}
5972
5973	#[tokio::test]
5974	async fn test_with_root_nested() {
5975		let origin = Origin::random().produce();
5976
5977		// Create nested roots
5978		let foo_producer = origin.with_root("foo").expect("should create foo root");
5979		let foo_bar_producer = foo_producer.with_root("bar").expect("should create bar root");
5980		assert_eq!(foo_bar_producer.root().as_str(), "foo/bar");
5981
5982		let mut consumer = origin.consume().announced();
5983
5984		// Publishing to "baz" should actually publish to "foo/bar/baz"
5985		let _broadcast = foo_bar_producer
5986			.create_broadcast("baz", announce())
5987			.expect("publish allowed");
5988		settle().await;
5989		// The original consumer sees the full path
5990		consumer.assert_next_some("foo/bar/baz");
5991
5992		// Consumer from foo_bar_producer sees just "baz"
5993		let mut foo_bar_consumer = foo_bar_producer.consume().announced();
5994		foo_bar_consumer.assert_next_some("baz");
5995	}
5996
5997	#[tokio::test]
5998	async fn test_publish_scope_allows() {
5999		let origin = Origin::random().produce();
6000
6001		// Create a producer that can only publish to "allowed" paths
6002		let limited_producer = origin
6003			.scope(&["allowed/path1".into(), "allowed/path2".into()])
6004			.expect("should create limited producer");
6005
6006		// Should be able to publish to allowed paths
6007		let _broadcast = limited_producer
6008			.create_broadcast("allowed/path1", announce())
6009			.expect("publish allowed");
6010		let _keep2 = limited_producer
6011			.create_broadcast("allowed/path1/nested", announce())
6012			.expect("publish allowed");
6013		let _keep3 = limited_producer
6014			.create_broadcast("allowed/path2", announce())
6015			.expect("publish allowed");
6016		settle().await;
6017
6018		// Should not be able to publish to disallowed paths
6019		assert!(limited_producer.create_broadcast("notallowed", announce()).is_err());
6020		assert!(limited_producer.create_broadcast("allowed", announce()).is_err()); // Parent of allowed path
6021		assert!(limited_producer.create_broadcast("other/path", announce()).is_err());
6022	}
6023
6024	#[tokio::test]
6025	async fn test_publish_max_parts() {
6026		let origin = Origin::random().produce();
6027
6028		let at_limit = (0..Path::MAX_PARTS)
6029			.map(|i| i.to_string())
6030			.collect::<Vec<_>>()
6031			.join("/");
6032		let _broadcast = origin
6033			.create_broadcast(at_limit.as_str(), announce())
6034			.expect("publish allowed");
6035		settle().await;
6036
6037		let too_deep = format!("{at_limit}/extra");
6038		assert!(origin.create_broadcast(too_deep.as_str(), announce()).is_err());
6039
6040		// The root counts toward the limit; a joined path past 32 parts is rejected.
6041		let rooted = origin.with_root("root").expect("wildcard allows any root");
6042		assert!(rooted.create_broadcast(at_limit.as_str(), announce()).is_err());
6043	}
6044
6045	#[tokio::test]
6046	async fn test_publish_scope_empty() {
6047		let origin = Origin::random().produce();
6048
6049		// Creating a producer with no allowed paths should return None
6050		assert!(origin.scope(&[]).is_none());
6051	}
6052
6053	#[tokio::test]
6054	async fn test_consume_scope_filters() {
6055		let origin = Origin::random().produce();
6056
6057		let mut consumer = origin.consume().announced();
6058
6059		// Publish to different paths
6060		let _broadcast1 = origin.create_broadcast("allowed", announce()).unwrap();
6061		let _broadcast2 = origin.create_broadcast("allowed/nested", announce()).unwrap();
6062		let _broadcast3 = origin.create_broadcast("notallowed", announce()).unwrap();
6063		settle().await;
6064
6065		// Create a consumer that only sees "allowed" paths
6066		let mut limited_consumer = origin
6067			.consume()
6068			.scope(&["allowed".into()])
6069			.expect("should create limited consumer")
6070			.announced();
6071
6072		// Should only receive broadcasts under "allowed"
6073		limited_consumer.assert_next_some("allowed");
6074		limited_consumer.assert_next_some("allowed/nested");
6075		limited_consumer.assert_next_wait(); // Should not see "notallowed"
6076
6077		// Unscoped consumer should see all
6078		consumer.assert_next_some("allowed");
6079		consumer.assert_next_some("allowed/nested");
6080		consumer.assert_next_some("notallowed");
6081	}
6082
6083	#[tokio::test]
6084	async fn test_consume_scope_multiple_prefixes() {
6085		let origin = Origin::random().produce();
6086
6087		let _broadcast1 = origin.create_broadcast("foo/test", announce()).unwrap();
6088		let _broadcast2 = origin.create_broadcast("bar/test", announce()).unwrap();
6089		let _broadcast3 = origin.create_broadcast("baz/test", announce()).unwrap();
6090		settle().await;
6091
6092		// Consumer that only sees "foo" and "bar" paths
6093		let mut limited_consumer = origin
6094			.consume()
6095			.scope(&["foo".into(), "bar".into()])
6096			.expect("should create limited consumer")
6097			.announced();
6098
6099		// Order depends on PathPrefixes canonical sort (lexicographic for same length)
6100		limited_consumer.assert_next_some("bar/test");
6101		limited_consumer.assert_next_some("foo/test");
6102		limited_consumer.assert_next_wait(); // Should not see "baz/test"
6103	}
6104
6105	#[tokio::test]
6106	async fn test_with_root_and_publish_scope() {
6107		let origin = Origin::random().produce();
6108
6109		// User connects to /foo root
6110		let foo_producer = origin.with_root("foo").expect("should create foo root");
6111
6112		// Limit them to publish only to "bar" and "goop/pee" within /foo
6113		let limited_producer = foo_producer
6114			.scope(&["bar".into(), "goop/pee".into()])
6115			.expect("should create limited producer");
6116
6117		let mut consumer = origin.consume().announced();
6118
6119		// Should be able to publish to foo/bar and foo/goop/pee (but user sees as bar and goop/pee)
6120		let _broadcast = limited_producer
6121			.create_broadcast("bar", announce())
6122			.expect("publish allowed");
6123		let _keep2 = limited_producer
6124			.create_broadcast("bar/nested", announce())
6125			.expect("publish allowed");
6126		let _keep3 = limited_producer
6127			.create_broadcast("goop/pee", announce())
6128			.expect("publish allowed");
6129		let _keep4 = limited_producer
6130			.create_broadcast("goop/pee/nested", announce())
6131			.expect("publish allowed");
6132		settle().await;
6133
6134		// Should not be able to publish outside allowed paths
6135		assert!(limited_producer.create_broadcast("baz", announce()).is_err());
6136		assert!(limited_producer.create_broadcast("goop", announce()).is_err()); // Parent of allowed
6137		assert!(limited_producer.create_broadcast("goop/other", announce()).is_err());
6138
6139		// Original consumer sees full paths
6140		consumer.assert_next_some("foo/bar");
6141		consumer.assert_next_some("foo/bar/nested");
6142		consumer.assert_next_some("foo/goop/pee");
6143		consumer.assert_next_some("foo/goop/pee/nested");
6144	}
6145
6146	#[tokio::test]
6147	async fn test_with_root_and_consume_scope() {
6148		let origin = Origin::random().produce();
6149
6150		// Publish broadcasts
6151		let _broadcast1 = origin.create_broadcast("foo/bar/test", announce()).unwrap();
6152		let _broadcast2 = origin.create_broadcast("foo/goop/pee/test", announce()).unwrap();
6153		let _broadcast3 = origin.create_broadcast("foo/other/test", announce()).unwrap();
6154		settle().await;
6155
6156		// User connects to /foo root
6157		let foo_producer = origin.with_root("foo").expect("should create foo root");
6158
6159		// Create consumer limited to "bar" and "goop/pee" within /foo
6160		let mut limited_consumer = foo_producer
6161			.consume()
6162			.scope(&["bar".into(), "goop/pee".into()])
6163			.expect("should create limited consumer")
6164			.announced();
6165
6166		// Should only see allowed paths (without foo prefix)
6167		limited_consumer.assert_next_some("bar/test");
6168		limited_consumer.assert_next_some("goop/pee/test");
6169		limited_consumer.assert_next_wait(); // Should not see "other/test"
6170	}
6171
6172	#[tokio::test]
6173	async fn test_with_root_unauthorized() {
6174		let origin = Origin::random().produce();
6175
6176		// First limit the producer to specific paths
6177		let limited_producer = origin
6178			.scope(&["allowed".into()])
6179			.expect("should create limited producer");
6180
6181		// Trying to create a root outside allowed paths should fail
6182		assert!(limited_producer.with_root("notallowed").is_none());
6183
6184		// But creating a root within allowed paths should work
6185		let allowed_root = limited_producer
6186			.with_root("allowed")
6187			.expect("should create allowed root");
6188		assert_eq!(allowed_root.root().as_str(), "allowed");
6189	}
6190
6191	#[tokio::test]
6192	async fn test_wildcard_permission() {
6193		let origin = Origin::random().produce();
6194
6195		// Producer with root access (empty string means wildcard)
6196		let root_producer = origin.clone();
6197
6198		// Should be able to publish anywhere
6199		let _broadcast = root_producer
6200			.create_broadcast("any/path", announce())
6201			.expect("publish allowed");
6202		let _keep2 = root_producer
6203			.create_broadcast("other/path", announce())
6204			.expect("publish allowed");
6205		settle().await;
6206
6207		// Can create any root
6208		let foo_producer = root_producer.with_root("foo").expect("should create any root");
6209		assert_eq!(foo_producer.root().as_str(), "foo");
6210	}
6211
6212	#[tokio::test]
6213	async fn test_consume_broadcast_with_permissions() {
6214		let origin = Origin::random().produce();
6215
6216		let _broadcast1 = origin.create_broadcast("allowed/test", announce()).unwrap();
6217		let _broadcast2 = origin.create_broadcast("notallowed/test", announce()).unwrap();
6218		settle().await;
6219
6220		// Create limited consumer
6221		let limited_consumer = origin
6222			.consume()
6223			.scope(&["allowed".into()])
6224			.expect("should create limited consumer");
6225
6226		// Should be able to get allowed broadcast
6227		let result = limited_consumer.get_broadcast("allowed/test");
6228		assert!(result.is_some());
6229		assert!(
6230			result
6231				.unwrap()
6232				.is_clone(&origin.consume().get_broadcast("allowed/test").unwrap())
6233		);
6234
6235		// Should not be able to get disallowed broadcast
6236		assert!(limited_consumer.get_broadcast("notallowed/test").is_none());
6237
6238		// Original consumer can get both
6239		let consumer = origin.consume();
6240		assert!(consumer.get_broadcast("allowed/test").is_some());
6241		assert!(consumer.get_broadcast("notallowed/test").is_some());
6242	}
6243
6244	#[tokio::test]
6245	async fn test_nested_paths_with_permissions() {
6246		let origin = Origin::random().produce();
6247
6248		// Create producer limited to "a/b/c"
6249		let limited_producer = origin.scope(&["a/b/c".into()]).expect("should create limited producer");
6250
6251		// Should be able to publish to exact path and nested paths
6252		let _broadcast = limited_producer
6253			.create_broadcast("a/b/c", announce())
6254			.expect("publish allowed");
6255		let _keep2 = limited_producer
6256			.create_broadcast("a/b/c/d", announce())
6257			.expect("publish allowed");
6258		let _keep3 = limited_producer
6259			.create_broadcast("a/b/c/d/e", announce())
6260			.expect("publish allowed");
6261		settle().await;
6262
6263		// Should not be able to publish to parent or sibling paths
6264		assert!(limited_producer.create_broadcast("a", announce()).is_err());
6265		assert!(limited_producer.create_broadcast("a/b", announce()).is_err());
6266		assert!(limited_producer.create_broadcast("a/b/other", announce()).is_err());
6267	}
6268
6269	#[tokio::test]
6270	async fn test_multiple_consumers_with_different_permissions() {
6271		let origin = Origin::random().produce();
6272
6273		// Publish to different paths
6274		let _broadcast1 = origin.create_broadcast("foo/test", announce()).unwrap();
6275		let _broadcast2 = origin.create_broadcast("bar/test", announce()).unwrap();
6276		let _broadcast3 = origin.create_broadcast("baz/test", announce()).unwrap();
6277		settle().await;
6278
6279		// Create consumers with different permissions
6280		let mut foo_consumer = origin
6281			.consume()
6282			.scope(&["foo".into()])
6283			.expect("should create foo consumer")
6284			.announced();
6285
6286		let mut bar_consumer = origin
6287			.consume()
6288			.scope(&["bar".into()])
6289			.expect("should create bar consumer")
6290			.announced();
6291
6292		let mut foobar_consumer = origin
6293			.consume()
6294			.scope(&["foo".into(), "bar".into()])
6295			.expect("should create foobar consumer")
6296			.announced();
6297
6298		// Each consumer should only see their allowed paths
6299		foo_consumer.assert_next_some("foo/test");
6300		foo_consumer.assert_next_wait();
6301
6302		bar_consumer.assert_next_some("bar/test");
6303		bar_consumer.assert_next_wait();
6304
6305		foobar_consumer.assert_next_some("bar/test");
6306		foobar_consumer.assert_next_some("foo/test");
6307		foobar_consumer.assert_next_wait();
6308	}
6309
6310	#[tokio::test]
6311	async fn test_select_with_empty_prefix() {
6312		let origin = Origin::random().produce();
6313
6314		// User with root "demo" allowed to subscribe to "worm-node" and "foobar"
6315		let demo_producer = origin.with_root("demo").expect("should create demo root");
6316		let limited_producer = demo_producer
6317			.scope(&["worm-node".into(), "foobar".into()])
6318			.expect("should create limited producer");
6319
6320		// Publish some broadcasts
6321		let _broadcast1 = limited_producer
6322			.create_broadcast("worm-node/test", announce())
6323			.expect("publish allowed");
6324		let _broadcast2 = limited_producer
6325			.create_broadcast("foobar/test", announce())
6326			.expect("publish allowed");
6327		settle().await;
6328
6329		// scope with empty prefix should keep the exact same "worm-node" and "foobar" nodes
6330		let mut consumer = limited_producer
6331			.consume()
6332			.scope(&["".into()])
6333			.expect("should create consumer with empty prefix")
6334			.announced();
6335
6336		// Should see both broadcasts (order depends on PathPrefixes sort)
6337		let a1 = consumer.try_next().expect("expected first announcement");
6338		let a2 = consumer.try_next().expect("expected second announcement");
6339		consumer.assert_next_wait();
6340
6341		let mut paths: Vec<_> = [&a1, &a2].iter().map(|a| a.path.to_string()).collect();
6342		paths.sort();
6343		assert_eq!(paths, ["foobar/test", "worm-node/test"]);
6344	}
6345
6346	#[tokio::test]
6347	async fn test_select_narrowing_scope() {
6348		let origin = Origin::random().produce();
6349
6350		// User with root "demo" allowed to subscribe to "worm-node" and "foobar"
6351		let demo_producer = origin.with_root("demo").expect("should create demo root");
6352		let limited_producer = demo_producer
6353			.scope(&["worm-node".into(), "foobar".into()])
6354			.expect("should create limited producer");
6355
6356		// Publish broadcasts at different levels
6357		let _broadcast1 = limited_producer
6358			.create_broadcast("worm-node", announce())
6359			.expect("publish allowed");
6360		let _broadcast2 = limited_producer
6361			.create_broadcast("worm-node/foo", announce())
6362			.expect("publish allowed");
6363		let _broadcast3 = limited_producer
6364			.create_broadcast("foobar/bar", announce())
6365			.expect("publish allowed");
6366		settle().await;
6367
6368		// Test 1: scope("worm-node") should result in a single "" node with contents of "worm-node" ONLY
6369		let mut worm_consumer = limited_producer
6370			.consume()
6371			.scope(&["worm-node".into()])
6372			.expect("should create worm-node consumer")
6373			.announced();
6374
6375		// Should see worm-node content with paths stripped to ""
6376		worm_consumer.assert_next_some("worm-node");
6377		worm_consumer.assert_next_some("worm-node/foo");
6378		worm_consumer.assert_next_wait(); // Should NOT see foobar content
6379
6380		// Test 2: scope("worm-node/foo") should result in a "" node with contents of "worm-node/foo"
6381		let mut foo_consumer = limited_producer
6382			.consume()
6383			.scope(&["worm-node/foo".into()])
6384			.expect("should create worm-node/foo consumer")
6385			.announced();
6386
6387		foo_consumer.assert_next_some("worm-node/foo");
6388		foo_consumer.assert_next_wait(); // Should NOT see other content
6389	}
6390
6391	#[tokio::test]
6392	async fn test_select_multiple_roots_with_empty_prefix() {
6393		let origin = Origin::random().produce();
6394
6395		// Producer with multiple allowed roots
6396		let limited_producer = origin
6397			.scope(&["app1".into(), "app2".into(), "shared".into()])
6398			.expect("should create limited producer");
6399
6400		// Publish to each root
6401		let _broadcast1 = limited_producer
6402			.create_broadcast("app1/data", announce())
6403			.expect("publish allowed");
6404		let _broadcast2 = limited_producer
6405			.create_broadcast("app2/config", announce())
6406			.expect("publish allowed");
6407		let _broadcast3 = limited_producer
6408			.create_broadcast("shared/resource", announce())
6409			.expect("publish allowed");
6410		settle().await;
6411
6412		// scope with empty prefix should maintain all roots
6413		let mut consumer = limited_producer
6414			.consume()
6415			.scope(&["".into()])
6416			.expect("should create consumer with empty prefix")
6417			.announced();
6418
6419		// Should see all broadcasts from all roots
6420		consumer.assert_next_some("app1/data");
6421		consumer.assert_next_some("app2/config");
6422		consumer.assert_next_some("shared/resource");
6423		consumer.assert_next_wait();
6424	}
6425
6426	#[tokio::test]
6427	async fn test_publish_scope_with_empty_prefix() {
6428		let origin = Origin::random().produce();
6429
6430		// Producer with specific allowed paths
6431		let limited_producer = origin
6432			.scope(&["services/api".into(), "services/web".into()])
6433			.expect("should create limited producer");
6434
6435		// scope with empty prefix should keep the same restrictions
6436		let same_producer = limited_producer
6437			.scope(&["".into()])
6438			.expect("should create producer with empty prefix");
6439
6440		// Should still have the same publishing restrictions
6441		let _broadcast = same_producer
6442			.create_broadcast("services/api", announce())
6443			.expect("publish allowed");
6444		let _keep2 = same_producer
6445			.create_broadcast("services/web", announce())
6446			.expect("publish allowed");
6447		assert!(same_producer.create_broadcast("services/db", announce()).is_err());
6448		assert!(same_producer.create_broadcast("other", announce()).is_err());
6449	}
6450
6451	#[tokio::test]
6452	async fn test_select_narrowing_to_deeper_path() {
6453		let origin = Origin::random().produce();
6454
6455		// Producer with broad permission
6456		let limited_producer = origin.scope(&["org".into()]).expect("should create limited producer");
6457
6458		// Publish at various depths
6459		let _broadcast1 = limited_producer
6460			.create_broadcast("org/team1/project1", announce())
6461			.expect("publish allowed");
6462		let _broadcast2 = limited_producer
6463			.create_broadcast("org/team1/project2", announce())
6464			.expect("publish allowed");
6465		let _broadcast3 = limited_producer
6466			.create_broadcast("org/team2/project1", announce())
6467			.expect("publish allowed");
6468		settle().await;
6469
6470		// Narrow down to team2 only
6471		let mut team2_consumer = limited_producer
6472			.consume()
6473			.scope(&["org/team2".into()])
6474			.expect("should create team2 consumer")
6475			.announced();
6476
6477		team2_consumer.assert_next_some("org/team2/project1");
6478		team2_consumer.assert_next_wait(); // Should NOT see team1 content
6479
6480		// Further narrow down to team1/project1
6481		let mut project1_consumer = limited_producer
6482			.consume()
6483			.scope(&["org/team1/project1".into()])
6484			.expect("should create project1 consumer")
6485			.announced();
6486
6487		// Should only see project1 content at root
6488		project1_consumer.assert_next_some("org/team1/project1");
6489		project1_consumer.assert_next_wait();
6490	}
6491
6492	#[tokio::test]
6493	async fn test_select_with_non_matching_prefix() {
6494		let origin = Origin::random().produce();
6495
6496		// Producer with specific allowed paths
6497		let limited_producer = origin
6498			.scope(&["allowed/path".into()])
6499			.expect("should create limited producer");
6500
6501		// Trying to scope with a completely different prefix should return None
6502		assert!(limited_producer.consume().scope(&["different/path".into()]).is_none());
6503
6504		// Similarly for scope
6505		assert!(limited_producer.scope(&["other/path".into()]).is_none());
6506	}
6507
6508	// Regression test for https://github.com/moq-dev/moq/issues/910
6509	// with_root panics when String has trailing slash (AsPath for String skips normalization)
6510	#[tokio::test]
6511	async fn test_with_root_trailing_slash_consumer() {
6512		let origin = Origin::random().produce();
6513
6514		// Use an owned String so the trailing slash is NOT normalized away.
6515		let prefix = "some_prefix/".to_string();
6516		let mut consumer = origin.consume().with_root(prefix).unwrap().announced();
6517
6518		let _b = origin.create_broadcast("some_prefix/test", announce()).unwrap();
6519		settle().await;
6520		consumer.assert_next_some("test");
6521	}
6522
6523	// Same issue but for the producer side of with_root
6524	#[tokio::test]
6525	async fn test_with_root_trailing_slash_producer() {
6526		let origin = Origin::random().produce();
6527
6528		// Use an owned String so the trailing slash is NOT normalized away.
6529		let prefix = "some_prefix/".to_string();
6530		let rooted = origin.with_root(prefix).unwrap();
6531
6532		let _b = rooted.create_broadcast("test", announce()).unwrap();
6533		settle().await;
6534
6535		let mut consumer = rooted.consume().announced();
6536		consumer.assert_next_some("test");
6537	}
6538
6539	// Verify unannounce also doesn't panic with trailing slash
6540	#[tokio::test]
6541	async fn test_with_root_trailing_slash_unannounce() {
6542		tokio::time::pause();
6543
6544		let origin = Origin::random().produce();
6545
6546		let prefix = "some_prefix/".to_string();
6547		let mut consumer = origin.consume().with_root(prefix).unwrap().announced();
6548
6549		let mut b = origin.create_broadcast("some_prefix/test", announce()).unwrap();
6550		settle().await;
6551		consumer.assert_next_some("test");
6552
6553		// Finish the broadcast to trigger an immediate unannounce.
6554		b.finish();
6555		settle().await;
6556
6557		// unannounce also calls strip_prefix(&self.root).unwrap()
6558		consumer.assert_next_none("test");
6559	}
6560
6561	#[tokio::test]
6562	async fn test_select_maintains_access_with_wider_prefix() {
6563		let origin = Origin::random().produce();
6564
6565		// Setup: user with root "demo" allowed to subscribe to specific paths
6566		let demo_producer = origin.with_root("demo").expect("should create demo root");
6567		let user_producer = demo_producer
6568			.scope(&["worm-node".into(), "foobar".into()])
6569			.expect("should create user producer");
6570
6571		// Publish some data
6572		let _broadcast1 = user_producer
6573			.create_broadcast("worm-node/data", announce())
6574			.expect("publish allowed");
6575		let _broadcast2 = user_producer
6576			.create_broadcast("foobar", announce())
6577			.expect("publish allowed");
6578		settle().await;
6579
6580		// Key test: scope with "" should maintain access to allowed roots
6581		let mut consumer = user_producer
6582			.consume()
6583			.scope(&["".into()])
6584			.expect("scope with empty prefix should not fail when user has specific permissions")
6585			.announced();
6586
6587		// Should still receive broadcasts from allowed paths (order not guaranteed)
6588		let a1 = consumer.try_next().expect("expected first announcement");
6589		let a2 = consumer.try_next().expect("expected second announcement");
6590		consumer.assert_next_wait();
6591
6592		let mut paths: Vec<_> = [&a1, &a2].iter().map(|a| a.path.to_string()).collect();
6593		paths.sort();
6594		assert_eq!(paths, ["foobar", "worm-node/data"]);
6595
6596		// Also test that we can still narrow the scope
6597		let mut narrow_consumer = user_producer
6598			.consume()
6599			.scope(&["worm-node".into()])
6600			.expect("should be able to narrow scope to worm-node")
6601			.announced();
6602
6603		narrow_consumer.assert_next_some("worm-node/data");
6604		narrow_consumer.assert_next_wait(); // Should not see foobar
6605	}
6606
6607	#[tokio::test]
6608	async fn test_duplicate_prefixes_deduped() {
6609		let origin = Origin::random().produce();
6610
6611		// scope with duplicate prefixes should work (deduped internally)
6612		let producer = origin
6613			.scope(&["demo".into(), "demo".into()])
6614			.expect("should create producer");
6615
6616		let _broadcast = producer
6617			.create_broadcast("demo/stream", announce())
6618			.expect("publish allowed");
6619		settle().await;
6620
6621		let mut consumer = producer.consume().announced();
6622		consumer.assert_next_some("demo/stream");
6623		consumer.assert_next_wait();
6624	}
6625
6626	#[tokio::test]
6627	async fn test_overlapping_prefixes_deduped() {
6628		let origin = Origin::random().produce();
6629
6630		// "demo" and "demo/foo". "demo/foo" is redundant, only "demo" should remain
6631		let producer = origin
6632			.scope(&["demo".into(), "demo/foo".into()])
6633			.expect("should create producer");
6634
6635		// Can still publish under "demo/bar" since "demo" covers everything
6636		let _broadcast = producer
6637			.create_broadcast("demo/bar/stream", announce())
6638			.expect("publish allowed");
6639		settle().await;
6640
6641		let mut consumer = producer.consume().announced();
6642		consumer.assert_next_some("demo/bar/stream");
6643		consumer.assert_next_wait();
6644	}
6645
6646	#[tokio::test]
6647	async fn test_overlapping_prefixes_no_duplicate_announcements() {
6648		let origin = Origin::random().produce();
6649
6650		// Both "demo" and "demo/foo" are requested. Should only have one node
6651		let producer = origin
6652			.scope(&["demo".into(), "demo/foo".into()])
6653			.expect("should create producer");
6654
6655		let _broadcast = producer
6656			.create_broadcast("demo/foo/stream", announce())
6657			.expect("publish allowed");
6658		settle().await;
6659
6660		let mut consumer = producer.consume().announced();
6661		// Should only get ONE announcement (not two from overlapping nodes)
6662		consumer.assert_next_some("demo/foo/stream");
6663		consumer.assert_next_wait();
6664	}
6665
6666	#[tokio::test]
6667	async fn test_allowed_returns_deduped_prefixes() {
6668		let origin = Origin::random().produce();
6669
6670		let producer = origin
6671			.scope(&["demo".into(), "demo/foo".into(), "anon".into()])
6672			.expect("should create producer");
6673
6674		let allowed: Vec<_> = producer.allowed().collect();
6675		assert_eq!(allowed.len(), 2, "demo/foo should be subsumed by demo");
6676	}
6677
6678	#[tokio::test]
6679	async fn test_announced_broadcast_already_announced() {
6680		let origin = Origin::random().produce();
6681
6682		let _broadcast = origin.create_broadcast("test", announce()).unwrap();
6683		settle().await;
6684
6685		let consumer = origin.consume();
6686		let result = consumer.announced_broadcast("test").await.expect("should find it");
6687		assert!(result.is_clone(&consumer.get_broadcast("test").unwrap()));
6688	}
6689
6690	#[tokio::test]
6691	async fn test_announced_broadcast_delayed() {
6692		tokio::time::pause();
6693
6694		let origin = Origin::random().produce();
6695
6696		let consumer = origin.consume();
6697
6698		// Start waiting before it's announced.
6699		let wait = tokio::spawn({
6700			let consumer = consumer.clone();
6701			async move { consumer.announced_broadcast("test").await }
6702		});
6703
6704		// Give the spawned task a chance to subscribe.
6705		tokio::task::yield_now().await;
6706
6707		let _broadcast = origin.create_broadcast("test", announce()).unwrap();
6708		settle().await;
6709
6710		let result = wait.await.unwrap().expect("should find it");
6711		assert!(result.is_clone(&consumer.get_broadcast("test").unwrap()));
6712	}
6713
6714	#[tokio::test]
6715	async fn test_announced_broadcast_ignores_unrelated_paths() {
6716		tokio::time::pause();
6717
6718		let origin = Origin::random().produce();
6719
6720		let consumer = origin.consume();
6721
6722		let wait = tokio::spawn({
6723			let consumer = consumer.clone();
6724			async move { consumer.announced_broadcast("target").await }
6725		});
6726
6727		tokio::task::yield_now().await;
6728
6729		// Publish an unrelated broadcast first. announced_broadcast should skip it.
6730		let _other = origin.create_broadcast("other", announce()).unwrap();
6731		settle().await;
6732		tokio::task::yield_now().await;
6733		assert!(!wait.is_finished(), "must not resolve on unrelated path");
6734
6735		let _target = origin.create_broadcast("target", announce()).unwrap();
6736		settle().await;
6737		let result = wait.await.unwrap().expect("should find target");
6738		assert!(result.is_clone(&consumer.get_broadcast("target").unwrap()));
6739	}
6740
6741	#[tokio::test]
6742	async fn test_announced_broadcast_skips_nested_paths() {
6743		tokio::time::pause();
6744
6745		let origin = Origin::random().produce();
6746
6747		let consumer = origin.consume();
6748
6749		let wait = tokio::spawn({
6750			let consumer = consumer.clone();
6751			async move { consumer.announced_broadcast("foo").await }
6752		});
6753
6754		tokio::task::yield_now().await;
6755
6756		// "foo/bar" is under the prefix scope, but it's not the exact path. Skip it.
6757		let _nested = origin.create_broadcast("foo/bar", announce()).unwrap();
6758		settle().await;
6759		tokio::task::yield_now().await;
6760		assert!(!wait.is_finished(), "must not resolve on a nested path");
6761
6762		let _exact = origin.create_broadcast("foo", announce()).unwrap();
6763		settle().await;
6764		let result = wait.await.unwrap().expect("should find foo exactly");
6765		assert!(result.is_clone(&consumer.get_broadcast("foo").unwrap()));
6766	}
6767
6768	#[tokio::test]
6769	async fn test_announced_broadcast_disallowed() {
6770		let origin = Origin::random().produce();
6771		let limited = origin
6772			.consume()
6773			.scope(&["allowed".into()])
6774			.expect("should create limited");
6775
6776		// Path is outside allowed prefixes. Should return None immediately.
6777		assert!(limited.announced_broadcast("notallowed").await.is_none());
6778	}
6779
6780	#[tokio::test]
6781	async fn test_announced_broadcast_scope_too_narrow() {
6782		// Consumer's scope is narrower than the requested path: asking for `foo` on a consumer
6783		// limited to `foo/specific` can never resolve. Must return None, not loop forever.
6784		let origin = Origin::random().produce();
6785		let limited = origin
6786			.consume()
6787			.scope(&["foo/specific".into()])
6788			.expect("should create limited");
6789
6790		// now_or_never so we fail fast instead of hanging if the guard regresses.
6791		let result = limited
6792			.announced_broadcast("foo")
6793			.now_or_never()
6794			.expect("must not block");
6795		assert!(result.is_none());
6796	}
6797
6798	// Coalescing tests: a slow cursor that doesn't drain between updates
6799	// should observe a bounded number of deliveries.
6800
6801	#[tokio::test]
6802	async fn test_coalesce_announce_then_unannounce() {
6803		// announce + unannounce that the cursor hasn't observed yet collapses to nothing.
6804		tokio::time::pause();
6805
6806		let origin = Origin::random().produce();
6807		let mut announced = origin.consume().announced();
6808
6809		let mut broadcast = origin.create_broadcast("test", announce()).unwrap();
6810		settle().await;
6811		broadcast.finish();
6812
6813		settle().await;
6814
6815		announced.assert_next_wait();
6816	}
6817
6818	#[tokio::test]
6819	async fn test_coalesce_announce_unannounce_announce() {
6820		// announce, unannounce, announce that the cursor hasn't drained collapses
6821		// to a single Announce of the latest broadcast.
6822		tokio::time::pause();
6823
6824		let origin = Origin::random().produce();
6825		let mut announced = origin.consume().announced();
6826
6827		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
6828		settle().await;
6829		broadcast1.finish();
6830		settle().await;
6831		let _broadcast2 = origin.create_broadcast("test", announce()).unwrap();
6832		settle().await;
6833
6834		announced.assert_next_some("test");
6835		announced.assert_next_wait();
6836	}
6837
6838	#[tokio::test]
6839	async fn test_coalesce_unannounce_announce_preserved() {
6840		// unannounce followed by announce of a different broadcast must be preserved
6841		// as two deliveries so the cursor learns the origin changed.
6842		tokio::time::pause();
6843
6844		let origin = Origin::random().produce();
6845		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
6846		settle().await;
6847
6848		let mut announced = origin.consume().announced();
6849		announced.assert_next_some("test");
6850
6851		// Finish, then publish a fresh broadcast at the same path.
6852		broadcast1.finish();
6853		settle().await;
6854
6855		let _broadcast2 = origin.create_broadcast("test", announce()).unwrap();
6856		settle().await;
6857
6858		// The cursor must see the unannounce before the new announce.
6859		announced.assert_next_none("test");
6860		announced.assert_next_some("test");
6861		announced.assert_next_wait();
6862	}
6863
6864	#[tokio::test]
6865	async fn test_coalesce_unannounce_announce_unannounce() {
6866		// unannounce + announce + unannounce collapses to a single unannounce: the
6867		// embedded announce was never observed.
6868		tokio::time::pause();
6869
6870		let origin = Origin::random().produce();
6871		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
6872		settle().await;
6873
6874		let mut announced = origin.consume().announced();
6875		announced.assert_next_some("test");
6876
6877		broadcast1.finish();
6878		settle().await;
6879
6880		let mut broadcast2 = origin.create_broadcast("test", announce()).unwrap();
6881		settle().await;
6882		broadcast2.finish();
6883		settle().await;
6884
6885		announced.assert_next_none("test");
6886		announced.assert_next_wait();
6887	}
6888
6889	#[tokio::test]
6890	async fn test_coalesce_churn_bounded() {
6891		// A churn loop on a single path should keep the pending set bounded.
6892		// Backup promotion during cleanup can leave the cursor with zero or one
6893		// pending update for "test" depending on the order tasks run; we only
6894		// require that churn doesn't accumulate across iterations.
6895		tokio::time::pause();
6896
6897		let origin = Origin::random().produce();
6898		let mut announced = origin.consume().announced();
6899
6900		for _ in 0..1000 {
6901			let mut broadcast = origin.create_broadcast("test", announce()).unwrap();
6902			settle().await;
6903			broadcast.finish();
6904		}
6905		settle().await;
6906
6907		let mut collected = Vec::new();
6908		while let Some(update) = announced.try_next() {
6909			collected.push(update);
6910		}
6911		assert!(
6912			collected.len() <= 1,
6913			"expected at most one pending update, got {}",
6914			collected.len()
6915		);
6916		assert!(
6917			collected.iter().all(|a| a.path == Path::new("test")),
6918			"unexpected path in pending updates",
6919		);
6920	}
6921
6922	// Consumer should be cheap to clone: cloning must NOT drain any
6923	// other cursor's announce channel. A freshly-built AnnounceConsumer
6924	// still receives the active backlog.
6925	#[tokio::test]
6926	async fn test_consumer_clone_is_side_effect_free() {
6927		let origin = Origin::random().produce();
6928
6929		let _broadcast1 = origin.create_broadcast("test1", announce()).unwrap();
6930		let _broadcast2 = origin.create_broadcast("test2", announce()).unwrap();
6931		settle().await;
6932
6933		let consumer = origin.consume();
6934		let mut announced = consumer.announced();
6935
6936		// Cloning the Consumer many times and looking up broadcasts
6937		// must not consume any events from the existing cursor.
6938		for _ in 0..16 {
6939			let cloned = consumer.clone();
6940			assert!(cloned.get_broadcast("test1").is_some());
6941			assert!(cloned.get_broadcast("test2").is_some());
6942		}
6943
6944		// The original cursor still sees both announcements in their
6945		// natural order, undisturbed by the clones above.
6946		let a1 = announced.try_next().expect("first announcement");
6947		let a2 = announced.try_next().expect("second announcement");
6948		announced.assert_next_wait();
6949
6950		let mut paths: Vec<_> = [&a1, &a2].iter().map(|a| a.path.to_string()).collect();
6951		paths.sort();
6952		assert_eq!(paths, ["test1", "test2"]);
6953
6954		// A freshly-built AnnounceConsumer still receives the active backlog.
6955		let mut fresh = consumer.announced();
6956		let b1 = fresh.try_next().expect("backlog: first");
6957		let b2 = fresh.try_next().expect("backlog: second");
6958		fresh.assert_next_wait();
6959
6960		let mut paths: Vec<_> = [&b1, &b2].iter().map(|a| a.path.to_string()).collect();
6961		paths.sort();
6962		assert_eq!(paths, ["test1", "test2"]);
6963	}
6964
6965	// With no Dynamic handler, an unannounced path resolves to Unroutable.
6966	#[tokio::test]
6967	async fn dynamic_request_unroutable_without_handler() {
6968		let origin = Origin::random().produce();
6969		let consumer = origin.consume();
6970		assert!(matches!(
6971			consumer.request_broadcast("missing").await,
6972			Err(Error::Unroutable)
6973		));
6974	}
6975
6976	// A dynamically served broadcast resolves the requester and serves tracks, but is
6977	// never announced.
6978	#[tokio::test(start_paused = true)]
6979	async fn dynamic_request_served_not_announced() {
6980		let origin = Origin::random().produce();
6981		let mut dynamic = origin.dynamic();
6982		let consumer = origin.consume();
6983
6984		// A separate announce cursor must never observe the dynamic broadcast.
6985		let mut announced = origin.consume().announced();
6986		announced.assert_next_wait();
6987
6988		let served = broadcast::Info::new().produce();
6989		// Request a path that nobody announced; the future stays pending until served.
6990		// Registration happens up front, so the handler sees the request immediately.
6991		let request_fut = consumer.request_broadcast("fallback");
6992
6993		// The handler serves it with a live broadcast it keeps producing into.
6994		let mut served_dynamic = served.dynamic();
6995
6996		let request = dynamic.requested_broadcast().await.unwrap();
6997		assert_eq!(request.path(), &Path::new("fallback"));
6998		request.accept(&served);
6999
7000		let broadcast = request_fut.await.unwrap();
7001		assert!(broadcast.is_clone(&served.consume()));
7002
7003		// The served broadcast is live: a track subscription resolves via its handler.
7004		let track_fut = broadcast.track("video").unwrap().subscribe(None);
7005		let mut producer = served_dynamic.requested_track().await.unwrap().accept(None);
7006		let mut track = track_fut.await.unwrap();
7007		producer.append_group().unwrap();
7008		track.assert_group();
7009
7010		// Still nothing announced.
7011		announced.assert_next_wait();
7012	}
7013
7014	// Concurrent requests for the same queued path coalesce onto one handler request.
7015	#[tokio::test(start_paused = true)]
7016	async fn dynamic_request_coalesces() {
7017		let origin = Origin::random().produce();
7018		let mut dynamic = origin.dynamic();
7019		let consumer = origin.consume();
7020
7021		// Both register before the handler drains either.
7022		let f1 = consumer.request_broadcast("dup");
7023		let f2 = consumer.request_broadcast("dup");
7024
7025		// Exactly one request reaches the handler.
7026		let request = dynamic.requested_broadcast().await.unwrap();
7027		assert_eq!(request.path(), &Path::new("dup"));
7028		assert!(
7029			dynamic.requested_broadcast().now_or_never().is_none(),
7030			"a coalesced request must not be served twice"
7031		);
7032
7033		// Accepting resolves both awaiting requesters with the same broadcast.
7034		let served = broadcast::Info::new().produce();
7035		request.accept(&served);
7036		assert!(f1.await.unwrap().is_clone(&served.consume()));
7037		assert!(f2.await.unwrap().is_clone(&served.consume()));
7038	}
7039
7040	// A repeat request for an already-served, still-live path shares the same broadcast
7041	// instead of asking the handler again (no duplicate upstream subscription).
7042	#[tokio::test(start_paused = true)]
7043	async fn dynamic_request_dedups_served() {
7044		let origin = Origin::random().produce();
7045		let mut dynamic = origin.dynamic();
7046		let consumer = origin.consume();
7047
7048		let request_fut = consumer.request_broadcast("fallback");
7049		let request = dynamic.requested_broadcast().await.unwrap();
7050		let served = broadcast::Info::new().produce();
7051		request.accept(&served);
7052		let first = request_fut.await.unwrap();
7053		assert!(first.is_clone(&served.consume()));
7054
7055		// The repeat resolves immediately to the same broadcast...
7056		let second = consumer.request_broadcast("fallback").await.unwrap();
7057		assert!(second.is_clone(&served.consume()));
7058
7059		// ...and the handler never sees a second request.
7060		assert!(
7061			dynamic.requested_broadcast().now_or_never().is_none(),
7062			"a still-live served broadcast must not be re-requested from the handler"
7063		);
7064	}
7065
7066	// Once a served broadcast closes, its cache entry is stale, so the next request re-serves.
7067	#[tokio::test(start_paused = true)]
7068	async fn dynamic_request_reserves_after_close() {
7069		let origin = Origin::random().produce();
7070		let mut dynamic = origin.dynamic();
7071		let consumer = origin.consume();
7072
7073		let request_fut = consumer.request_broadcast("fallback");
7074		let request = dynamic.requested_broadcast().await.unwrap();
7075		let served = broadcast::Info::new().produce();
7076		request.accept(&served);
7077		request_fut.await.unwrap();
7078
7079		// Close the first served broadcast; the weak cache entry goes stale.
7080		drop(served);
7081
7082		// A fresh request must reach the handler again and resolve to the new broadcast.
7083		let request_fut = consumer.request_broadcast("fallback");
7084		let request = dynamic.requested_broadcast().await.unwrap();
7085		assert_eq!(request.path(), &Path::new("fallback"));
7086		let served = broadcast::Info::new().produce();
7087		request.accept(&served);
7088		assert!(request_fut.await.unwrap().is_clone(&served.consume()));
7089	}
7090
7091	// Serving many distinct one-shot paths that each close must not grow the `served` cache
7092	// unboundedly: the amortized GC on `accept` reclaims the stale entries left by closed ones.
7093	#[tokio::test(start_paused = true)]
7094	async fn dynamic_request_served_cache_bounded() {
7095		let origin = Origin::random().produce();
7096		let mut dynamic = origin.dynamic();
7097		let consumer = origin.consume();
7098
7099		for i in 0..100 {
7100			let path = format!("one-shot/{i}");
7101			let request_fut = consumer.request_broadcast(&path);
7102			let request = dynamic.requested_broadcast().await.unwrap();
7103			let served = broadcast::Info::new().produce();
7104			request.accept(&served);
7105			request_fut.await.unwrap();
7106			// Close the served broadcast; its cache entry is now stale.
7107			drop(served);
7108		}
7109
7110		// The GC keeps the map bounded by the live count (zero here) plus a small probe window,
7111		// rather than one entry per distinct path.
7112		assert!(
7113			origin.dynamic.read().served.len() <= 4,
7114			"stale served entries must be reclaimed, not accumulate per distinct path: {}",
7115			origin.dynamic.read().served.len()
7116		);
7117	}
7118
7119	// A repeat request in the window after the handler picks one up but before it accepts
7120	// coalesces onto the in-flight request instead of queuing a duplicate.
7121	#[tokio::test(start_paused = true)]
7122	async fn dynamic_request_coalesces_after_handoff() {
7123		let origin = Origin::random().produce();
7124		let mut dynamic = origin.dynamic();
7125		let consumer = origin.consume();
7126
7127		let f1 = consumer.request_broadcast("fallback");
7128		// Handler drains the request but has not accepted yet.
7129		let request = dynamic.requested_broadcast().await.unwrap();
7130
7131		// A second request in this window must not queue another handler request.
7132		let f2 = consumer.request_broadcast("fallback");
7133		assert!(
7134			dynamic.requested_broadcast().now_or_never().is_none(),
7135			"a repeat request during hand-off must coalesce, not re-queue"
7136		);
7137
7138		// Accepting resolves both awaiting requesters with the same broadcast.
7139		let served = broadcast::Info::new().produce();
7140		request.accept(&served);
7141		assert!(f1.await.unwrap().is_clone(&served.consume()));
7142		assert!(f2.await.unwrap().is_clone(&served.consume()));
7143	}
7144
7145	// Dropping a handed-off request without accept/reject rejects every coalesced requester.
7146	#[tokio::test(start_paused = true)]
7147	async fn dynamic_request_dropped_after_handoff() {
7148		let origin = Origin::random().produce();
7149		let mut dynamic = origin.dynamic();
7150		let consumer = origin.consume();
7151
7152		let f1 = consumer.request_broadcast("fallback");
7153		let request = dynamic.requested_broadcast().await.unwrap();
7154		let f2 = consumer.request_broadcast("fallback");
7155
7156		// Abandon it; both requesters resolve to Unroutable instead of hanging.
7157		drop(request);
7158		assert!(matches!(f1.await, Err(Error::Unroutable)));
7159		assert!(matches!(f2.await, Err(Error::Unroutable)));
7160	}
7161
7162	// Rejecting a request resolves the requester with the error.
7163	#[tokio::test(start_paused = true)]
7164	async fn dynamic_request_rejected() {
7165		let origin = Origin::random().produce();
7166		let mut dynamic = origin.dynamic();
7167		let consumer = origin.consume();
7168
7169		let request_fut = consumer.request_broadcast("fallback");
7170
7171		let request = dynamic.requested_broadcast().await.unwrap();
7172		request.reject(Error::Cancel);
7173
7174		assert!(matches!(request_fut.await, Err(Error::Cancel)));
7175	}
7176
7177	// After a rejected hand-off, a fresh request for the same path reaches the handler again:
7178	// the rejected `Request`'s removal + `Drop` leave the request queue consistent
7179	// (a stale/clobbered entry would strand this request or panic the handler).
7180	#[tokio::test(start_paused = true)]
7181	async fn dynamic_request_rerequest_after_reject() {
7182		let origin = Origin::random().produce();
7183		let mut dynamic = origin.dynamic();
7184		let consumer = origin.consume();
7185
7186		let f1 = consumer.request_broadcast("fallback");
7187		dynamic.requested_broadcast().await.unwrap().reject(Error::Unroutable);
7188		assert!(matches!(f1.await, Err(Error::Unroutable)));
7189
7190		let served = broadcast::Info::new().produce();
7191		// A fresh request re-reaches the handler and can be served.
7192		let f2 = consumer.request_broadcast("fallback");
7193		let request = dynamic.requested_broadcast().await.unwrap();
7194		assert_eq!(request.path(), &Path::new("fallback"));
7195		request.accept(&served);
7196		assert!(f2.await.unwrap().is_clone(&served.consume()));
7197	}
7198
7199	// Dropping the last handler resolves queued requests with an error and reverts to
7200	// resolving Unroutable.
7201	#[tokio::test(start_paused = true)]
7202	async fn dynamic_request_handler_dropped() {
7203		let origin = Origin::random().produce();
7204		let dynamic = origin.dynamic();
7205		let consumer = origin.consume();
7206
7207		let request_fut = consumer.request_broadcast("fallback");
7208		drop(dynamic);
7209		assert!(matches!(request_fut.await, Err(Error::Unroutable)));
7210
7211		// With no handler left, a fresh request resolves Unroutable.
7212		assert!(matches!(
7213			consumer.request_broadcast("again").await,
7214			Err(Error::Unroutable)
7215		));
7216	}
7217
7218	// `accept` is decoupled from the dynamic count: once a handler has picked a request up,
7219	// it can still serve it even if every handler (including itself) drops first, flipping the
7220	// count to zero. The in-flight request must not be rejected as `Unroutable`.
7221	#[tokio::test(start_paused = true)]
7222	async fn dynamic_request_accept_after_handler_dropped() {
7223		let origin = Origin::random().produce();
7224		let mut dynamic = origin.dynamic();
7225		let consumer = origin.consume();
7226
7227		let request_fut = consumer.request_broadcast("fallback");
7228
7229		// The handler picks the request up, then every handler drops (count -> 0).
7230		let request = dynamic.requested_broadcast().await.unwrap();
7231		drop(dynamic);
7232
7233		let served = broadcast::Info::new().produce();
7234		// Accept still resolves the awaiting requester with the served broadcast.
7235		request.accept(&served);
7236		assert!(request_fut.await.unwrap().is_clone(&served.consume()));
7237	}
7238
7239	// A published broadcast wins over the dynamic fallback; no request is queued.
7240	#[tokio::test(start_paused = true)]
7241	async fn dynamic_request_prefers_announced() {
7242		let origin = Origin::random().produce();
7243		let mut dynamic = origin.dynamic();
7244		let consumer = origin.consume();
7245
7246		let _broadcast = origin.create_broadcast("live", announce()).unwrap();
7247		settle().await;
7248
7249		let got = consumer.request_broadcast("live").await.unwrap();
7250		assert!(
7251			got.is_clone(&consumer.get_broadcast("live").unwrap()),
7252			"should return the published broadcast"
7253		);
7254		assert!(
7255			dynamic.requested_broadcast().now_or_never().is_none(),
7256			"a published path must not queue a fallback request"
7257		);
7258	}
7259
7260	// Cloning a handler and dropping the clone must not flip the count to zero.
7261	#[tokio::test(start_paused = true)]
7262	async fn dynamic_clone_keeps_alive() {
7263		let origin = Origin::random().produce();
7264		let dynamic = origin.dynamic();
7265		let consumer = origin.consume();
7266
7267		drop(dynamic.clone());
7268
7269		// The original handle is still live, so the request registers (stays pending)
7270		// instead of resolving Unroutable.
7271		let request_fut = consumer.request_broadcast("fallback");
7272		assert!(
7273			request_fut.now_or_never().is_none(),
7274			"request should stay pending until served"
7275		);
7276	}
7277
7278	/// Run `scenario` on its own thread with a current_thread runtime and fail
7279	/// if it does not complete within `secs`. A `serve_track` task that spins
7280	/// inside a single poll (the livelock class this guards against) never
7281	/// yields, so the runtime wedges and the scenario cannot finish; a timeout
7282	/// here IS the detection, not flakiness.
7283	fn wedge_watchdog<F>(name: &str, secs: u64, scenario: F)
7284	where
7285		F: std::future::Future<Output = ()> + Send + 'static,
7286	{
7287		let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
7288		let handle = std::thread::spawn(move || {
7289			let rt = ::tokio::runtime::Builder::new_current_thread()
7290				.enable_time()
7291				.build()
7292				.unwrap();
7293			rt.block_on(scenario);
7294			let _ = done_tx.send(());
7295		});
7296		match done_rx.recv_timeout(std::time::Duration::from_secs(secs)) {
7297			Ok(()) => {
7298				let _ = handle.join();
7299			}
7300			Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
7301				// The scenario thread panicked before sending: surface it.
7302				let err = handle.join().unwrap_err();
7303				std::panic::resume_unwind(err);
7304			}
7305			Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
7306				panic!("{name}: scenario wedged; a task is spinning inside a single poll")
7307			}
7308		}
7309	}
7310
7311	/// A closing route that is still `active` (its watcher has not detached it
7312	/// yet) must not be re-dispatched after a successful takeover from a
7313	/// healthy standby. If the takeover re-admits the corpse, serve_track
7314	/// cycles splice(corpse) -> splice(healthy) -> takeover -> splice(corpse)
7315	/// with no await: a livelock inside one task poll that also starves the
7316	/// watcher whose detach would end it.
7317	///
7318	/// The setup makes every step of the cycle synchronous: the healthy source
7319	/// holds a live producer (so a re-splice needs no handler roundtrip), and
7320	/// the corpse is aborted while its track request is still pending (so
7321	/// serve_track, a value waiter, wakes before the corpse's closed-watcher
7322	/// and observes the corpse still attached).
7323	#[test]
7324	fn test_active_corpse_does_not_livelock_takeover() {
7325		wedge_watchdog("active-corpse", 20, async {
7326			let origin = Origin::random().produce();
7327			let consumer = origin.consume();
7328			let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
7329
7330			// The healthy source: accepts the track and keeps the producer
7331			// alive, so a later re-splice resolves synchronously from cache.
7332			let source_a = origin
7333				.create_broadcast("test", announce().with_hops(hops.clone()))
7334				.unwrap();
7335			let mut dynamic_a = source_a.dynamic();
7336			settle().await;
7337			settle().await;
7338
7339			let broadcast = consumer.request_broadcast("test").await.unwrap();
7340			let subscribing = broadcast.track("video").unwrap().subscribe(None);
7341			let mut producer_a = accept_track(&mut dynamic_a, "video").await;
7342			settle().await;
7343			let mut sub = subscribing.await.unwrap();
7344			producer_a.append_group().unwrap();
7345			sub.assert_group();
7346
7347			// A newer same-cost source attaches: recency wins the tie, so it
7348			// becomes `active` and the serve task asks it for the track. Leave
7349			// the request unanswered.
7350			let source_b = origin
7351				.create_broadcast("test", announce().with_hops(hops.clone()))
7352				.unwrap();
7353			let dynamic_b = source_b.dynamic();
7354			settle().await;
7355
7356			// Kill it with the request still pending: a corpse that is still
7357			// attached and still `active`. Dropping the handler first queues the
7358			// serve task's request-failure wake ahead of the corpse watcher's
7359			// closed-wake, so the serve task observes the corpse before the
7360			// watcher can detach it - the fleet-wedge ordering. It must fail
7361			// over to the healthy cached copy and park there instead of
7362			// re-dispatching the corpse.
7363			drop(dynamic_b);
7364			source_b.abort(Error::Dropped).unwrap();
7365			settle().await;
7366
7367			// Progress through the healthy source proves the serve task parked
7368			// instead of spinning.
7369			producer_a.append_group().unwrap();
7370			sub.assert_group();
7371			sub.assert_not_closed();
7372		});
7373	}
7374
7375	/// Randomized source/subscriber churn over one path: sources attach with
7376	/// per-track behaviors (refuse, serve-then-abort, serve-and-hold, finish),
7377	/// detach by abort or plain drop, and subscribers come and go. A wedge net
7378	/// for the livelock class the test above pins down. The LCG makes each
7379	/// seed's action sequence repeatable, but task interleaving still varies
7380	/// per run, so treat a wedge here as real and shrink it with the seed as a
7381	/// starting point rather than expecting an identical replay.
7382	#[test]
7383	fn test_route_churn_never_wedges() {
7384		for seed in 1..=8u64 {
7385			wedge_watchdog(&format!("churn seed {seed}"), 30, churn_scenario(seed));
7386		}
7387	}
7388
7389	async fn churn_scenario(seed: u64) {
7390		// LCG: deterministic sequence per seed.
7391		let mut rng = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
7392		let mut next = move || {
7393			rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
7394			rng >> 33
7395		};
7396
7397		let origin = Origin::random().produce();
7398		let consumer = origin.consume();
7399		let names: Vec<Arc<str>> = (0..8).map(|i| Arc::from(format!("t{i}"))).collect();
7400
7401		let mut subs: Vec<track::Subscriber> = Vec::new();
7402		let mut pending_subs: Vec<kio::Pending<track::Subscribing>> = Vec::new();
7403
7404		struct Source {
7405			producer: Option<broadcast::Producer>,
7406			server: ::tokio::task::JoinHandle<()>,
7407		}
7408		let mut sources: Vec<Source> = Vec::new();
7409
7410		for step in 0..400u64 {
7411			match next() % 10 {
7412				// Attach a source whose handler randomly refuses / serves-then-
7413				// aborts / serves-and-holds / finishes each track request.
7414				0 | 1 => {
7415					if sources.len() >= 3 {
7416						continue;
7417					}
7418					let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
7419					let route = announce().with_hops(hops).with_cost(next() % 4);
7420					let Ok(source) = origin.create_broadcast("test", route) else {
7421						continue;
7422					};
7423					let mut dynamic = source.dynamic();
7424					let behavior = next();
7425					let server = ::tokio::spawn(async move {
7426						let mut round = 0u64;
7427						let mut kept: Vec<track::Producer> = Vec::new();
7428						while let Ok(request) = dynamic.requested_track().await {
7429							round += 1;
7430							match (behavior >> (round % 16)) % 4 {
7431								0 => drop(request), // refuse
7432								1 => {
7433									let mut producer = request.accept(None);
7434									let _ = producer.create_group(group::Info { sequence: round });
7435									let _ = producer.abort(Error::Dropped);
7436								}
7437								2 => {
7438									let mut producer = request.accept(None);
7439									let _ = producer.create_group(group::Info { sequence: round });
7440									kept.push(producer);
7441								}
7442								_ => {
7443									let mut producer = request.accept(None);
7444									let _ = producer.finish();
7445								}
7446							}
7447						}
7448					});
7449					sources.push(Source {
7450						producer: Some(source),
7451						server,
7452					});
7453				}
7454				// Detach a source, by abort or plain drop.
7455				2 | 3 => {
7456					if sources.is_empty() {
7457						continue;
7458					}
7459					let i = (next() as usize) % sources.len();
7460					let mut source = sources.swap_remove(i);
7461					if next() % 2 == 0
7462						&& let Some(producer) = source.producer.take()
7463					{
7464						let _ = producer.abort(Error::Dropped);
7465					}
7466					source.server.abort();
7467				}
7468				// Subscribe to a random track.
7469				4..=6 => {
7470					if subs.len() + pending_subs.len() >= 24 {
7471						continue;
7472					}
7473					let Some(broadcast) = consumer.get_broadcast("test") else {
7474						continue;
7475					};
7476					let name = &names[(next() as usize) % names.len()];
7477					if let Ok(track) = broadcast.track(name.as_ref()) {
7478						pending_subs.push(track.subscribe(None));
7479					}
7480				}
7481				// Drop a random subscriber.
7482				7 => {
7483					if subs.is_empty() {
7484						continue;
7485					}
7486					let i = (next() as usize) % subs.len();
7487					subs.swap_remove(i);
7488				}
7489				// Drain: resolve pending subscriptions and read groups.
7490				_ => {
7491					for sub in pending_subs.drain(..) {
7492						match ::tokio::time::timeout(std::time::Duration::from_millis(5), sub).await {
7493							Ok(Ok(sub)) => subs.push(sub),
7494							Ok(Err(_)) => {}
7495							// Still pending: dropping unsubscribes.
7496							Err(_) => {}
7497						}
7498					}
7499					for sub in subs.iter_mut() {
7500						while let Some(Ok(Some(_))) = sub.recv_group().now_or_never() {}
7501					}
7502				}
7503			}
7504			if step % 16 == 0 {
7505				settle().await;
7506			}
7507			// Vary task interleaving per seed.
7508			for _ in 0..(next() % 3) {
7509				::tokio::task::yield_now().await;
7510			}
7511		}
7512
7513		for source in sources {
7514			source.server.abort();
7515		}
7516		settle().await;
7517	}
7518}