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