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		let used = resume.is_used();
1911		idle_since = match (serving.is_some(), used) {
1912			(true, false) => idle_since.or_else(|| Some(web_async::time::Instant::now())),
1913			_ => None,
1914		};
1915		deadline.set(idle_since.and_then(|at| at.checked_add(TRACK_IDLE_LINGER)));
1916
1917		let step = {
1918			let skip = |id: u64| refused.contains(&id) || dead.contains(&id);
1919			kio::wait(|waiter| {
1920				// Watch the source table: the front closing, a better servable
1921				// source than the one spliced in (skipping any we already know
1922				// can't serve this track), or the served route leaving the table,
1923				// which retires the refusals collected against it. Splicing waits
1924				// for a reader.
1925				match state.poll(waiter, |s| {
1926					let gone = serving_id.is_some_and(|id| !s.routes.iter().any(|r| r.id == id));
1927					if s.closed
1928						|| (used && (gone || matches!(s.serve_route(skip), Some(next) if Some(next) != serving_id)))
1929					{
1930						Poll::Ready(())
1931					} else {
1932						Poll::Pending
1933					}
1934				}) {
1935					Poll::Ready(Ok(guard)) => {
1936						if guard.closed {
1937							return Poll::Ready(Step::Closed);
1938						}
1939						let Some(next) = guard.serve_route(skip) else {
1940							return Poll::Ready(Step::Resweep);
1941						};
1942						let source = guard
1943							.routes
1944							.iter()
1945							.find(|r| r.id == next)
1946							.expect("servable source in table")
1947							.source
1948							.clone();
1949						return Poll::Ready(Step::Splice(next, source));
1950					}
1951					Poll::Ready(Err(_)) => return Poll::Ready(Step::Closed),
1952					Poll::Pending => {}
1953				}
1954
1955				// Watch the demand edge in whichever direction is unmet. This has to end
1956				// the wait, not just wake it: `used` and the countdown are computed by
1957				// the outer loop, so a wake that stayed inside would re-poll with the
1958				// stale value and never arm (or cancel) the linger.
1959				let edge = match used {
1960					true => resume.poll_unused(waiter),
1961					false => resume.poll_used(waiter),
1962				};
1963				if edge.is_ready() {
1964					return Poll::Ready(Step::Demand);
1965				}
1966
1967				// Watch the spliced copy for its end: complete means the logical
1968				// track is over; anything else means the serving source died.
1969				if let Some((_, track)) = &serving
1970					&& let Poll::Ready(result) = track.poll_complete(waiter)
1971				{
1972					return Poll::Ready(match result {
1973						Ok(()) => Step::Complete,
1974						Err(_) => Step::Failed,
1975					});
1976				}
1977
1978				deadline.poll(waiter).map(|_| Step::Idle)
1979			})
1980			.await
1981		};
1982
1983		match step {
1984			// The front's teardown aborts the logical track.
1985			Step::Closed => return,
1986			Step::Complete => {
1987				let _ = resume.finish();
1988				return;
1989			}
1990			Step::Failed => {
1991				// The spliced copy died mid-serve: failover, not a strike.
1992				// Re-splice from the (possibly same) active source.
1993				serving = None;
1994			}
1995			// The outer loop recomputes `used` and the countdown on the next pass.
1996			Step::Demand => {}
1997			// The sweep is closed out at the top of the loop, which clears the
1998			// refusals so the retry below picks a source. The spliced copy stays
1999			// readable until a replacement is proven servable.
2000			Step::Resweep => {}
2001			Step::Idle => {
2002				// Nobody has read the track for the linger: drop the source's copy so
2003				// its session can release the track (and the cached `track::Info` that
2004				// came with it). The logical track stays alive and re-splices on the
2005				// next reader, so a returning viewer or a follow-up fetch resumes.
2006				if resume.release().is_err() {
2007					// Finished or aborted meanwhile; the track is over either way.
2008					return;
2009				}
2010				serving = None;
2011			}
2012			Step::Splice(id, source) => {
2013				// Ask the source for its copy and wait for the info to resolve,
2014				// proving it servable, before splicing it in. Bail out early if
2015				// the table moves on while waiting.
2016				let attempt = match source.track(&name) {
2017					Ok(track) => {
2018						// `into_inner` sheds the `Pending` future wrapper so only
2019						// the pollable (which is `Sync`) is held across the await.
2020						let query = track.info().into_inner();
2021						let skip = |id: u64| refused.contains(&id) || dead.contains(&id);
2022						let info = kio::wait(|waiter| {
2023							if let Poll::Ready(result) = query.poll(waiter) {
2024								return Poll::Ready(Some(result));
2025							}
2026							match state.poll(waiter, |s| {
2027								if s.closed || s.serve_route(skip) != Some(id) {
2028									Poll::Ready(())
2029								} else {
2030									Poll::Pending
2031								}
2032							}) {
2033								Poll::Ready(_) => Poll::Ready(None),
2034								Poll::Pending => Poll::Pending,
2035							}
2036						})
2037						.await;
2038						match info {
2039							// The table changed under us; retry from the top
2040							// without a strike.
2041							None => continue,
2042							// A copy that is already aborted can't be spliced;
2043							// count a strike, or a source pinning a dead track
2044							// alive would spin this loop without ever yielding.
2045							Some(Ok(_)) => match track.poll_complete(&kio::Waiter::noop()) {
2046								Poll::Ready(Err(err)) => Err(err),
2047								_ => Ok(track),
2048							},
2049							Some(Err(err)) => Err(err),
2050						}
2051					}
2052					Err(err) => Err(err),
2053				};
2054
2055				match attempt {
2056					Ok(track) => {
2057						if resume.takeover(&track).is_err() {
2058							// The logical track already ended (finished or
2059							// aborted); nothing left to serve.
2060							return;
2061						}
2062						// A successful splice proves the track servable: reset
2063						// the strike budget. `refused` stays: re-trying the
2064						// refusers now would only re-refuse and re-splice, and
2065						// the loop would spin. A failover clears it instead.
2066						fails = 0;
2067						dead.clear();
2068						serving = Some((id, track));
2069					}
2070					// The source itself closed or deliberately ended: not a
2071					// rejection, so no strike. Park until its watcher detaches
2072					// it and the table promotes a replacement.
2073					Err(_) if source.is_closing() => {
2074						dead.insert(id);
2075						serving = None;
2076					}
2077					// This source can't serve the track: rule it out of this track
2078					// only and let the next-best route try. The strike is spent by
2079					// the sweep, at the top of the loop.
2080					Err(err) => {
2081						tracing::debug!(name = %name, source = id, %err, "source refused track");
2082						refused.insert(id);
2083						serving = None;
2084					}
2085				}
2086			}
2087		}
2088	}
2089}
2090
2091/// Shared fallback request queue for an origin.
2092///
2093/// Lives off to the side of the announce tree because dynamically served broadcasts
2094/// are never announced. Carried in a [`kio::Shared`], so consumers enqueue and handlers
2095/// drain under one lock. Mirrors the fetch state of the track model.
2096#[derive(Default)]
2097struct OriginDynamicState {
2098	// Result channels for pending requests, keyed by absolute path so concurrent
2099	// `request_broadcast` calls for the same path coalesce onto one channel.
2100	requests: Requests<PathOwned, kio::Producer<PendingBroadcast>>,
2101
2102	// Broadcasts a handler has already served, kept weakly so a repeat request for the
2103	// same path resolves to a shared clone instead of re-invoking the handler (which would
2104	// open a duplicate upstream subscription). Weak so a served broadcast still closes once
2105	// its real consumers drop. The cache reclaims closed entries incrementally on insert, so a
2106	// long-lived origin serving many distinct one-shot paths stays bounded by the live count.
2107	served: WeakCache<PathOwned, broadcast::WeakConsumer>,
2108}
2109
2110/// One-shot result of a dynamic broadcast request.
2111///
2112/// Stays `None` until a handler [`accept`](Request::accept)s (yielding the served
2113/// broadcast) or [`reject`](Request::reject)s (yielding an error). The producer is
2114/// dropped right after writing, closing the channel; kio checks the value before the closed
2115/// flag, so an awaiting requester still observes the final result.
2116#[derive(Default)]
2117struct PendingBroadcast {
2118	resolved: Option<Result<broadcast::Consumer, Error>>,
2119}
2120
2121/// Picks up [`Consumer::request_broadcast`] calls for paths that are not announced.
2122///
2123/// The origin-level analogue of [`broadcast::Dynamic`]: where that serves tracks on
2124/// demand within a broadcast, this serves whole broadcasts on demand within an origin. A
2125/// relay uses it as a fallback router, fetching a broadcast from upstream only when a
2126/// downstream consumer asks for an exact path that nobody announced.
2127///
2128/// Served broadcasts are deliberately *not* announced, so they never appear in
2129/// [`Consumer::announced`]. Drop this handle (and every clone) to reject the
2130/// requests still waiting to be served.
2131pub struct Dynamic {
2132	info: Origin,
2133	root: PathOwned,
2134	state: kio::Shared<OriginDynamicState>,
2135}
2136
2137impl Clone for Dynamic {
2138	fn clone(&self) -> Self {
2139		// Mirror `new`: count each live handle. Without this, dropping a clone would
2140		// decrement past `new`'s increment and prematurely flip the handler count to
2141		// zero, making future `request_broadcast` calls return `Unroutable`.
2142		self.state.lock().requests.add_handler();
2143
2144		Self {
2145			info: self.info,
2146			root: self.root.clone(),
2147			state: self.state.clone(),
2148		}
2149	}
2150}
2151
2152impl Dynamic {
2153	fn new(info: Origin, root: PathOwned, state: kio::Shared<OriginDynamicState>) -> Self {
2154		state.lock().requests.add_handler();
2155
2156		Self { info, root, state }
2157	}
2158
2159	/// The origin this handler belongs to.
2160	pub fn info(&self) -> &Origin {
2161		&self.info
2162	}
2163
2164	/// Poll for the next requested broadcast, without blocking.
2165	pub fn poll_requested_broadcast(&mut self, waiter: &kio::Waiter) -> Poll<Result<Request, Error>> {
2166		let mut state = ready!(self.state.poll(waiter, |state| {
2167			if state.requests.has_queued() {
2168				Poll::Ready(())
2169			} else {
2170				Poll::Pending
2171			}
2172		}));
2173
2174		let path = state.requests.pop().expect("predicate guaranteed a request");
2175		// The popped request stays pending, so a repeat request in the window between
2176		// hand-off and accept coalesces onto it instead of re-invoking the handler. The
2177		// producer is a shared clone; `Request::{accept, reject, drop}` removes the
2178		// entry. This mirrors how `poll_requested_track` keeps a served track
2179		// discoverable via the weak cache across the same window.
2180		let producer = state.requests.get(&path).expect("popped key must be pending").clone();
2181		Poll::Ready(Ok(Request {
2182			path,
2183			producer,
2184			state: self.state.clone(),
2185		}))
2186	}
2187
2188	/// Block until a consumer requests an unannounced broadcast, returning a
2189	/// [`Request`] to serve.
2190	pub async fn requested_broadcast(&mut self) -> Result<Request, Error> {
2191		kio::wait(|waiter| self.poll_requested_broadcast(waiter)).await
2192	}
2193
2194	/// Returns the prefix that is automatically stripped from requested paths.
2195	pub fn root(&self) -> &Path<'_> {
2196		&self.root
2197	}
2198}
2199
2200impl Drop for Dynamic {
2201	fn drop(&mut self) {
2202		// Decrement and reject under one lock, so a `request_broadcast` that saw a
2203		// live handler through the same lock can't slip a request past the rejection.
2204		let mut state = self.state.lock();
2205		if state.requests.remove_handler() {
2206			// No handlers left to pop queued requests; drop them, closing their result
2207			// channels so awaiting requesters resolve to `Unroutable`. A request already
2208			// handed to a handler stays, resolved by its `Request` instead.
2209			state.requests.drain_queued();
2210		}
2211	}
2212}
2213
2214/// A pending request for a broadcast that was not announced.
2215///
2216/// Yielded by [`Dynamic::requested_broadcast`]. The requester is awaiting inside
2217/// [`Consumer::request_broadcast`]; [`accept`](Self::accept) resolves it with a live
2218/// broadcast (which the handler keeps producing into) and [`reject`](Self::reject) resolves
2219/// it with an error. Dropping the request without either rejects it.
2220pub struct Request {
2221	// Absolute path that was requested.
2222	path: PathOwned,
2223
2224	// Result channel back to the awaiting requester(s). Writing `resolved` and dropping
2225	// this wakes them with the outcome.
2226	producer: kio::Producer<PendingBroadcast>,
2227
2228	// Shared dynamic state, so `accept` can cache the served broadcast for repeat requests.
2229	state: kio::Shared<OriginDynamicState>,
2230}
2231
2232impl Request {
2233	/// The absolute path that was requested.
2234	pub fn path(&self) -> &Path<'_> {
2235		&self.path
2236	}
2237
2238	/// Accept the request, resolving every awaiting requester with `broadcast`.
2239	///
2240	/// The caller keeps producing into `broadcast` (e.g. a relay proxying tracks from
2241	/// upstream); the requesters receive a consumer for it. The broadcast is *not*
2242	/// announced.
2243	pub fn accept(self, broadcast: impl Consume<broadcast::Consumer>) {
2244		let broadcast = broadcast.consume();
2245
2246		// Move the entry out of the in-flight queue and into the weak `served` cache, so repeat
2247		// requests for this path share the same broadcast instead of asking the handler to serve
2248		// (and subscribe upstream) again. Re-check under the lock: if a live broadcast was already
2249		// served for this path while we were fetching upstream, dedup onto it and drop ours rather
2250		// than replace a good entry with a duplicate subscription.
2251		let resolved = {
2252			let mut state = self.state.lock();
2253			let existing = state.served.insert(self.path.clone(), broadcast.weak());
2254			state
2255				.requests
2256				.remove_if(&self.path, |producer| producer.same_channel(&self.producer));
2257			existing.map(|weak| weak.consume()).unwrap_or(broadcast)
2258		};
2259
2260		if let Ok(mut pending) = self.producer.write() {
2261			pending.resolved = Some(Ok(resolved));
2262		}
2263		// `self.producer` drops here, closing the channel; the value is still observable.
2264	}
2265
2266	/// Reject the request, resolving every awaiting requester with `err`.
2267	pub fn reject(self, err: Error) {
2268		self.state
2269			.lock()
2270			.requests
2271			.remove_if(&self.path, |producer| producer.same_channel(&self.producer));
2272		if let Ok(mut state) = self.producer.write() {
2273			state.resolved = Some(Err(err));
2274		}
2275	}
2276}
2277
2278impl Drop for Request {
2279	fn drop(&mut self) {
2280		// Handed off but neither accepted nor rejected: drop the still-pending entry so its
2281		// producer clone (plus this one) closes the channel, resolving coalesced requesters to
2282		// `Unroutable` rather than hanging.
2283		//
2284		// The identity guard matters: `accept`/`reject` already removed our entry and released
2285		// the lock before we run, so a concurrent request for the same path may have registered
2286		// a *new* one here. Removing unconditionally would clobber it, stranding its requesters.
2287		self.state
2288			.lock()
2289			.requests
2290			.remove_if(&self.path, |producer| producer.same_channel(&self.producer));
2291	}
2292}
2293
2294/// The pollable result of [`Consumer::request_broadcast`].
2295///
2296/// Awaited via the [`kio::Pending`] wrapper; resolves to the [`broadcast::Consumer`]
2297/// immediately when the broadcast was already announced, or once an [`Dynamic`]
2298/// handler serves the request. Resolves to an error if the request is rejected or every
2299/// handler drops before serving it.
2300pub struct Requesting {
2301	inner: RequestState,
2302	// Egress scope applied to the resolved broadcast, so its reads are attributed.
2303	// Empty (no-op) for an untagged consumer.
2304	stats: stats::Scope,
2305}
2306
2307enum RequestState {
2308	// Already announced: resolves immediately with a clone of this broadcast.
2309	Ready(broadcast::Consumer),
2310	// Unroutable at request time: resolves immediately with this error. Baked in so
2311	// `request_broadcast` itself stays infallible.
2312	Failed(Error),
2313	// Awaiting a handler: resolves when the request's result channel is written.
2314	Pending(kio::Consumer<PendingBroadcast>),
2315}
2316
2317impl Requesting {
2318	fn ready(broadcast: broadcast::Consumer) -> Self {
2319		Self {
2320			inner: RequestState::Ready(broadcast),
2321			stats: stats::Scope::default(),
2322		}
2323	}
2324
2325	fn failed(error: Error) -> Self {
2326		Self {
2327			inner: RequestState::Failed(error),
2328			stats: stats::Scope::default(),
2329		}
2330	}
2331
2332	fn pending(consumer: kio::Consumer<PendingBroadcast>) -> Self {
2333		Self {
2334			inner: RequestState::Pending(consumer),
2335			stats: stats::Scope::default(),
2336		}
2337	}
2338
2339	fn with_stats(mut self, scope: stats::Scope) -> Self {
2340		self.stats = scope;
2341		self
2342	}
2343
2344	/// Poll for the requested broadcast without blocking.
2345	pub fn poll_ok(&self, waiter: &kio::Waiter) -> Poll<Result<broadcast::Consumer, Error>> {
2346		match &self.inner {
2347			RequestState::Ready(broadcast) => Poll::Ready(Ok(broadcast.clone().with_stats(self.stats.clone()))),
2348			RequestState::Failed(error) => Poll::Ready(Err(error.clone())),
2349			RequestState::Pending(consumer) => Poll::Ready(
2350				match ready!(consumer.poll(waiter, |state| match &state.resolved {
2351					Some(result) => Poll::Ready(result.clone()),
2352					None => Poll::Pending,
2353				})) {
2354					Ok(result) => result.map(|broadcast| broadcast.with_stats(self.stats.clone())),
2355					// Every handler dropped without resolving: nobody could route it.
2356					Err(_closed) => Err(Error::Unroutable),
2357				},
2358			),
2359		}
2360	}
2361}
2362
2363impl kio::Pollable for Requesting {
2364	type Output = Result<broadcast::Consumer, Error>;
2365
2366	fn poll(&self, waiter: &kio::Waiter) -> Poll<Self::Output> {
2367		self.poll_ok(waiter)
2368	}
2369}
2370
2371/// Derive a read view from a handle.
2372///
2373/// Lets APIs accept either a producer or a consumer (e.g.
2374/// [`Client::with_publisher`](crate::Client::with_publisher),
2375/// [`Request::accept`]). The blanket `&T` impl means you can
2376/// pass by value (`foo(x)`) to hand off ownership, or by reference (`foo(&x)`)
2377/// to keep it, without spelling out `.consume()`.
2378pub trait Consume<T> {
2379	/// Derive a read view (a consumer) from this handle.
2380	fn consume(&self) -> T;
2381}
2382
2383impl<T, U: Consume<T>> Consume<T> for &U {
2384	fn consume(&self) -> T {
2385		(**self).consume()
2386	}
2387}
2388
2389impl Consume<Consumer> for Producer {
2390	fn consume(&self) -> Consumer {
2391		// Mirrors the inherent `Producer::consume`; inlined to avoid the
2392		// inherent-vs-trait `consume` ambiguity. Untagged: egress is tagged
2393		// separately from ingress.
2394		Consumer::new(
2395			self.info,
2396			self.root.clone(),
2397			self.nodes.clone(),
2398			self.dynamic.clone(),
2399			stats::Session::default(),
2400		)
2401	}
2402}
2403
2404impl Consume<Consumer> for Consumer {
2405	fn consume(&self) -> Consumer {
2406		self.clone()
2407	}
2408}
2409
2410impl Consume<broadcast::Consumer> for broadcast::Producer {
2411	fn consume(&self) -> broadcast::Consumer {
2412		// The inherent `consume` shadows this trait method, so this delegates.
2413		self.consume()
2414	}
2415}
2416
2417impl Consume<broadcast::Consumer> for broadcast::Consumer {
2418	fn consume(&self) -> broadcast::Consumer {
2419		self.clone()
2420	}
2421}
2422
2423impl Consume<track::Consumer> for track::Producer {
2424	fn consume(&self) -> track::Consumer {
2425		self.consume()
2426	}
2427}
2428
2429impl Consume<track::Consumer> for track::Consumer {
2430	fn consume(&self) -> track::Consumer {
2431		self.clone()
2432	}
2433}
2434
2435/// Cheap read handle over an origin's broadcast tree.
2436///
2437/// Clones share the underlying tree state without allocating any per-cursor
2438/// resources. To actually receive announce / unannounce events, call
2439/// [`Self::announced`] to obtain an [`AnnounceConsumer`].
2440#[derive(Clone)]
2441pub struct Consumer {
2442	// Identity of the origin this consumer was derived from.
2443	info: Origin,
2444	nodes: OriginNodes,
2445
2446	// A prefix that is automatically stripped from all paths.
2447	root: PathOwned,
2448
2449	// Shared fallback request queue, fed to any `Dynamic` handler on the
2450	// producer side. Used only by `request_broadcast`; announced lookups ignore it.
2451	dynamic: kio::Shared<OriginDynamicState>,
2452
2453	// Egress stats context. Broadcasts handed out through this consumer (and any
2454	// handle derived from them) are attributed to it (reads counted on the
2455	// publisher/egress side). Empty (no-op) unless a session tagged this handle.
2456	stats: stats::Session,
2457
2458	// Data-plane split horizon: broadcasts resolved through this handle are
2459	// served from a source whose hop chain excludes this origin (the requesting
2460	// peer). `None` (the default) serves from the active source as usual.
2461	exclude: Option<Origin>,
2462}
2463
2464impl std::ops::Deref for Consumer {
2465	type Target = Origin;
2466
2467	fn deref(&self) -> &Self::Target {
2468		&self.info
2469	}
2470}
2471
2472impl Consumer {
2473	fn new(
2474		info: Origin,
2475		root: PathOwned,
2476		nodes: OriginNodes,
2477		dynamic: kio::Shared<OriginDynamicState>,
2478		stats: stats::Session,
2479	) -> Self {
2480		Self {
2481			info,
2482			nodes,
2483			root,
2484			dynamic,
2485			stats,
2486			exclude: None,
2487		}
2488	}
2489
2490	/// A clone that never serves the given peer its own data: broadcasts resolve
2491	/// to a source whose hop chain excludes `peer`, matching what the announce
2492	/// loop advertises to them. Sessions apply this once they learn the peer's
2493	/// origin id.
2494	pub(crate) fn excluding(mut self, peer: Origin) -> Self {
2495		self.exclude = Some(peer);
2496		self
2497	}
2498
2499	/// Attach an egress stats context: broadcasts handed out through this handle (and
2500	/// any handle derived from it) are attributed to `session` on the publisher
2501	/// (egress) side. Pass [`stats::Session::default`] to opt out.
2502	pub fn with_stats(mut self, session: stats::Session) -> Self {
2503		self.stats = session;
2504		self
2505	}
2506
2507	/// A clone of this consumer with its stats context cleared, so an internal
2508	/// lookup stream (e.g. [`Self::announced_broadcast`]) doesn't drive the egress
2509	/// announce guards; the caller re-attributes the result itself.
2510	fn untagged(&self) -> Self {
2511		Self {
2512			stats: stats::Session::default(),
2513			..self.clone()
2514		}
2515	}
2516
2517	/// A view with this consumer's identity and root but no broadcasts:
2518	/// [`announced`](Self::announced) yields nothing. Used to answer a peer's
2519	/// announce-interest for a prefix outside our scope by announcing nothing,
2520	/// rather than tearing the stream down.
2521	pub(crate) fn empty(&self) -> Self {
2522		Self {
2523			info: self.info,
2524			nodes: OriginNodes { nodes: Vec::new() },
2525			root: self.root.clone(),
2526			dynamic: self.dynamic.clone(),
2527			stats: self.stats.clone(),
2528			exclude: self.exclude,
2529		}
2530	}
2531
2532	/// Subscribe to announce / unannounce events for this consumer's subtree.
2533	///
2534	/// Allocates a per-cursor coalescing buffer, registers it with each root
2535	/// in this consumer's scope, and replays the currently active broadcast
2536	/// set as initial announcements. Drop the returned [`AnnounceConsumer`]
2537	/// to unregister.
2538	pub fn announced(&self) -> AnnounceConsumer {
2539		AnnounceConsumer::new(self.root.clone(), self.nodes.clone(), self.stats.clone())
2540	}
2541
2542	/// Returns a cheap duplicate of this read handle.
2543	pub fn consume(&self) -> Self {
2544		self.clone()
2545	}
2546
2547	/// Internal synchronous lookup: how the broadcast at `path` resolves for this
2548	/// consumer, telling "announced but every route loops back through you" apart
2549	/// from "nothing here".
2550	///
2551	/// Races announcement gossip (a freshly-connected consumer sees `Missing` even when
2552	/// the broadcast is about to arrive), so it is not public. [`Self::request_broadcast`]
2553	/// is the public lookup: it builds on this for the announced case, then falls back to
2554	/// a dynamic handler. [`Self::announced_broadcast`] waits for a future announcement.
2555	fn resolve(&self, path: impl AsPath) -> Resolved {
2556		let path = path.as_path();
2557		let Some((root, rest)) = self.nodes.get(&path) else {
2558			return Resolved::Missing;
2559		};
2560		let state = root.lock();
2561		state.resolve_broadcast(&rest, self.exclude)
2562	}
2563
2564	/// [`Self::resolve`] reduced to "can I read it": the peek the tests assert on.
2565	#[cfg(test)]
2566	fn get_broadcast(&self, path: impl AsPath) -> Option<broadcast::Consumer> {
2567		match self.resolve(path) {
2568			Resolved::Found(broadcast) => Some(broadcast),
2569			Resolved::Excluded | Resolved::Missing => None,
2570		}
2571	}
2572
2573	/// Block until a broadcast with the given path is announced and return it.
2574	///
2575	/// Returns `None` if the path is outside this consumer's allowed prefixes or if the consumer
2576	/// is closed before the broadcast is announced. The returned broadcast may itself be closed
2577	/// later. Subscribers should watch [`broadcast::Consumer::closed`] to react to that.
2578	///
2579	/// Prefer this over [`Self::request_broadcast`] when you know the exact path you want but
2580	/// cannot guarantee the announcement has already been received. With moq-lite-05 (and
2581	/// the older Lite01/02) `connect()` already blocks until the initial announce set lands,
2582	/// so [`Self::request_broadcast`] is race-free for broadcasts that were live at connect time;
2583	/// this method is still needed to wait for a broadcast that comes online *after* connect.
2584	pub async fn announced_broadcast(&self, path: impl AsPath) -> Option<broadcast::Consumer> {
2585		let path = path.as_path();
2586
2587		// Scope a fresh consumer down to this path so we only wake up for relevant announcements.
2588		let consumer = self.scope(std::slice::from_ref(&path))?;
2589
2590		// `scope` keeps narrower permissions intact: if we ask for `foo` on a consumer limited
2591		// to `foo/specific`, `scope` returns a consumer scoped to `foo/specific`. No
2592		// announcement at the exact path `foo` can ever arrive. Bail rather than loop forever.
2593		if !consumer.allowed().any(|allowed| path.has_prefix(allowed)) {
2594			return None;
2595		}
2596
2597		// Use an untagged stream: this is a lookup, not egress announce forwarding, so
2598		// it must not drive the announce guards. The matched result is attributed
2599		// with the egress scope instead.
2600		let mut announced = consumer.untagged().announced();
2601		let scope = self.stats.egress(self.root.join(&path).to_owned());
2602		loop {
2603			let OriginAnnounce {
2604				path: announced_path,
2605				broadcast,
2606			} = announced.next().await?;
2607			// `scope` narrows by prefix, but we only want an exact-path match.
2608			if announced_path.as_path() == path
2609				&& let Some(broadcast) = broadcast
2610			{
2611				return Some(broadcast.with_stats(scope));
2612			}
2613		}
2614	}
2615
2616	/// Returns a new Consumer restricted to broadcasts under one of `prefixes`.
2617	///
2618	/// Returns None if there are no legal prefixes (the requested prefixes are
2619	/// disjoint from this consumer's current scope, so it would always return None).
2620	// TODO accept PathPrefixes instead of &[Path]
2621	pub fn scope(&self, prefixes: &[Path]) -> Option<Consumer> {
2622		let prefixes = PathPrefixes::new(prefixes);
2623		Some(Consumer {
2624			info: self.info,
2625			root: self.root.clone(),
2626			nodes: self.nodes.select(&prefixes)?,
2627			dynamic: self.dynamic.clone(),
2628			stats: self.stats.clone(),
2629			exclude: self.exclude,
2630		})
2631	}
2632
2633	/// Get a broadcast by path, falling back to a dynamic request when it is not announced.
2634	///
2635	/// Returns a [`kio::Pending`] future (resolved synchronously for an announced broadcast,
2636	/// otherwise once a handler serves it), mirroring [`track::Consumer::fetch_group`](track::Consumer::fetch_group).
2637	/// The lookup order is: an already-announced broadcast resolves
2638	/// immediately; otherwise, if an [`Dynamic`] handler is live (see
2639	/// [`Producer::dynamic`]), a fallback request is registered and the future resolves
2640	/// when the handler [`accept`](Request::accept)s it (or errors if it
2641	/// [`reject`](Request::reject)s or every handler drops). Concurrent requests for
2642	/// the same unannounced path coalesce onto one handler request, and once served the
2643	/// broadcast is cached weakly so *later* requests for that path also share it (rather
2644	/// than re-invoking the handler and opening a duplicate upstream subscription) for as
2645	/// long as it stays live; a closed one is re-served on the next request.
2646	///
2647	/// The returned future resolves to [`Error::Unroutable`] when the path is not announced and no
2648	/// dynamic handler exists. A request that is registered while a handler is live but then loses
2649	/// every handler before being served also resolves to [`Error::Unroutable`]. Unlike an announced
2650	/// broadcast, a dynamically served one is never visible to [`Self::announced`].
2651	pub fn request_broadcast(&self, path: impl AsPath) -> kio::Pending<Requesting> {
2652		let path = path.as_path();
2653
2654		// Key requests by absolute path so a scoped/rooted consumer and the handler
2655		// (which may have a different root) agree on the same entry, and so the egress
2656		// counters resolve against the same broadcast the ingress side wrote.
2657		let absolute = self.root.join(&path).to_owned();
2658		let scope = self.stats.egress(&absolute);
2659
2660		// Prefer a live announcement when one is present; the dynamic queue is only a
2661		// fallback for a path we hold nothing for. A broadcast we do hold but cannot
2662		// serve this requester is unroutable, not missing: a handler resolves paths
2663		// with no route chain to check, so falling through would let it route around
2664		// the split horizon and rebuild the loop.
2665		match self.resolve(&path) {
2666			Resolved::Found(broadcast) => return kio::Pending::new(Requesting::ready(broadcast).with_stats(scope)),
2667			Resolved::Excluded => return kio::Pending::new(Requesting::failed(Error::Unroutable)),
2668			Resolved::Missing => {}
2669		}
2670
2671		let mut state = self.dynamic.lock();
2672
2673		// Reuse a still-live broadcast a handler already served for this path, so repeat
2674		// requests share one upstream subscription. A closed entry is stale; `get` drops it
2675		// and returns `None`, so we fall through and re-serve below.
2676		if let Some(weak) = state.served.get(&absolute) {
2677			return kio::Pending::new(Requesting::ready(weak.consume()).with_stats(scope));
2678		}
2679
2680		// Coalesce onto a pending request for the same path; otherwise register a new
2681		// one, unless there is no handler alive to serve it.
2682		let consumer = if let Some(producer) = state.requests.join(&absolute) {
2683			producer.consume()
2684		} else {
2685			let producer = kio::Producer::<PendingBroadcast>::default();
2686			let consumer = producer.consume();
2687			if state.requests.insert(absolute, producer).is_err() {
2688				return kio::Pending::new(Requesting::failed(Error::Unroutable));
2689			}
2690			consumer
2691		};
2692
2693		kio::Pending::new(Requesting::pending(consumer).with_stats(scope))
2694	}
2695
2696	/// Returns a new Consumer that automatically strips out the provided prefix.
2697	///
2698	/// Returns None if the provided root is not authorized; when [`Self::scope`] was
2699	/// already used without a wildcard.
2700	pub fn with_root(&self, prefix: impl AsPath) -> Option<Self> {
2701		let prefix = prefix.as_path();
2702
2703		Some(Self {
2704			info: self.info,
2705			root: self.root.join(&prefix).to_owned(),
2706			nodes: self.nodes.root(&prefix)?,
2707			dynamic: self.dynamic.clone(),
2708			stats: self.stats.clone(),
2709			exclude: self.exclude,
2710		})
2711	}
2712
2713	/// Returns the prefix that is automatically stripped from all paths.
2714	pub fn root(&self) -> &Path<'_> {
2715		&self.root
2716	}
2717
2718	/// Iterate over the path prefixes this handle is permitted to publish or subscribe under.
2719	// TODO return PathPrefixes
2720	pub fn allowed(&self) -> impl Iterator<Item = &Path<'_>> {
2721		self.nodes.nodes.iter().map(|(root, _)| root)
2722	}
2723
2724	/// Converts a relative path to an absolute path.
2725	pub fn absolute(&self, path: impl AsPath) -> Path<'_> {
2726		self.root.join(path)
2727	}
2728}
2729
2730/// Handle to the announcement stream for a subtree.
2731///
2732/// Symmetric counterpart of [`AnnounceConsumer`]. Cheap to clone; call
2733/// [`Self::consume`] to obtain an [`AnnounceConsumer`] that receives events.
2734#[derive(Clone)]
2735pub struct AnnounceProducer {
2736	nodes: OriginNodes,
2737	root: PathOwned,
2738}
2739
2740impl AnnounceProducer {
2741	fn new(root: PathOwned, nodes: OriginNodes) -> Self {
2742		Self { nodes, root }
2743	}
2744
2745	/// Subscribe to announce / unannounce events for this subtree.
2746	///
2747	/// Allocates a per-cursor coalescing buffer and replays the currently active broadcast set
2748	/// as initial announcements. Drop the returned [`AnnounceConsumer`] to
2749	/// unregister.
2750	pub fn consume(&self) -> AnnounceConsumer {
2751		// Untagged: `AnnounceProducer` is used for internal announce plumbing, not
2752		// egress attribution (which flows through `origin::Consumer::announced`).
2753		AnnounceConsumer::new(self.root.clone(), self.nodes.clone(), stats::Session::default())
2754	}
2755
2756	/// Returns the prefix that is automatically stripped from announced paths.
2757	pub fn root(&self) -> &Path<'_> {
2758		&self.root
2759	}
2760}
2761
2762/// Receives announce / unannounce events for a subtree.
2763///
2764/// Created by [`Consumer::announced`] or [`AnnounceProducer::consume`].
2765/// Drop to unregister.
2766pub struct AnnounceConsumer {
2767	id: ConsumerId,
2768	nodes: OriginNodes,
2769	root: PathOwned,
2770
2771	// Pending updates queued for this cursor. Coalesced so a slow consumer
2772	// can't accumulate redundant announce/unannounce pairs.
2773	state: kio::Producer<OriginConsumerState>,
2774
2775	// Egress stats context (empty for an untagged stream). Announce events drive the
2776	// per-broadcast announce guards below and tag the broadcasts handed out.
2777	stats: stats::Session,
2778
2779	// Live egress announce guards, keyed by absolute broadcast path. An announce
2780	// opens one (bumping `announced` + `announced_bytes`); the matching unannounce
2781	// drops it (bumping `announced_closed` + `announced_bytes`).
2782	guards: HashMap<PathOwned, stats::Announce>,
2783}
2784
2785impl AnnounceConsumer {
2786	fn new(root: PathOwned, nodes: OriginNodes, stats: stats::Session) -> Self {
2787		let state = kio::Producer::<OriginConsumerState>::default();
2788		let id = ConsumerId::new();
2789
2790		for (_, node) in &nodes.nodes {
2791			let notify = AnnounceConsumerNotify {
2792				root: root.clone(),
2793				state: state.clone(),
2794			};
2795			node.lock().consume(id, notify);
2796		}
2797
2798		Self {
2799			id,
2800			nodes,
2801			root,
2802			state,
2803			stats,
2804			guards: HashMap::new(),
2805		}
2806	}
2807
2808	/// Drive the egress announce guards and tag the broadcast for one update.
2809	///
2810	/// An announce opens a guard (keyed by absolute path) and tags the yielded
2811	/// broadcast with the egress scope; an unannounce drops the guard. A no-op for
2812	/// an untagged stream.
2813	fn attribute(&mut self, update: OriginAnnounce) -> OriginAnnounce {
2814		let OriginAnnounce { path, broadcast } = update;
2815		let absolute = self.root.join(&path).to_owned();
2816		match broadcast {
2817			Some(broadcast) => {
2818				let scope = self.stats.egress(&absolute);
2819				self.guards.entry(absolute).or_insert_with(|| scope.announce());
2820				OriginAnnounce {
2821					path,
2822					broadcast: Some(broadcast.with_stats(scope)),
2823				}
2824			}
2825			None => {
2826				self.guards.remove(&absolute);
2827				OriginAnnounce { path, broadcast: None }
2828			}
2829		}
2830	}
2831
2832	/// Returns the next (un)announced broadcast and its path relative to this
2833	/// cursor's root.
2834	///
2835	/// The broadcast will only be announced if it was previously unannounced.
2836	/// The same path won't be announced/unannounced twice in a row; instead it
2837	/// toggles. Returns None if the cursor is closed.
2838	pub async fn next(&mut self) -> Option<OriginAnnounce> {
2839		kio::wait(|waiter| self.poll_next(waiter)).await
2840	}
2841
2842	/// Poll for the next (un)announced broadcast, without blocking.
2843	///
2844	/// Returns `Poll::Ready(Some(_))` for an update, `Poll::Ready(None)` if the
2845	/// cursor is closed, or `Poll::Pending` after registering `waiter` to be
2846	/// notified when the next update arrives.
2847	pub fn poll_next(&mut self, waiter: &kio::Waiter) -> Poll<Option<OriginAnnounce>> {
2848		let update = {
2849			let mut state = match ready!(self.state.poll(waiter, |state| {
2850				if state.pending.is_empty() {
2851					Poll::Pending
2852				} else {
2853					Poll::Ready(())
2854				}
2855			})) {
2856				Ok(state) => state,
2857				// Closed: discard the Ref so its MutexGuard doesn't escape this call.
2858				Err(_) => return Poll::Ready(None),
2859			};
2860			state.take().expect("predicate guaranteed an update")
2861		};
2862		Poll::Ready(Some(self.attribute(update)))
2863	}
2864
2865	/// Returns the next (un)announced broadcast without blocking.
2866	///
2867	/// Returns None if there is no update available; NOT because the cursor is closed.
2868	/// Use [`Self::is_closed`] to check if the cursor is closed.
2869	pub fn try_next(&mut self) -> Option<OriginAnnounce> {
2870		let update = self.state.write().ok()?.take()?;
2871		Some(self.attribute(update))
2872	}
2873
2874	/// Returns true if the cursor is closed (no more updates will arrive).
2875	pub fn is_closed(&self) -> bool {
2876		self.state.write().is_err()
2877	}
2878
2879	/// Returns the prefix that is automatically stripped from emitted paths.
2880	pub fn root(&self) -> &Path<'_> {
2881		&self.root
2882	}
2883
2884	/// Converts a relative path to an absolute path.
2885	pub fn absolute(&self, path: impl AsPath) -> Path<'_> {
2886		self.root.join(path)
2887	}
2888}
2889
2890impl Drop for AnnounceConsumer {
2891	fn drop(&mut self) {
2892		for (_, root) in &self.nodes.nodes {
2893			root.lock().unconsume(self.id);
2894		}
2895	}
2896}
2897
2898#[cfg(test)]
2899use futures::FutureExt;
2900
2901#[cfg(test)]
2902#[allow(missing_docs)] // test-only assertion helpers
2903impl AnnounceConsumer {
2904	pub fn assert_next(&mut self, expected: impl AsPath, broadcast: &broadcast::Consumer) {
2905		let expected = expected.as_path();
2906		let announce = self.next().now_or_never().expect("next blocked").expect("no next");
2907		assert_eq!(announce.path, expected, "wrong path");
2908		let announced = announce.broadcast.expect("should be an active announce");
2909		assert!(announced.is_clone(broadcast), "should be the same broadcast");
2910	}
2911
2912	/// An announce for `expected`, without asserting which broadcast backs it
2913	/// (the origin owns the announced broadcast, not the publisher). Returns the
2914	/// announced consumer.
2915	pub fn assert_next_some(&mut self, expected: impl AsPath) -> broadcast::Consumer {
2916		let expected = expected.as_path();
2917		let announce = self.next().now_or_never().expect("next blocked").expect("no next");
2918		assert_eq!(announce.path, expected, "wrong path");
2919		announce.broadcast.expect("should be an active announce")
2920	}
2921
2922	pub fn assert_try_next(&mut self, expected: impl AsPath, broadcast: &broadcast::Consumer) {
2923		let expected = expected.as_path();
2924		let announce = self.try_next().expect("no next");
2925		assert_eq!(announce.path, expected, "wrong path");
2926		let announced = announce.broadcast.expect("should be an active announce");
2927		assert!(announced.is_clone(broadcast), "should be the same broadcast");
2928	}
2929
2930	/// The `try_next` counterpart of [`Self::assert_next_some`].
2931	pub fn assert_try_next_some(&mut self, expected: impl AsPath) -> broadcast::Consumer {
2932		let expected = expected.as_path();
2933		let announce = self.try_next().expect("no next");
2934		assert_eq!(announce.path, expected, "wrong path");
2935		announce.broadcast.expect("should be an active announce")
2936	}
2937
2938	pub fn assert_next_none(&mut self, expected: impl AsPath) {
2939		let expected = expected.as_path();
2940		let announce = self.next().now_or_never().expect("next blocked").expect("no next");
2941		assert_eq!(announce.path, expected, "wrong path");
2942		assert!(announce.broadcast.is_none(), "should be unannounced");
2943	}
2944
2945	pub fn assert_next_wait(&mut self) {
2946		if let Some(res) = self.next().now_or_never() {
2947			panic!("next should block: got {:?}", res.map(|a| a.path));
2948		}
2949	}
2950
2951	/*
2952	pub fn assert_next_closed(&mut self) {
2953		assert!(
2954			self.next().now_or_never().expect("next blocked").is_none(),
2955			"next should be closed"
2956		);
2957	}
2958	*/
2959}
2960
2961#[cfg(test)]
2962mod tests {
2963	use crate::coding::Decode;
2964	use crate::group;
2965
2966	use super::*;
2967
2968	/// An announced direct route.
2969	fn announce() -> broadcast::Route {
2970		broadcast::Route::new().with_announce(true)
2971	}
2972
2973	/// The first origin whose handover key for `name` sits above (`true`) or below
2974	/// (`false`) the peer's, so tests exercising the carrying gate are
2975	/// deterministic instead of hinging on a random id winning a hash comparison.
2976	/// Starts searching above the small ids the tests use in hop chains, so the
2977	/// result never collides with a hop (a looping chain trips a debug_assert).
2978	fn origin_keyed(name: &str, peer: Origin, above: bool) -> Origin {
2979		let name = Path::new(name);
2980		let peer_key = fnv_key(&name, [peer]);
2981		(100u64..)
2982			.map(|id| Origin::new(id).unwrap())
2983			.find(|origin| (fnv_key(&name, [*origin]) > peer_key) == above)
2984			.unwrap()
2985	}
2986
2987	/// A front table for reselect tests: routes get ids in order, the first is
2988	/// the incumbent.
2989	fn front_state(self_origin: Origin, routes: Vec<broadcast::Route>) -> FrontState {
2990		let source = broadcast::Info::new().produce().consume();
2991		FrontState {
2992			path: Path::new("test").to_owned(),
2993			self_origin,
2994			publisher: routes.first().and_then(|r| r.hops.iter().next().copied()),
2995			next_route: routes.len() as u64,
2996			excluded: HashMap::new(),
2997			routes: routes
2998				.into_iter()
2999				.enumerate()
3000				.map(|(id, route)| FrontRoute {
3001					id: id as u64,
3002					route,
3003					source: source.clone(),
3004				})
3005				.collect(),
3006			active: Some(0),
3007			linger: Duration::ZERO,
3008			closed: false,
3009		}
3010	}
3011
3012	/// A route as a warm sibling would announce it: zero cost, chain ending at
3013	/// the announcing peer.
3014	fn sibling_route(peer: Origin) -> broadcast::Route {
3015		let hops = OriginList::try_from(vec![Origin::new(90).unwrap(), peer]).unwrap();
3016		announce().with_hops(hops)
3017	}
3018
3019	/// A route as the upstream announces it: priced, one hop.
3020	fn upstream_route(cost: u64) -> broadcast::Route {
3021		let hops = OriginList::try_from(vec![Origin::new(90).unwrap()]).unwrap();
3022		announce().with_hops(hops).with_cost(cost)
3023	}
3024
3025	/// While carrying, a strictly cheaper route from a peer that hashes above us
3026	/// must not displace the incumbent; the same table re-parents freely once
3027	/// idle, or when the peer hashes below us.
3028	#[test]
3029	fn test_carrying_gate_keys() {
3030		let peer = Origin::new(3).unwrap();
3031
3032		// We lose the key comparison: stay put while carrying, migrate when idle.
3033		let mut lost = front_state(
3034			origin_keyed("test", peer, false),
3035			vec![upstream_route(10), sibling_route(peer)],
3036		);
3037		lost.reselect(true);
3038		assert_eq!(
3039			lost.active,
3040			Some(0),
3041			"carrying front re-parented onto a higher-keyed peer"
3042		);
3043		lost.reselect(false);
3044		assert_eq!(lost.active, Some(1), "idle front must take the cheaper route");
3045
3046		// We win the key comparison: re-parent even while carrying.
3047		let mut won = front_state(
3048			origin_keyed("test", peer, true),
3049			vec![upstream_route(10), sibling_route(peer)],
3050		);
3051		won.reselect(true);
3052		assert_eq!(won.active, Some(1), "carrying front must follow a lower-keyed peer");
3053	}
3054
3055	/// The simultaneous-activation race: two relays that each pulled the same
3056	/// broadcast independently see each other's zero-cost route. Exactly one of
3057	/// them re-parents; the other keeps its upstream, so the broadcast is never
3058	/// left without a source.
3059	#[test]
3060	fn test_carrying_gate_symmetric_race() {
3061		let a = Origin::new(1).unwrap();
3062		let b = Origin::new(2).unwrap();
3063
3064		let mut a_view = front_state(a, vec![upstream_route(10), sibling_route(b)]);
3065		let mut b_view = front_state(b, vec![upstream_route(10), sibling_route(a)]);
3066		a_view.reselect(true);
3067		b_view.reselect(true);
3068
3069		let a_moved = a_view.active == Some(1);
3070		let b_moved = b_view.active == Some(1);
3071		assert!(
3072			a_moved != b_moved,
3073			"exactly one side must re-parent (a: {a_moved}, b: {b_moved})"
3074		);
3075	}
3076
3077	/// The gate is scoped to warm siblings: a cheaper route via a relay that is
3078	/// not itself carrying (advertised nonzero), or directly from the original
3079	/// publisher (single-hop chain), is taken immediately even while carrying
3080	/// and even when we would lose the key comparison.
3081	#[test]
3082	fn test_carrying_switches_to_benign_routes() {
3083		let peer = Origin::new(3).unwrap();
3084		let lost = origin_keyed("test", peer, false);
3085
3086		// A cheaper forwarder path: the relay advertised its accumulated cost.
3087		let mut forwarder = sibling_route(peer).with_cost(4);
3088		forwarder.advertised = 4;
3089		let mut state = front_state(lost, vec![upstream_route(10), forwarder]);
3090		state.reselect(true);
3091		assert_eq!(
3092			state.active,
3093			Some(1),
3094			"a cheaper forwarder path must win while carrying"
3095		);
3096
3097		// Directly from the original publisher: single-hop chain, advertised zero.
3098		let direct = announce().with_hops(OriginList::try_from(vec![peer]).unwrap());
3099		let mut state = front_state(lost, vec![upstream_route(10), direct]);
3100		state.reselect(true);
3101		assert_eq!(
3102			state.active,
3103			Some(1),
3104			"a direct publisher route must win while carrying"
3105		);
3106
3107		// The peer's session reconnecting: same chain, same cost, so the gate's
3108		// strictly-cheaper test does not apply and recency decides. Loosening that
3109		// test to `<=` would hold a carrying front on the dead session until the
3110		// transport timed it out, which is the whole point of the recency order.
3111		let mut state = front_state(lost, vec![sibling_route(peer), sibling_route(peer)]);
3112		state.reselect(true);
3113		assert_eq!(
3114			state.active,
3115			Some(1),
3116			"a reconnect on an identical chain must win while carrying"
3117		);
3118	}
3119
3120	/// The gate only protects an announced incumbent: one that lost its announce
3121	/// (the upstream retracted) is displaced regardless of the key comparison.
3122	#[test]
3123	fn test_carrying_gate_ignores_unannounced_incumbent() {
3124		let peer = Origin::new(3).unwrap();
3125		let unannounced = upstream_route(10).with_announce(false);
3126		let mut state = front_state(
3127			origin_keyed("test", peer, false),
3128			vec![unannounced, sibling_route(peer)],
3129		);
3130		state.reselect(true);
3131		assert_eq!(
3132			state.active,
3133			Some(1),
3134			"an unannounced incumbent must always be displaced"
3135		);
3136	}
3137
3138	/// Let the spawned origin tasks (source watchers, front dispatch) run. The
3139	/// tests pause tokio time, so this advances the clock instantly.
3140	async fn settle() {
3141		tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
3142	}
3143
3144	/// Serve one requested track from a source like a session would: wait for the
3145	/// origin to dispatch it, then accept with default info.
3146	async fn accept_track(dynamic: &mut broadcast::Dynamic, name: &str) -> track::Producer {
3147		let request = tokio::time::timeout(std::time::Duration::from_secs(1), dynamic.requested_track())
3148			.await
3149			.expect("timed out waiting for a track request")
3150			.expect("source closed");
3151		assert_eq!(request.name(), name, "unexpected track dispatched");
3152		request.accept(None)
3153	}
3154
3155	/// Tagging both origin handles with one context attributes the full model path:
3156	/// ingress writes on the subscriber side, egress reads on the publisher side,
3157	/// each counter landing exactly once (the model-layer silent-zero guard).
3158	#[tokio::test]
3159	async fn test_stats_tagged_end_to_end() {
3160		use crate::Timestamp;
3161		use crate::stats::{Config, Registry, Tier};
3162		use bytes::Bytes;
3163
3164		tokio::time::pause();
3165
3166		let registry = Registry::new(Config::new());
3167		let ctx = registry.tier(Tier::default()).session("acme");
3168
3169		let origin = Origin::random().produce();
3170		let ingress = origin.clone().with_stats(ctx.clone());
3171		let egress = origin.consume().with_stats(ctx.clone());
3172
3173		// Egress announce stream: this is the tagged stream that drives the egress
3174		// announce guard.
3175		let mut announced = egress.announced();
3176
3177		// Ingress publishes an announced broadcast.
3178		let source = ingress.create_broadcast("demo", announce()).unwrap();
3179		let mut dynamic = source.dynamic();
3180		settle().await;
3181		settle().await;
3182
3183		// Egress observes the announce and gets the tagged broadcast.
3184		let update = announced.next().await.unwrap();
3185		assert_eq!(update.path.as_str(), "demo");
3186		let broadcast = update.broadcast.unwrap();
3187
3188		// Egress subscribes; the ingress side serves the track on demand.
3189		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3190		let mut producer = accept_track(&mut dynamic, "video").await;
3191		settle().await;
3192		let mut sub = subscribing.await.unwrap();
3193
3194		// Ingress writes one group with two 5-byte frames.
3195		let mut group = producer.append_group().unwrap();
3196		group
3197			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
3198			.unwrap();
3199		group
3200			.write_frame(Timestamp::ZERO, Bytes::from_static(b"world"))
3201			.unwrap();
3202		group.finish().unwrap();
3203
3204		// Egress reads the group and both frames.
3205		let mut group_c = sub.recv_group().await.unwrap().unwrap();
3206		let mut frames = 0;
3207		while let Some(frame) = group_c.read_frame().await.unwrap() {
3208			assert_eq!(frame.payload.len(), 5);
3209			frames += 1;
3210		}
3211		assert_eq!(frames, 2);
3212		settle().await;
3213
3214		let report = registry.report();
3215		let entry = report
3216			.traffic
3217			.iter()
3218			.find(|e| e.path.as_str() == "demo")
3219			.expect("demo tracked");
3220		let path_len = "demo".len() as u64;
3221
3222		// Egress (publisher side): reads out of the model.
3223		let egress = &entry.publisher;
3224		assert_eq!(egress.announced, 1, "one egress announce");
3225		assert_eq!(egress.announced_bytes, path_len);
3226		assert_eq!(egress.subscriptions, 1, "one egress subscription");
3227		assert_eq!(egress.broadcasts, 1, "one viewer");
3228		assert_eq!(egress.groups, 1);
3229		assert_eq!(egress.frames, 2);
3230		assert_eq!(egress.bytes, 10);
3231		assert_eq!(egress.fetches, 0);
3232
3233		// Ingress (subscriber side): writes into the model.
3234		let ingress = &entry.subscriber;
3235		assert_eq!(ingress.announced, 1, "one ingress announce");
3236		assert_eq!(ingress.announced_bytes, path_len);
3237		assert_eq!(ingress.subscriptions, 1, "one ingress track");
3238		assert_eq!(ingress.broadcasts, 0, "ingress has no viewer refcount");
3239		assert_eq!(ingress.groups, 1);
3240		assert_eq!(ingress.frames, 2);
3241		assert_eq!(ingress.bytes, 10);
3242
3243		// A fetch bumps only `fetches` on the egress side, plus the delivered group.
3244		let fetched = broadcast.track("video").unwrap().fetch_group(0, None).await.unwrap();
3245		let _ = fetched;
3246		settle().await;
3247		let report = registry.report();
3248		let entry = report.traffic.iter().find(|e| e.path.as_str() == "demo").unwrap();
3249		assert_eq!(entry.publisher.fetches, 1, "one fetch");
3250		assert_eq!(entry.publisher.subscriptions, 1, "fetch does not bump subscriptions");
3251		assert_eq!(entry.publisher.broadcasts, 1, "fetch does not bump the viewer refcount");
3252		// `fetches` is egress-only for the same structural reason as `broadcasts`:
3253		// only a `track::Consumer` can fetch, and the ingress scope never reaches one
3254		// (`broadcast::Producer::consume` hands out an untagged consumer).
3255		assert_eq!(entry.subscriber.fetches, 0, "ingress cannot fetch");
3256	}
3257
3258	/// `Subscriber::read_frame` collapses a group to its first frame. The paths it
3259	/// delegates to (plain and spliced) build their own *unmetered* group consumers,
3260	/// so the wrapper is the only place that can attribute the read: exactly one
3261	/// group, one frame, and the payload bytes, counted once each.
3262	#[tokio::test]
3263	async fn test_stats_read_frame_counts_once() {
3264		use crate::Timestamp;
3265		use crate::stats::{Config, Registry, Tier};
3266		use bytes::Bytes;
3267
3268		tokio::time::pause();
3269
3270		let registry = Registry::new(Config::new());
3271		let ctx = registry.tier(Tier::default()).session("acme");
3272
3273		let origin = Origin::random().produce();
3274		let ingress = origin.clone().with_stats(ctx.clone());
3275		let egress = origin.consume().with_stats(ctx.clone());
3276
3277		let mut announced = egress.announced();
3278		let source = ingress.create_broadcast("demo", announce()).unwrap();
3279		let mut dynamic = source.dynamic();
3280		settle().await;
3281		settle().await;
3282
3283		let broadcast = announced.next().await.unwrap().broadcast.unwrap();
3284		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3285		let mut producer = accept_track(&mut dynamic, "video").await;
3286		settle().await;
3287		let mut sub = subscribing.await.unwrap();
3288
3289		// A single-frame group, read back through the collapsing helper.
3290		producer
3291			.write_frame(Timestamp::ZERO, Bytes::from_static(b"hello"))
3292			.unwrap();
3293
3294		let frame = sub.read_frame().await.unwrap().expect("frame");
3295		assert_eq!(frame.payload.len(), 5);
3296		settle().await;
3297
3298		let report = registry.report();
3299		let entry = report
3300			.traffic
3301			.iter()
3302			.find(|e| e.path.as_str() == "demo")
3303			.expect("demo tracked");
3304		assert_eq!(entry.publisher.groups, 1, "one group, counted once");
3305		assert_eq!(entry.publisher.frames, 1, "one frame, counted once");
3306		assert_eq!(
3307			entry.publisher.bytes, 5,
3308			"payload counted once, not zero and not doubled"
3309		);
3310	}
3311
3312	/// Datagrams bypass the group/frame handles entirely, so they're metered at the
3313	/// producer (ingress write) and the subscriber (egress read). Each one counts as
3314	/// the single-frame group it stands in for, plus the `datagrams` breakout.
3315	#[tokio::test]
3316	async fn test_stats_datagrams_counted_both_sides() {
3317		use crate::Timestamp;
3318		use crate::stats::{Config, Registry, Tier};
3319
3320		tokio::time::pause();
3321
3322		let registry = Registry::new(Config::new());
3323		let ctx = registry.tier(Tier::default()).session("acme");
3324
3325		let origin = Origin::random().produce();
3326		let ingress = origin.clone().with_stats(ctx.clone());
3327		let egress = origin.consume().with_stats(ctx.clone());
3328
3329		let mut announced = egress.announced();
3330		let source = ingress.create_broadcast("demo", announce()).unwrap();
3331		let mut dynamic = source.dynamic();
3332		settle().await;
3333		settle().await;
3334
3335		let broadcast = announced.next().await.unwrap().broadcast.unwrap();
3336		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3337		let mut producer = accept_track(&mut dynamic, "video").await;
3338		settle().await;
3339		let mut sub = subscribing.await.unwrap();
3340
3341		producer.append_datagram(Timestamp::ZERO, &b"hello"[..]).unwrap();
3342		let datagram = sub.recv_datagram().await.unwrap().expect("datagram");
3343		assert_eq!(&datagram.payload[..], b"hello");
3344		settle().await;
3345
3346		let report = registry.report();
3347		let entry = report
3348			.traffic
3349			.iter()
3350			.find(|e| e.path.as_str() == "demo")
3351			.expect("demo tracked");
3352
3353		for (side, traffic) in [("egress", &entry.publisher), ("ingress", &entry.subscriber)] {
3354			assert_eq!(traffic.datagrams, 1, "{side}: one datagram");
3355			assert_eq!(traffic.groups, 1, "{side}: counted as its single-frame group");
3356			assert_eq!(traffic.frames, 1, "{side}: one frame");
3357			assert_eq!(traffic.bytes, 5, "{side}: payload counted once");
3358		}
3359	}
3360
3361	#[test]
3362	fn origin_rejects_reserved_ids() {
3363		assert!(Origin::new(0).is_err());
3364		assert!(Origin::new(1u64 << 62).is_err());
3365		assert_eq!(Origin::new(1).unwrap().id(), 1);
3366
3367		let mut zero = [0u8].as_slice();
3368		assert_eq!(
3369			Origin::decode(&mut zero, crate::lite::Version::Lite05).unwrap(),
3370			Origin::UNKNOWN
3371		);
3372	}
3373
3374	#[test]
3375	fn origin_list_push_fails_at_limit() {
3376		let mut list = OriginList::new();
3377		for _ in 0..MAX_HOPS {
3378			list.push(Origin::random()).unwrap();
3379		}
3380		assert_eq!(list.len(), MAX_HOPS);
3381		assert_eq!(list.push(Origin::random()), Err(TooManyOrigins));
3382	}
3383
3384	#[test]
3385	fn origin_list_replace_first() {
3386		let mut list = OriginList::new();
3387		for _ in 0..3 {
3388			list.push(Origin::UNKNOWN).unwrap();
3389		}
3390
3391		// Rewrites only the first placeholder, keeping the length the same.
3392		assert!(list.replace_first(Origin::UNKNOWN, Origin::new(7).unwrap()));
3393		assert_eq!(
3394			list.as_slice(),
3395			&[Origin::new(7).unwrap(), Origin::UNKNOWN, Origin::UNKNOWN]
3396		);
3397
3398		// No match leaves the list untouched.
3399		assert!(!list.replace_first(Origin::new(99).unwrap(), Origin::new(8).unwrap()));
3400		assert_eq!(list.len(), 3);
3401	}
3402
3403	#[test]
3404	fn origin_list_try_from_vec_enforces_limit() {
3405		let under: Vec<Origin> = (0..MAX_HOPS).map(|_| Origin::random()).collect();
3406		assert!(OriginList::try_from(under).is_ok());
3407
3408		let over: Vec<Origin> = (0..MAX_HOPS + 1).map(|_| Origin::random()).collect();
3409		assert_eq!(OriginList::try_from(over), Err(TooManyOrigins));
3410	}
3411
3412	#[tokio::test]
3413	async fn test_announce() {
3414		tokio::time::pause();
3415
3416		let origin = Origin::random().produce();
3417
3418		let mut consumer1 = origin.consume().announced();
3419		consumer1.assert_next_wait();
3420
3421		// Publish the first broadcast; it becomes visible asynchronously.
3422		let mut broadcast1 = origin.create_broadcast("test1", announce()).unwrap();
3423		settle().await;
3424
3425		consumer1.assert_next_some("test1");
3426		consumer1.assert_next_wait();
3427
3428		// Make a new consumer that should get the existing broadcast.
3429		// But we don't consume it yet.
3430		let mut consumer2 = origin.consume().announced();
3431
3432		// Publish the second broadcast.
3433		let mut broadcast2 = origin.create_broadcast("test2", announce()).unwrap();
3434		settle().await;
3435
3436		consumer1.assert_next_some("test2");
3437		consumer1.assert_next_wait();
3438
3439		consumer2.assert_next_some("test1");
3440		consumer2.assert_next_some("test2");
3441		consumer2.assert_next_wait();
3442
3443		// Finish the first broadcast: a graceful end unannounces immediately.
3444		broadcast1.finish();
3445		settle().await;
3446
3447		// All consumers should get a None now.
3448		consumer1.assert_next_none("test1");
3449		consumer2.assert_next_none("test1");
3450		consumer1.assert_next_wait();
3451		consumer2.assert_next_wait();
3452
3453		// And a new consumer only gets the last broadcast.
3454		let mut consumer3 = origin.consume().announced();
3455		consumer3.assert_next_some("test2");
3456		consumer3.assert_next_wait();
3457
3458		broadcast2.finish();
3459		settle().await;
3460
3461		consumer1.assert_next_none("test2");
3462		consumer2.assert_next_none("test2");
3463		consumer3.assert_next_none("test2");
3464	}
3465
3466	/// Multiple sources created at one path feed a single origin-owned broadcast:
3467	/// one announce, no churn as sources come and go, and an unannounce only when
3468	/// the last source leaves.
3469	#[tokio::test]
3470	async fn test_duplicate() {
3471		tokio::time::pause();
3472
3473		let origin = Origin::random().produce();
3474		let consumer = origin.consume();
3475		let mut announced = consumer.announced();
3476
3477		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
3478		let mut broadcast2 = origin.create_broadcast("test", announce()).unwrap();
3479		let mut broadcast3 = origin.create_broadcast("test", announce()).unwrap();
3480		settle().await;
3481		assert!(consumer.get_broadcast("test").is_some());
3482
3483		announced.assert_next_some("test");
3484		announced.assert_next_wait();
3485
3486		// A standby source finishing changes nothing.
3487		broadcast2.finish();
3488		settle().await;
3489		assert!(consumer.get_broadcast("test").is_some());
3490		announced.assert_next_wait();
3491
3492		// The active source finishing hands over to a survivor, invisibly.
3493		broadcast1.finish();
3494		settle().await;
3495		assert!(consumer.get_broadcast("test").is_some());
3496		announced.assert_next_wait();
3497
3498		// The last source finishing unannounces and removes the broadcast.
3499		broadcast3.finish();
3500		settle().await;
3501		assert!(consumer.get_broadcast("test").is_none());
3502
3503		announced.assert_next_none("test");
3504		announced.assert_next_wait();
3505	}
3506
3507	/// A source dying mid-serve fails over: the track re-splices from the standby
3508	/// source and resumes exactly at the first missing group.
3509	#[tokio::test]
3510	async fn test_route_failover() {
3511		tokio::time::pause();
3512
3513		let origin = Origin::random().produce();
3514		let consumer = origin.consume();
3515		let mut announced = consumer.announced();
3516
3517		// Both routes share the first hop (the original publisher): only
3518		// interchangeable content may join as a standby.
3519		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3520		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
3521
3522		// The first source announces the broadcast.
3523		let source_a = origin.create_broadcast("test", announce().with_hops(hops_a)).unwrap();
3524		let mut dynamic_a = source_a.dynamic();
3525		settle().await;
3526		settle().await;
3527		let broadcast = consumer.request_broadcast("test").await.unwrap();
3528		announced.assert_next_some("test");
3529
3530		// A second (longer) source joins silently as a standby.
3531		let source_b = origin.create_broadcast("test", announce().with_hops(hops_b)).unwrap();
3532		let mut dynamic_b = source_b.dynamic();
3533		settle().await;
3534		settle().await;
3535		announced.assert_next_wait();
3536
3537		// Subscribing dispatches the track to the best source (A).
3538		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3539		let mut producer = accept_track(&mut dynamic_a, "video").await;
3540		settle().await;
3541		dynamic_b.assert_no_request();
3542
3543		let mut sub = subscribing.await.unwrap();
3544		// Demand registers as the subscriber polls; a fresh segment carries no
3545		// boundary, so the demand is the subscriber's own.
3546		sub.assert_no_group();
3547		assert_eq!(producer.subscription().unwrap().group_start, None);
3548
3549		producer.append_group().unwrap();
3550		producer.append_group().unwrap();
3551		assert_eq!(sub.assert_group().sequence, 0);
3552		assert_eq!(sub.assert_group().sequence, 1);
3553
3554		// Source A dies (session loss): the track re-splices from B and nothing
3555		// is announced.
3556		// abort() consumes the producer, so this both aborts and drops it.
3557		producer.abort(Error::Dropped).unwrap();
3558		source_a.abort(Error::Dropped).unwrap();
3559		drop(dynamic_a);
3560		settle().await;
3561		announced.assert_next_wait();
3562
3563		// The new copy resumes one past the spliced groups: its demand starts at
3564		// the boundary, and groups the old source already delivered are filtered.
3565		let mut producer = accept_track(&mut dynamic_b, "video").await;
3566		settle().await;
3567		sub.assert_no_group();
3568		assert_eq!(producer.subscription().unwrap().group_start, Some(2));
3569		producer.create_group(group::Info { sequence: 1 }).unwrap();
3570		producer.create_group(group::Info { sequence: 2 }).unwrap();
3571		assert_eq!(sub.assert_group().sequence, 2, "groups below the boundary are filtered");
3572		sub.assert_not_closed();
3573	}
3574
3575	/// `route_changed` yields the current route first, then each change; equal
3576	/// updates coalesce, and the watch errors once every producer is gone.
3577	#[tokio::test]
3578	async fn test_broadcast_route_watch() {
3579		let mut producer = broadcast::Info::new().produce();
3580		let mut consumer = producer.consume();
3581
3582		// Initial value: the default route.
3583		assert_eq!(consumer.route_changed().await.unwrap(), broadcast::Route::default());
3584
3585		// An equal update is a no-op.
3586		producer.set_route(broadcast::Route::default()).unwrap();
3587		assert!(consumer.route_changed().now_or_never().is_none());
3588
3589		let mut hops = OriginList::new();
3590		hops.push(Origin::new(7).unwrap()).unwrap();
3591		let route = broadcast::Route::new().with_hops(hops).with_cost(3);
3592		producer.set_route(route.clone()).unwrap();
3593		assert_eq!(consumer.route_changed().await.unwrap(), route);
3594
3595		// A fresh consumer sees the current value immediately.
3596		let mut fresh = producer.consume();
3597		assert_eq!(fresh.route_changed().await.unwrap(), route);
3598
3599		drop(producer);
3600		assert!(matches!(consumer.route_changed().await.unwrap_err(), Error::Dropped));
3601	}
3602
3603	/// A cost update that flips the winning source hands live tracks over at a
3604	/// group boundary and re-advertises the broadcast's route, without announce
3605	/// churn.
3606	#[tokio::test]
3607	async fn test_route_cost_update() {
3608		tokio::time::pause();
3609
3610		// The takeover happens while a subscriber is live (carrying), so the local
3611		// origin must win the handover key comparison against B's announcing hop
3612		// (origin 3); a random id would flake on the hash.
3613		let origin = Info::new(origin_keyed("test", Origin::new(3).unwrap(), true)).produce();
3614		let consumer = origin.consume();
3615		let mut announced = consumer.announced();
3616
3617		// Both routes share the first hop (the original publisher): only
3618		// interchangeable content may join as a standby.
3619		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3620		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
3621
3622		// A (shorter chain) wins at equal cost.
3623		let mut source_a = origin
3624			.create_broadcast("test", announce().with_hops(hops_a.clone()))
3625			.unwrap();
3626		let mut dynamic_a = source_a.dynamic();
3627		settle().await;
3628		let broadcast = consumer.request_broadcast("test").await.unwrap();
3629		announced.assert_next_some("test");
3630
3631		let mut watch = broadcast.clone();
3632		assert_eq!(watch.route_changed().await.unwrap().hops, hops_a);
3633
3634		let mut source_b = origin
3635			.create_broadcast("test", announce().with_hops(hops_b.clone()))
3636			.unwrap();
3637		let mut dynamic_b = source_b.dynamic();
3638		settle().await;
3639		assert!(
3640			watch.route_changed().now_or_never().is_none(),
3641			"a losing standby must not change the advertised route"
3642		);
3643
3644		// Dispatch the track to A and deliver a group.
3645		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3646		let mut producer = accept_track(&mut dynamic_a, "video").await;
3647		settle().await;
3648		let mut sub = subscribing.await.unwrap();
3649		producer.append_group().unwrap();
3650		assert_eq!(sub.assert_group().sequence, 0);
3651
3652		// A's cost rises above B's: B takes over at the boundary and the
3653		// broadcast re-advertises B's route. No announce events.
3654		source_a
3655			.set_route(announce().with_hops(hops_a.clone()).with_cost(10))
3656			.unwrap();
3657		settle().await;
3658		assert_eq!(watch.route_changed().await.unwrap().hops, hops_b);
3659		announced.assert_next_wait();
3660
3661		let mut producer_b = accept_track(&mut dynamic_b, "video").await;
3662		settle().await;
3663		// Demand registers as the subscriber polls; the new segment starts at the
3664		// splice boundary.
3665		sub.assert_no_group();
3666		assert_eq!(producer_b.subscription().unwrap().group_start, Some(1));
3667		producer_b.create_group(group::Info { sequence: 1 }).unwrap();
3668		assert_eq!(sub.assert_group().sequence, 1);
3669		sub.assert_not_closed();
3670
3671		// The active source updating its own metadata re-advertises in place.
3672		source_b
3673			.set_route(announce().with_hops(hops_b.clone()).with_cost(5))
3674			.unwrap();
3675		settle().await;
3676		let advertised = watch.route_changed().await.unwrap();
3677		assert_eq!(advertised.hops, hops_b);
3678		assert_eq!(advertised.cost, 5);
3679		announced.assert_next_wait();
3680	}
3681
3682	/// A track completed for good must survive later source churn: it is never
3683	/// re-dispatched, and late subscribers still see a clean end.
3684	#[tokio::test]
3685	async fn test_completed_track_survives_route_churn() {
3686		tokio::time::pause();
3687
3688		let origin = Origin::random().produce();
3689		let consumer = origin.consume();
3690
3691		// Shared first hop, so B is a standby rather than a parked replacement.
3692		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3693		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
3694
3695		let source_a = origin.create_broadcast("test", announce().with_hops(hops_a)).unwrap();
3696		let mut dynamic_a = source_a.dynamic();
3697		settle().await;
3698		let source_b = origin.create_broadcast("test", announce().with_hops(hops_b)).unwrap();
3699		let mut dynamic_b = source_b.dynamic();
3700		settle().await;
3701		settle().await;
3702		let broadcast = consumer.request_broadcast("test").await.unwrap();
3703
3704		// Serve the track via A and end it for good.
3705		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3706		let mut producer = accept_track(&mut dynamic_a, "video").await;
3707		settle().await;
3708		let mut sub = subscribing.await.unwrap();
3709		producer.append_group().unwrap();
3710		assert_eq!(sub.assert_group().sequence, 0);
3711		producer.finish().unwrap();
3712		drop(producer);
3713		settle().await;
3714		sub.assert_closed();
3715
3716		// A detaching must not re-dispatch the finished track to B.
3717		source_a.abort(Error::Dropped).unwrap();
3718		drop(dynamic_a);
3719		settle().await;
3720		dynamic_b.assert_no_request();
3721
3722		// A late subscriber sees the same clean end, not an abort.
3723		let mut late = broadcast.track("video").unwrap().subscribe(None).await.unwrap();
3724		late.assert_closed();
3725	}
3726
3727	/// A successful splice resets the retry budget: transient pre-splice failures
3728	/// spread over a track's lifetime never accumulate into an abort.
3729	#[tokio::test]
3730	async fn test_serve_resets_retry_budget() {
3731		tokio::time::pause();
3732
3733		let origin = Origin::random().produce();
3734		let consumer = origin.consume();
3735
3736		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3737		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
3738		let mut dynamic = source.dynamic();
3739		settle().await;
3740		settle().await;
3741		let broadcast = consumer.request_broadcast("test").await.unwrap();
3742
3743		// Queue the track; the subscription resolves once a serve finally sticks.
3744		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3745
3746		// Alternate a pre-splice failure with a successful splice, well past the
3747		// retry cap; the resets keep the track alive.
3748		for _ in 0..2 * MAX_TRACK_RETRIES {
3749			let request = tokio::time::timeout(std::time::Duration::from_secs(1), dynamic.requested_track())
3750				.await
3751				.expect("timed out waiting for a retry")
3752				.unwrap();
3753			request.reject(Error::NotFound);
3754			let producer = accept_track(&mut dynamic, "video").await;
3755			settle().await;
3756			drop(producer);
3757		}
3758
3759		let _producer = accept_track(&mut dynamic, "video").await;
3760		settle().await;
3761		let mut sub = subscribing.await.unwrap();
3762		sub.assert_not_closed();
3763	}
3764
3765	/// A better source attaching mid-subscription takes the track over at an
3766	/// explicit group boundary: the old copy's demand is capped, the new copy
3767	/// starts at the boundary, and the subscriber reads a seamless sequence.
3768	#[tokio::test]
3769	async fn test_route_handover() {
3770		tokio::time::pause();
3771
3772		let origin = Origin::random().produce();
3773		let consumer = origin.consume();
3774		let mut announced = consumer.announced();
3775
3776		// Shared first hop, so the short route joins as an interchangeable source.
3777		let hops_long = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
3778		let hops_short = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3779
3780		let source_a = origin
3781			.create_broadcast("test", announce().with_hops(hops_long))
3782			.unwrap();
3783		let mut dynamic_a = source_a.dynamic();
3784		settle().await;
3785		settle().await;
3786		let broadcast = consumer.request_broadcast("test").await.unwrap();
3787		announced.assert_next_some("test");
3788
3789		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3790		let mut producer_a = accept_track(&mut dynamic_a, "video").await;
3791		settle().await;
3792		let mut sub = subscribing.await.unwrap();
3793		producer_a.append_group().unwrap();
3794		producer_a.append_group().unwrap();
3795		assert_eq!(sub.assert_group().sequence, 0);
3796		assert_eq!(sub.assert_group().sequence, 1);
3797
3798		// A strictly shorter source attaches: the live track is handed over with
3799		// no announce churn.
3800		let source_b = origin
3801			.create_broadcast("test", announce().with_hops(hops_short))
3802			.unwrap();
3803		let mut dynamic_b = source_b.dynamic();
3804		settle().await;
3805		settle().await;
3806		announced.assert_next_wait();
3807
3808		let mut producer_b = accept_track(&mut dynamic_b, "video").await;
3809		settle().await;
3810
3811		// The old copy's demand is capped at the boundary; the new copy's starts
3812		// there. Both propagate as the subscriber polls.
3813		sub.assert_no_group();
3814		assert_eq!(producer_a.subscription().unwrap().group_end, Some(1));
3815		assert_eq!(producer_b.subscription().unwrap().group_start, Some(2));
3816
3817		// The old copy racing past its cap is filtered; the new copy serves on.
3818		producer_a.create_group(group::Info { sequence: 2 }).unwrap();
3819		producer_b.create_group(group::Info { sequence: 2 }).unwrap();
3820		producer_b.create_group(group::Info { sequence: 3 }).unwrap();
3821		assert_eq!(sub.assert_group().sequence, 2);
3822		assert_eq!(sub.assert_group().sequence, 3);
3823		sub.assert_no_group();
3824		sub.assert_not_closed();
3825	}
3826
3827	/// A graceful detach (deliberate unannounce) closes immediately: no linger, so
3828	/// the unannounce propagates promptly and a re-create is a fresh broadcast.
3829	#[tokio::test(start_paused = true)]
3830	async fn test_route_unannounce_immediate() {
3831		let origin = Origin::random().produce();
3832		let consumer = origin.consume();
3833		let mut announced = consumer.announced();
3834
3835		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3836		let mut source = origin
3837			.create_broadcast("test", announce().with_hops(hops.clone()))
3838			.unwrap();
3839		settle().await;
3840		let broadcast = consumer.request_broadcast("test").await.unwrap();
3841		announced.assert_next_some("test");
3842
3843		// The peer deliberately unannounced: no reconnect window, the broadcast is
3844		// gone as soon as the teardown task observes the close.
3845		source.finish();
3846		settle().await;
3847		announced.assert_next_none("test");
3848
3849		// A re-create at the same path is a brand-new broadcast.
3850		let _source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
3851		settle().await;
3852		let fresh = consumer.request_broadcast("test").await.unwrap();
3853		announced.assert_next_some("test");
3854		assert!(
3855			!fresh.is_clone(&broadcast),
3856			"re-create must not splice the old broadcast"
3857		);
3858	}
3859
3860	/// With the default zero linger, a dying source (a session drop, not a
3861	/// deliberate unannounce) closes the broadcast just as promptly as a graceful
3862	/// one: no reconnect window, the tracks abort, and a re-create is a fresh
3863	/// broadcast rather than a splice.
3864	#[tokio::test(start_paused = true)]
3865	async fn test_route_detach_immediate() {
3866		let origin = Origin::random().produce();
3867		let consumer = origin.consume();
3868		let mut announced = consumer.announced();
3869
3870		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3871		let source = origin
3872			.create_broadcast("test", announce().with_hops(hops.clone()))
3873			.unwrap();
3874		let mut dynamic = source.dynamic();
3875		settle().await;
3876		settle().await;
3877		let broadcast = consumer.request_broadcast("test").await.unwrap();
3878		announced.assert_next_some("test");
3879
3880		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3881		let producer = accept_track(&mut dynamic, "video").await;
3882		settle().await;
3883		let mut sub = subscribing.await.unwrap();
3884
3885		// The session dies without unannouncing.
3886		drop(producer);
3887		source.abort(Error::Dropped).unwrap();
3888		drop(dynamic);
3889
3890		settle().await;
3891		announced.assert_next_none("test");
3892		sub.assert_error();
3893
3894		// A reconnecting session gets a brand-new broadcast, not a splice into
3895		// the old one.
3896		let _source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
3897		settle().await;
3898		settle().await;
3899		let fresh = consumer.request_broadcast("test").await.unwrap();
3900		announced.assert_next_some("test");
3901		assert!(
3902			!fresh.is_clone(&broadcast),
3903			"re-create must not splice the old broadcast"
3904		);
3905	}
3906
3907	/// A track nobody reads keeps the source's copy for [`TRACK_IDLE_LINGER`], then
3908	/// releases it. Crucially, the release must not immediately re-splice: the same
3909	/// demand signal gates both directions, so an idle track settles instead of
3910	/// re-requesting the track (and its info) every linger.
3911	#[tokio::test(start_paused = true)]
3912	async fn test_idle_track_releases_without_respinning() {
3913		let origin = Info::new(Origin::random()).produce();
3914		let consumer = origin.consume();
3915
3916		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3917		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
3918		let mut dynamic = source.dynamic();
3919		settle().await;
3920		let broadcast = consumer.request_broadcast("test").await.unwrap();
3921
3922		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3923		let producer = accept_track(&mut dynamic, "video").await;
3924		settle().await;
3925		let sub = subscribing.await.unwrap();
3926
3927		// The reader leaves, but the copy stays warm inside the window so a viewer
3928		// coming back (or a follow-up fetch) reuses it.
3929		drop(sub);
3930		tokio::time::sleep(TRACK_IDLE_LINGER / 2).await;
3931		settle().await;
3932		assert!(
3933			producer.poll_unused(&kio::Waiter::noop()).is_pending(),
3934			"the copy must stay spliced inside the linger",
3935		);
3936
3937		// Past the window the segment is released, so the serving session sees its
3938		// copy go unused and can drop it (along with the track info).
3939		tokio::time::sleep(TRACK_IDLE_LINGER).await;
3940		settle().await;
3941		assert!(
3942			producer.poll_unused(&kio::Waiter::noop()).is_ready(),
3943			"an idle copy must be released after the linger",
3944		);
3945
3946		// The anti-spin property: the release must not re-arm the splice. Ungated,
3947		// the loop re-attaches the copy immediately and drops it again every linger,
3948		// re-requesting the track (and its info) from the session each time it dies.
3949		for _ in 0..3 {
3950			tokio::time::sleep(TRACK_IDLE_LINGER).await;
3951			settle().await;
3952			assert!(
3953				producer.poll_unused(&kio::Waiter::noop()).is_ready(),
3954				"an unread copy must stay released, not be re-spliced",
3955			);
3956		}
3957		assert!(
3958			dynamic.requested_track().now_or_never().is_none(),
3959			"an unread track must not be re-requested",
3960		);
3961		drop(producer);
3962
3963		// A returning reader re-splices: the origin asks the source for a fresh copy.
3964		let subscribing = broadcast.track("video").unwrap().subscribe(None);
3965		let mut producer = accept_track(&mut dynamic, "video").await;
3966		settle().await;
3967		let mut sub = subscribing.await.unwrap();
3968		producer.append_group().unwrap();
3969		assert_eq!(sub.assert_group().sequence, 0);
3970	}
3971
3972	/// Back-to-back fetches reuse the source's copy: only the first asks the source
3973	/// for the track, so a fetch-driven consumer (HLS pulling segment after segment)
3974	/// doesn't re-request the track, and its `TRACK_INFO`, for every group.
3975	#[tokio::test(start_paused = true)]
3976	async fn test_back_to_back_fetches_reuse_the_track() {
3977		let origin = Info::new(Origin::random()).produce();
3978		let consumer = origin.consume();
3979
3980		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
3981		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
3982		let mut dynamic = source.dynamic();
3983		settle().await;
3984		let broadcast = consumer.request_broadcast("test").await.unwrap();
3985
3986		// The first fetch has to ask the source for the track.
3987		let fetching = broadcast.track("video").unwrap().fetch_group(0, None);
3988		let mut producer = accept_track(&mut dynamic, "video").await;
3989		producer.append_group().unwrap().finish().unwrap();
3990		settle().await;
3991		let first = fetching.await.expect("first fetch");
3992		drop(first);
3993
3994		// A second fetch inside the linger reuses the copy already spliced in.
3995		settle().await;
3996		let fetching = broadcast.track("video").unwrap().fetch_group(0, None);
3997		settle().await;
3998		assert!(
3999			dynamic.requested_track().now_or_never().is_none(),
4000			"a fetch inside the linger must reuse the track, not re-request it",
4001		);
4002		drop(fetching.await.expect("second fetch"));
4003
4004		// Once the fetches stop, the copy is released like any other idle track.
4005		tokio::time::sleep(TRACK_IDLE_LINGER * 2).await;
4006		settle().await;
4007		assert!(
4008			producer.poll_unused(&kio::Waiter::noop()).is_ready(),
4009			"the copy must be released once the fetches stop",
4010		);
4011		drop(producer);
4012
4013		// And a later fetch re-requests it: `accept_track` times out if it doesn't.
4014		settle().await;
4015		let fetching = broadcast.track("video").unwrap().fetch_group(0, None);
4016		let mut producer = accept_track(&mut dynamic, "video").await;
4017		producer.append_group().unwrap().finish().unwrap();
4018		settle().await;
4019		fetching.await.expect("fetch after the linger");
4020	}
4021
4022	/// With a linger configured, an ungraceful source loss keeps the broadcast
4023	/// alive and announced; a source re-attaching within the window splices in and
4024	/// consumers resume at the group boundary, never observing the outage.
4025	#[tokio::test(start_paused = true)]
4026	async fn test_linger_reconnect_splices() {
4027		let origin = Info::new(Origin::random())
4028			.with_linger(Duration::from_secs(5))
4029			.produce();
4030		let consumer = origin.consume();
4031		let mut announced = consumer.announced();
4032
4033		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4034		let source = origin
4035			.create_broadcast("test", announce().with_hops(hops.clone()))
4036			.unwrap();
4037		let mut dynamic = source.dynamic();
4038		settle().await;
4039		settle().await;
4040		let broadcast = consumer.request_broadcast("test").await.unwrap();
4041		announced.assert_next_some("test");
4042
4043		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4044		let mut producer = accept_track(&mut dynamic, "video").await;
4045		settle().await;
4046		let mut sub = subscribing.await.unwrap();
4047
4048		producer.append_group().unwrap();
4049		producer.append_group().unwrap();
4050		assert_eq!(sub.assert_group().sequence, 0);
4051		assert_eq!(sub.assert_group().sequence, 1);
4052
4053		// The session dies without unannouncing: the broadcast enters the linger
4054		// window instead of closing.
4055		drop(producer);
4056		source.abort(Error::Dropped).unwrap();
4057		drop(dynamic);
4058		settle().await;
4059
4060		// No unannounce, no track error: the outage is invisible so far.
4061		announced.assert_next_wait();
4062		sub.assert_no_group();
4063		sub.assert_not_closed();
4064
4065		// A consumer arriving mid-outage still resolves the lingering broadcast.
4066		let during = consumer.request_broadcast("test").await.unwrap();
4067		assert!(during.is_clone(&broadcast), "the lingering broadcast still resolves");
4068
4069		// The session reconnects within the window: the new source splices into
4070		// the same broadcast.
4071		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4072		let mut dynamic = source.dynamic();
4073		settle().await;
4074		settle().await;
4075		announced.assert_next_wait();
4076		let again = consumer.request_broadcast("test").await.unwrap();
4077		assert!(again.is_clone(&broadcast), "the reconnect must splice, not replace");
4078
4079		// The pending track re-splices from the new source and resumes one past
4080		// the groups the old source already delivered. Demand registers as the
4081		// subscriber polls.
4082		let mut producer = accept_track(&mut dynamic, "video").await;
4083		settle().await;
4084		sub.assert_no_group();
4085		assert_eq!(producer.subscription().unwrap().group_start, Some(2));
4086		producer.create_group(group::Info { sequence: 2 }).unwrap();
4087		assert_eq!(sub.assert_group().sequence, 2);
4088		sub.assert_not_closed();
4089	}
4090
4091	/// The linger window expiring without a replacement closes the broadcast: the
4092	/// path unannounces, tracks abort, and a later re-create is a fresh broadcast.
4093	#[tokio::test(start_paused = true)]
4094	async fn test_linger_expiry_closes() {
4095		let origin = Info::new(Origin::random())
4096			.with_linger(Duration::from_secs(5))
4097			.produce();
4098		let consumer = origin.consume();
4099		let mut announced = consumer.announced();
4100
4101		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4102		let source = origin
4103			.create_broadcast("test", announce().with_hops(hops.clone()))
4104			.unwrap();
4105		let mut dynamic = source.dynamic();
4106		settle().await;
4107		settle().await;
4108		let broadcast = consumer.request_broadcast("test").await.unwrap();
4109		announced.assert_next_some("test");
4110
4111		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4112		let producer = accept_track(&mut dynamic, "video").await;
4113		settle().await;
4114		let mut sub = subscribing.await.unwrap();
4115
4116		drop(producer);
4117		source.abort(Error::Dropped).unwrap();
4118		drop(dynamic);
4119		settle().await;
4120		announced.assert_next_wait();
4121
4122		// Nobody comes back: the window expires and the broadcast closes.
4123		tokio::time::sleep(std::time::Duration::from_secs(6)).await;
4124		settle().await;
4125		announced.assert_next_none("test");
4126		sub.assert_error();
4127
4128		// A session reconnecting after the window gets a brand-new broadcast.
4129		let _source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4130		settle().await;
4131		settle().await;
4132		let fresh = consumer.request_broadcast("test").await.unwrap();
4133		announced.assert_next_some("test");
4134		assert!(
4135			!fresh.is_clone(&broadcast),
4136			"a late re-create must not splice the expired broadcast"
4137		);
4138	}
4139
4140	/// A linger too large to represent as a deadline never expires: the broadcast
4141	/// outlives an arbitrarily long outage (a reconnect loop with no give-up
4142	/// timeout promises to retry forever).
4143	#[tokio::test(start_paused = true)]
4144	async fn test_linger_forever() {
4145		let origin = Info::new(Origin::random()).with_linger(Duration::MAX).produce();
4146		let consumer = origin.consume();
4147		let mut announced = consumer.announced();
4148
4149		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4150		let source = origin
4151			.create_broadcast("test", announce().with_hops(hops.clone()))
4152			.unwrap();
4153		settle().await;
4154		let broadcast = consumer.request_broadcast("test").await.unwrap();
4155		announced.assert_next_some("test");
4156
4157		source.abort(Error::Dropped).unwrap();
4158		settle().await;
4159
4160		// Days later the broadcast is still announced and still splices a reconnect.
4161		tokio::time::sleep(std::time::Duration::from_secs(60 * 60 * 24 * 3)).await;
4162		announced.assert_next_wait();
4163		let source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4164		settle().await;
4165		settle().await;
4166		let again = consumer.request_broadcast("test").await.unwrap();
4167		assert!(again.is_clone(&broadcast), "the reconnect must splice, not replace");
4168		drop(source);
4169	}
4170
4171	/// A deliberate finish never lingers, even with a linger configured: a clean
4172	/// unannounce from a peer must propagate immediately.
4173	#[tokio::test(start_paused = true)]
4174	async fn test_linger_skipped_on_finish() {
4175		let origin = Info::new(Origin::random())
4176			.with_linger(Duration::from_secs(5))
4177			.produce();
4178		let consumer = origin.consume();
4179		let mut announced = consumer.announced();
4180
4181		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4182		let mut source = origin
4183			.create_broadcast("test", announce().with_hops(hops.clone()))
4184			.unwrap();
4185		settle().await;
4186		let broadcast = consumer.request_broadcast("test").await.unwrap();
4187		announced.assert_next_some("test");
4188
4189		// A clean unannounce: the broadcast is gone as soon as the teardown task
4190		// observes the close, with no reconnect window.
4191		source.finish();
4192		settle().await;
4193		announced.assert_next_none("test");
4194
4195		// A re-create at the same path is a brand-new broadcast.
4196		let _source = origin.create_broadcast("test", announce().with_hops(hops)).unwrap();
4197		settle().await;
4198		let fresh = consumer.request_broadcast("test").await.unwrap();
4199		announced.assert_next_some("test");
4200		assert!(
4201			!fresh.is_clone(&broadcast),
4202			"a finish must not leave a lingering broadcast to splice into"
4203		);
4204	}
4205
4206	/// A non-live broadcast is reachable by exact path but never announced;
4207	/// toggling `live` announces and unannounces without touching the broadcast.
4208	#[tokio::test]
4209	async fn test_announce_toggle() {
4210		tokio::time::pause();
4211
4212		let origin = Origin::random().produce();
4213		let consumer = origin.consume();
4214		let mut announced = consumer.announced();
4215
4216		let mut source = origin.create_broadcast("test", broadcast::Route::new()).unwrap();
4217		settle().await;
4218
4219		// Routable but not announced.
4220		announced.assert_next_wait();
4221		let broadcast = consumer
4222			.get_broadcast("test")
4223			.expect("offline broadcast is still routable");
4224		assert!(!broadcast.route().announce);
4225
4226		// request_broadcast resolves the offline broadcast too.
4227		let requested = consumer.request_broadcast("test").await.unwrap();
4228		assert!(requested.is_clone(&broadcast));
4229
4230		// Going live announces.
4231		source.set_route(announce()).unwrap();
4232		settle().await;
4233		let face = announced.assert_next_some("test");
4234		assert!(face.is_clone(&broadcast));
4235
4236		// A fresh consumer replays only announced broadcasts.
4237		let mut fresh = origin.consume().announced();
4238		fresh.assert_next_some("test");
4239		fresh.assert_next_wait();
4240
4241		// Going offline unannounces but stays routable.
4242		source.set_route(broadcast::Route::new()).unwrap();
4243		settle().await;
4244		announced.assert_next_none("test");
4245		assert!(consumer.get_broadcast("test").is_some());
4246		let mut fresh = origin.consume().announced();
4247		fresh.assert_next_wait();
4248
4249		source.finish();
4250		settle().await;
4251		assert!(consumer.get_broadcast("test").is_none());
4252	}
4253
4254	/// An announced source outranks a cheaper offline one, so the broadcast
4255	/// stays announced and serves from it.
4256	#[tokio::test]
4257	async fn test_announce_beats_offline() {
4258		tokio::time::pause();
4259
4260		let origin = Origin::random().produce();
4261		let consumer = origin.consume();
4262		let mut announced = consumer.announced();
4263
4264		// An unannounced source with the best cost.
4265		let _offline = origin.create_broadcast("test", broadcast::Route::new()).unwrap();
4266		settle().await;
4267		announced.assert_next_wait();
4268
4269		// An announced source with a worse cost still wins: the path announces
4270		// and advertises its route.
4271		let mut announced_source = origin.create_broadcast("test", announce().with_cost(10)).unwrap();
4272		settle().await;
4273		announced.assert_next_some("test");
4274		let face = consumer.get_broadcast("test").unwrap();
4275		assert!(face.route().announce);
4276		assert_eq!(face.route().cost, 10);
4277
4278		// The announced source leaving falls back to the offline one: the path
4279		// unannounces but stays routable.
4280		announced_source.finish();
4281		settle().await;
4282		announced.assert_next_none("test");
4283		assert!(consumer.get_broadcast("test").is_some());
4284	}
4285
4286	/// A better source attaching does not churn announces: the broadcast identity
4287	/// is origin-owned, so the swap is invisible to consumers.
4288	#[tokio::test]
4289	async fn test_better_source_no_churn() {
4290		tokio::time::pause();
4291
4292		let origin = Origin::random().produce();
4293		let mut announced = origin.consume().announced();
4294
4295		// `a` carries two hops; `b` reaches the same publisher in one, so `b`
4296		// wins dispatch when it joins.
4297		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap(), Origin::new(3).unwrap()]).unwrap();
4298		let hops_b = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4299		let _a = origin.create_broadcast("test", announce().with_hops(hops_a)).unwrap();
4300		settle().await;
4301		let face = announced.assert_next_some("test");
4302
4303		let _b = origin
4304			.create_broadcast("test", announce().with_hops(hops_b.clone()))
4305			.unwrap();
4306		settle().await;
4307		announced.assert_next_wait();
4308		let current = origin.consume().get_broadcast("test").unwrap();
4309		assert!(current.is_clone(&face), "the broadcast identity must not change");
4310		// The face now advertises the winning (shorter) route.
4311		assert_eq!(current.route().hops, hops_b);
4312	}
4313
4314	/// A second source with a different original publisher (first hop) is new
4315	/// content, not a standby: it must not splice into the incumbent's
4316	/// subscribers. It takes the path over immediately, as a real unannounce +
4317	/// announce, rather than waiting out an incumbent whose session may only be
4318	/// alive because the transport has not timed it out yet.
4319	#[tokio::test]
4320	async fn test_publisher_mismatch_replaces() {
4321		tokio::time::pause();
4322
4323		let origin = Origin::random().produce();
4324		let consumer = origin.consume();
4325		let mut announced = consumer.announced();
4326
4327		let hops_a = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4328		let hops_b = OriginList::try_from(vec![Origin::new(2).unwrap()]).unwrap();
4329
4330		let mut source_a = origin
4331			.create_broadcast("test", announce().with_hops(hops_a.clone()))
4332			.unwrap();
4333		settle().await;
4334		let face_a = announced.assert_next_some("test");
4335
4336		// A different publisher at the same path: the newcomer wins it now, while
4337		// the incumbent is still attached and still believes it is publishing.
4338		let _source_b = origin
4339			.create_broadcast("test", announce().with_hops(hops_b.clone()))
4340			.unwrap();
4341		settle().await;
4342		settle().await;
4343		announced.assert_next_none("test");
4344		let face_b = announced.assert_next_some("test");
4345		assert!(!face_b.is_clone(&face_a), "a replacement, never a splice");
4346		assert_eq!(consumer.get_broadcast("test").unwrap().route().hops, hops_b);
4347		// The displaced front is torn down, not merely unpublished: leaving it
4348		// running would strand its subscribers and its source watchers on a face
4349		// nothing can reach.
4350		assert!(face_a.is_closed(), "the displaced front must close");
4351
4352		// The displaced incumbent ending is invisible: it no longer owns the path.
4353		source_a.finish();
4354		settle().await;
4355		settle().await;
4356		announced.assert_next_wait();
4357		assert_eq!(consumer.get_broadcast("test").unwrap().route().hops, hops_b);
4358	}
4359
4360	/// A publisher reconnecting under the same identity attaches as a second
4361	/// route with an identical hop chain and cost. The new session is the one
4362	/// actually carrying frames, so it must win selection immediately rather than
4363	/// waiting for the transport to retire the old one.
4364	#[tokio::test]
4365	async fn test_reconnect_wins_over_stale_route() {
4366		tokio::time::pause();
4367
4368		let origin = Origin::random().produce();
4369		let consumer = origin.consume();
4370
4371		let publisher = Origin::new(1).unwrap();
4372		let hops = OriginList::try_from(vec![publisher]).unwrap();
4373
4374		// The original session, still attached: its QUIC connection has not been
4375		// declared dead yet.
4376		let stale = origin
4377			.create_broadcast("test", announce().with_hops(hops.clone()))
4378			.unwrap();
4379		let mut stale_dynamic = stale.dynamic();
4380		settle().await;
4381
4382		// The same publisher reconnecting over a fresh session.
4383		let fresh = origin
4384			.create_broadcast("test", announce().with_hops(hops.clone()))
4385			.unwrap();
4386		let mut fresh_dynamic = fresh.dynamic();
4387		settle().await;
4388		settle().await;
4389
4390		// Track requests dispatch to the reconnect, not the corpse.
4391		let broadcast = consumer.request_broadcast("test").await.unwrap();
4392		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4393		settle().await;
4394		let _producer = accept_track(&mut fresh_dynamic, "video").await;
4395		settle().await;
4396		subscribing.await.unwrap();
4397		stale_dynamic.assert_no_request();
4398	}
4399
4400	/// The same reconnect, but arriving while a subscription is already spliced
4401	/// onto the stale route: the live track must re-splice onto the new session
4402	/// rather than ride the dead one until the transport gives up. The gate
4403	/// [`FrontState::reselect`] applies while carrying is pinned separately, in
4404	/// [`test_carrying_switches_to_benign_routes`].
4405	#[tokio::test]
4406	async fn test_carrying_reconnect_switches_immediately() {
4407		tokio::time::pause();
4408
4409		let origin = Origin::random().produce();
4410		let consumer = origin.consume();
4411
4412		let hops = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4413
4414		let stale = origin
4415			.create_broadcast("test", announce().with_hops(hops.clone()))
4416			.unwrap();
4417		let mut stale_dynamic = stale.dynamic();
4418		settle().await;
4419
4420		// A live subscription riding the original session.
4421		let broadcast = consumer.request_broadcast("test").await.unwrap();
4422		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4423		settle().await;
4424		let _stale_producer = accept_track(&mut stale_dynamic, "video").await;
4425		settle().await;
4426		// Held: the splice only follows the active route while the track is read.
4427		let _subscription = subscribing.await.unwrap();
4428
4429		// The reconnect arrives with the front already carrying.
4430		let fresh = origin
4431			.create_broadcast("test", announce().with_hops(hops.clone()))
4432			.unwrap();
4433		let mut fresh_dynamic = fresh.dynamic();
4434		settle().await;
4435		settle().await;
4436
4437		// The carrying front re-splices onto the reconnect rather than waiting for
4438		// the stale session to die.
4439		let _fresh_producer = accept_track(&mut fresh_dynamic, "video").await;
4440	}
4441
4442	/// Taking the path over is scoped to a newcomer that would actually outrank
4443	/// the incumbent. An offline source (a cache, or an on-demand handler) is
4444	/// ranked below every announced route, so arriving under a different
4445	/// publisher must not unannounce a live broadcast and cut its subscribers.
4446	///
4447	/// The tail of this test covers the park's *exit*: the parked source has to
4448	/// wake and attach once the incumbent ends. Nothing else drives that wait, so
4449	/// without this a lost wakeup would strand the source invisibly forever
4450	/// rather than failing anything.
4451	#[tokio::test]
4452	async fn test_offline_mismatch_never_evicts_a_live_front() {
4453		tokio::time::pause();
4454
4455		let origin = Origin::random().produce();
4456		let consumer = origin.consume();
4457		let mut announced = consumer.announced();
4458
4459		let hops_live = OriginList::try_from(vec![Origin::new(1).unwrap()]).unwrap();
4460		let hops_cache = OriginList::try_from(vec![Origin::new(2).unwrap()]).unwrap();
4461
4462		let mut live = origin
4463			.create_broadcast("test", announce().with_hops(hops_live.clone()))
4464			.unwrap();
4465		let mut live_dynamic = live.dynamic();
4466		settle().await;
4467		let face = announced.assert_next_some("test");
4468
4469		// A subscriber riding the live broadcast, which the eviction would cut.
4470		let broadcast = consumer.request_broadcast("test").await.unwrap();
4471		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4472		settle().await;
4473		let _producer = accept_track(&mut live_dynamic, "video").await;
4474		settle().await;
4475		subscribing.await.unwrap();
4476
4477		// An offline source for different content at the same path: it stays
4478		// invisible rather than displacing the live one.
4479		let cache = origin
4480			.create_broadcast("test", broadcast::Route::new().with_hops(hops_cache.clone()))
4481			.unwrap();
4482		settle().await;
4483		settle().await;
4484		announced.assert_next_wait();
4485		assert!(!face.is_closed(), "the live front must survive");
4486		assert_eq!(consumer.get_broadcast("test").unwrap().route().hops, hops_live);
4487
4488		// The incumbent ending hands the path to the parked source, which is the
4489		// only thing that ever wakes that wait.
4490		live.finish();
4491		settle().await;
4492		settle().await;
4493		announced.assert_next_none("test");
4494		let taken = consumer
4495			.get_broadcast("test")
4496			.expect("the parked source must take over");
4497		assert_eq!(taken.route().hops, hops_cache);
4498		// Offline, so it holds the path without being advertised.
4499		announced.assert_next_wait();
4500		drop(cache);
4501	}
4502
4503	/// A subscription from a peer the active chain flows through is served from
4504	/// the best clean source directly (data-plane split horizon), while every
4505	/// other consumer keeps the shared spliced broadcast fed by the active
4506	/// source. The two selections match what the announce loop advertises, so
4507	/// the data plane keeps the control plane's promise.
4508	#[tokio::test]
4509	async fn test_dispatch_excludes_requester() {
4510		tokio::time::pause();
4511
4512		let origin = Origin::random().produce();
4513		let consumer = origin.consume();
4514
4515		let peer = Origin::new(5).unwrap();
4516		let publisher = Origin::new(1).unwrap();
4517		// The route through the peer is cheaper, so it is the active source.
4518		let tainted = OriginList::try_from(vec![publisher, peer]).unwrap();
4519		let clean = OriginList::try_from(vec![publisher]).unwrap();
4520
4521		let source_a = origin.create_broadcast("test", announce().with_hops(tainted)).unwrap();
4522		let mut dynamic_a = source_a.dynamic();
4523		settle().await;
4524		let source_b = origin
4525			.create_broadcast("test", announce().with_hops(clean).with_cost(5))
4526			.unwrap();
4527		let mut dynamic_b = source_b.dynamic();
4528		settle().await;
4529		settle().await;
4530
4531		// An ordinary consumer rides the shared front, dispatched to the active
4532		// (peer-tainted) source.
4533		let shared = consumer.request_broadcast("test").await.unwrap();
4534		let subscribing = shared.track("video").unwrap().subscribe(None);
4535		let _producer_a = accept_track(&mut dynamic_a, "video").await;
4536		settle().await;
4537		subscribing.await.unwrap();
4538
4539		// The peer is pinned to the clean source. Crucially, nothing reaches the
4540		// via-peer source: a track request on it is what a session would forward
4541		// upstream as a SUBSCRIBE, and forwarding this one would send the peer's
4542		// own subscription back to them.
4543		let scoped = consumer.clone().excluding(peer);
4544		let pinned = scoped.request_broadcast("test").await.unwrap();
4545		let subscribing = pinned.track("video").unwrap().subscribe(None);
4546		let _producer_b = accept_track(&mut dynamic_b, "video").await;
4547		settle().await;
4548		subscribing.await.unwrap();
4549		dynamic_a.assert_no_request();
4550	}
4551
4552	/// A local standby with the same original publisher joining a front that is
4553	/// carrying the broadcast from a peer must splice the live subscription onto
4554	/// the new source, never tear it down (#2473, e2e finding 2). Redundant
4555	/// publishers sharing an origin id MUST produce the same tracks, so the
4556	/// splice resumes seamlessly at the group boundary.
4557	#[tokio::test]
4558	async fn test_standby_join_splices_live_subscriber() {
4559		tokio::time::pause();
4560
4561		let origin = Origin::random().produce();
4562		let consumer = origin.consume();
4563
4564		let publisher = Origin::new(1).unwrap();
4565		let peer = Origin::new(5).unwrap();
4566		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
4567		let local = OriginList::try_from(vec![publisher]).unwrap();
4568
4569		// Carrying via the peer, with a live subscriber mid-stream.
4570		let source_remote = origin
4571			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
4572			.unwrap();
4573		let mut dynamic_remote = source_remote.dynamic();
4574		settle().await;
4575		settle().await;
4576		let broadcast = consumer.request_broadcast("test").await.unwrap();
4577		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4578		let mut producer_remote = accept_track(&mut dynamic_remote, "video").await;
4579		settle().await;
4580		let mut sub = subscribing.await.unwrap();
4581		producer_remote.append_group().unwrap();
4582		assert_eq!(sub.assert_group().sequence, 0);
4583
4584		// The local standby joins with the same first hop and a cheaper route:
4585		// it wins dispatch and the live track re-splices at the boundary.
4586		let source_local = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
4587		let mut dynamic_local = source_local.dynamic();
4588		settle().await;
4589		let mut producer_local = accept_track(&mut dynamic_local, "video").await;
4590		settle().await;
4591		sub.assert_no_group();
4592		assert_eq!(producer_local.subscription().unwrap().group_start, Some(1));
4593		producer_local.create_group(group::Info { sequence: 1 }).unwrap();
4594		assert_eq!(sub.assert_group().sequence, 1);
4595		sub.assert_not_closed();
4596	}
4597
4598	/// The standby wins dispatch the instant it attaches, which is before a real
4599	/// publisher has created every track: a live `moq import` announces its
4600	/// broadcast on connect and only creates each track once its demuxer reaches
4601	/// it. Refusing one track must cost the incumbent nothing (#2473, e2e finding
4602	/// 2 in the field): the subscription keeps flowing from the route that has the
4603	/// track, and the standby is re-tried once it does.
4604	#[tokio::test]
4605	async fn test_standby_missing_track_keeps_incumbent() {
4606		tokio::time::pause();
4607
4608		let origin = Origin::random().produce();
4609		let consumer = origin.consume();
4610
4611		let publisher = Origin::new(1).unwrap();
4612		let peer = Origin::new(5).unwrap();
4613		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
4614		let local = OriginList::try_from(vec![publisher]).unwrap();
4615
4616		// Carrying via the peer, with a live subscriber mid-stream.
4617		let source_remote = origin
4618			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
4619			.unwrap();
4620		let mut dynamic_remote = source_remote.dynamic();
4621		settle().await;
4622		settle().await;
4623		let broadcast = consumer.request_broadcast("test").await.unwrap();
4624		let subscribing = broadcast.track("audio").unwrap().subscribe(None);
4625		let mut producer_remote = accept_track(&mut dynamic_remote, "audio").await;
4626		settle().await;
4627		let mut sub = subscribing.await.unwrap();
4628		producer_remote.append_group().unwrap();
4629		assert_eq!(sub.assert_group().sequence, 0);
4630
4631		// The standby joins and wins dispatch, but has not created "audio" yet.
4632		// Refuse it more times than the strike budget: the old code aborted the
4633		// logical track after three, killing a subscription the incumbent was
4634		// happily serving.
4635		let source_local = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
4636		let mut dynamic_local = source_local.dynamic();
4637		settle().await;
4638		for _ in 0..2 * MAX_TRACK_RETRIES {
4639			if let Some(Ok(request)) = dynamic_local.requested_track().now_or_never() {
4640				assert_eq!(request.name(), "audio");
4641				request.reject(Error::NotFound);
4642			}
4643			settle().await;
4644		}
4645
4646		// Still spliced to the incumbent, still delivering.
4647		producer_remote.append_group().unwrap();
4648		assert_eq!(sub.assert_group().sequence, 1);
4649		sub.assert_not_closed();
4650
4651		// The incumbent going away re-tries the standby, which by now has the
4652		// track: the subscription splices across instead of ending.
4653		source_remote.abort(Error::Dropped).unwrap();
4654		settle().await;
4655		settle().await;
4656		let mut producer_local = accept_track(&mut dynamic_local, "audio").await;
4657		settle().await;
4658		producer_local.create_group(group::Info { sequence: 2 }).unwrap();
4659		assert_eq!(sub.assert_group().sequence, 2);
4660		sub.assert_not_closed();
4661	}
4662
4663	/// A track every source refuses still aborts, but the verdict is not cached:
4664	/// it belongs to the sources attached at the time, so a later request re-asks
4665	/// them. Otherwise one early request for a track the publisher had not created
4666	/// yet leaves the name dead for the life of the front.
4667	#[tokio::test]
4668	async fn test_unservable_track_retried_by_a_later_request() {
4669		tokio::time::pause();
4670
4671		let origin = Origin::random().produce();
4672		let consumer = origin.consume();
4673
4674		let source = origin.create_broadcast("test", announce()).unwrap();
4675		let mut dynamic = source.dynamic();
4676		settle().await;
4677		settle().await;
4678		let broadcast = consumer.request_broadcast("test").await.unwrap();
4679
4680		// Nothing serves it: the strike budget runs out and the track aborts.
4681		let subscribing = broadcast.track("audio").unwrap().subscribe(None);
4682		for _ in 0..MAX_TRACK_RETRIES {
4683			let request = dynamic.requested_track().await.unwrap();
4684			request.reject(Error::NotFound);
4685			settle().await;
4686		}
4687		assert!(matches!(subscribing.await, Err(Error::Unroutable)));
4688
4689		// The publisher has the track now; a fresh request must reach it.
4690		let retry = broadcast.track("audio").unwrap().subscribe(None);
4691		let mut producer = accept_track(&mut dynamic, "audio").await;
4692		settle().await;
4693		let mut sub = retry.await.expect("a fresh request must reach the source");
4694		producer.append_group().unwrap();
4695		assert_eq!(sub.assert_group().sequence, 0);
4696	}
4697
4698	/// The front picks a source per track and re-picks on failover, so a route
4699	/// tainted for a peer is one the front may serve them from even when the
4700	/// active route is clean. Sharing the front therefore has to check the whole
4701	/// table, not just the active route: here the clean route is active but cannot
4702	/// carry the track, and the fallback is the peer's own route.
4703	#[tokio::test]
4704	async fn test_per_track_fallback_respects_exclusion() {
4705		tokio::time::pause();
4706
4707		let origin = Origin::random().produce();
4708		let consumer = origin.consume();
4709
4710		let publisher = Origin::new(1).unwrap();
4711		let peer = Origin::new(5).unwrap();
4712		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
4713		let local = OriginList::try_from(vec![publisher]).unwrap();
4714
4715		// The route through the peer has the track.
4716		let source_tainted = origin
4717			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
4718			.unwrap();
4719		let mut dynamic_tainted = source_tainted.dynamic();
4720		settle().await;
4721		settle().await;
4722
4723		// The clean route is cheaper, so it is active, but its publisher has not
4724		// created the track yet.
4725		let source_clean = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
4726		let mut dynamic_clean = source_clean.dynamic();
4727		settle().await;
4728
4729		let scoped = consumer.clone().excluding(peer);
4730		let broadcast = scoped.request_broadcast("test").await.unwrap();
4731		let _subscribing = broadcast.track("video").unwrap().subscribe(None);
4732		settle().await;
4733
4734		// The peer is pinned to the clean source, so the fallback the front would
4735		// take for everyone else is not reachable from their subscription.
4736		for _ in 0..2 * MAX_TRACK_RETRIES {
4737			if let Some(Ok(request)) = dynamic_clean.requested_track().now_or_never() {
4738				request.reject(Error::NotFound);
4739			}
4740			settle().await;
4741		}
4742		dynamic_tainted.assert_no_request();
4743	}
4744
4745	/// The same guarantee under failover: the peer resolves while a clean route is
4746	/// active, then that route dies. Its subscription must not migrate onto the
4747	/// route that flows back through it.
4748	#[tokio::test]
4749	async fn test_exclusion_survives_failover_onto_a_tainted_route() {
4750		tokio::time::pause();
4751
4752		let origin = Origin::random().produce();
4753		let consumer = origin.consume();
4754
4755		let publisher = Origin::new(1).unwrap();
4756		let peer = Origin::new(5).unwrap();
4757		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
4758		let local = OriginList::try_from(vec![publisher]).unwrap();
4759
4760		let source_tainted = origin
4761			.create_broadcast("test", announce().with_hops(via_peer).with_cost(2))
4762			.unwrap();
4763		let mut dynamic_tainted = source_tainted.dynamic();
4764		settle().await;
4765		settle().await;
4766		let source_clean = origin.create_broadcast("test", announce().with_hops(local)).unwrap();
4767		let mut dynamic_clean = source_clean.dynamic();
4768		settle().await;
4769
4770		let scoped = consumer.clone().excluding(peer);
4771		let broadcast = scoped.request_broadcast("test").await.unwrap();
4772		let _subscribing = broadcast.track("video").unwrap().subscribe(None);
4773		let _clean = accept_track(&mut dynamic_clean, "video").await;
4774		settle().await;
4775
4776		// The clean route dies, so the front's only remaining route is the peer's.
4777		source_clean.abort(Error::Dropped).unwrap();
4778		settle().await;
4779		settle().await;
4780		dynamic_tainted.assert_no_request();
4781
4782		// A fresh request now has nowhere clean to go, and says so rather than
4783		// silently serving the peer their own route.
4784		assert!(matches!(scoped.request_broadcast("test").await, Err(Error::Unroutable)));
4785	}
4786
4787	/// The resolve-time check only proves the table is clean for a peer at that
4788	/// instant. A route through them attaching *afterwards* must not be adopted
4789	/// underneath their live subscription: they hold the shared front, so the front
4790	/// stays off that route while a clean one remains.
4791	#[tokio::test]
4792	async fn test_exclusion_holds_when_a_tainted_route_attaches_later() {
4793		tokio::time::pause();
4794
4795		let origin = Origin::random().produce();
4796		let consumer = origin.consume();
4797
4798		let publisher = Origin::new(1).unwrap();
4799		let peer = Origin::new(5).unwrap();
4800		let local = OriginList::try_from(vec![publisher]).unwrap();
4801		let via_peer = OriginList::try_from(vec![publisher, peer]).unwrap();
4802
4803		// Only a clean route exists, so the peer legitimately gets the shared front.
4804		// Priced above the route that arrives later, so the front genuinely prefers
4805		// that one and staying put is the guard's doing, not the tie-break's.
4806		let source_clean = origin
4807			.create_broadcast("test", announce().with_hops(local).with_cost(5))
4808			.unwrap();
4809		let mut dynamic_clean = source_clean.dynamic();
4810		settle().await;
4811		settle().await;
4812		let scoped = consumer.clone().excluding(peer);
4813		let broadcast = scoped.request_broadcast("test").await.unwrap();
4814		let subscribing = broadcast.track("video").unwrap().subscribe(None);
4815		let mut producer_clean = accept_track(&mut dynamic_clean, "video").await;
4816		settle().await;
4817		let mut sub = subscribing.await.unwrap();
4818		producer_clean.append_group().unwrap();
4819		assert_eq!(sub.assert_group().sequence, 0);
4820
4821		// A cheaper route back through the peer attaches. Without the registration
4822		// the front would re-splice onto it and hand the peer its own bytes.
4823		// `advertised` non-zero says the announcing relay is not itself carrying, which
4824		// keeps the simultaneous-activation handover gate (whose key comparison is
4825		// hash-random) out of this test: the front takes the cheaper route outright
4826		// unless the exclusion stops it.
4827		let mut tainted = announce().with_hops(via_peer.clone()).with_cost(0);
4828		tainted.advertised = 1;
4829		let mut source_tainted = origin.create_broadcast("test", tainted).unwrap();
4830		let mut dynamic_tainted = source_tainted.dynamic();
4831		settle().await;
4832		settle().await;
4833		dynamic_tainted.assert_no_request();
4834		producer_clean.append_group().unwrap();
4835		assert_eq!(sub.assert_group().sequence, 1);
4836		sub.assert_not_closed();
4837
4838		// The registration ends with the last handle. The next table change is free
4839		// to take the cheaper route, which is what proves it was released.
4840		drop(sub);
4841		drop(broadcast);
4842		drop(scoped);
4843		settle().await;
4844		let mut bumped = announce().with_hops(via_peer).with_cost(1);
4845		bumped.advertised = 1;
4846		source_tainted.set_route(bumped).unwrap();
4847		settle().await;
4848		let plain = consumer.request_broadcast("test").await.unwrap();
4849		let _plain_track = plain.track("video").unwrap().subscribe(None);
4850		settle().await;
4851		settle().await;
4852		assert!(
4853			dynamic_tainted.requested_track().now_or_never().is_some(),
4854			"the front must be free to use the route again once the peer is gone"
4855		);
4856	}
4857
4858	/// An announced path every route of which loops through the requester is
4859	/// unroutable, not missing: the dynamic handler resolves paths with no route
4860	/// chain to check, so consulting it would route around the split horizon.
4861	#[tokio::test]
4862	async fn test_excluded_path_never_reaches_the_dynamic_handler() {
4863		tokio::time::pause();
4864
4865		let origin = Origin::random().produce();
4866		let consumer = origin.consume();
4867		let mut dynamic = origin.dynamic();
4868
4869		let peer = Origin::new(5).unwrap();
4870		let tainted = OriginList::try_from(vec![Origin::new(1).unwrap(), peer]).unwrap();
4871		let _source = origin.create_broadcast("test", announce().with_hops(tainted)).unwrap();
4872		settle().await;
4873		settle().await;
4874
4875		let scoped = consumer.clone().excluding(peer);
4876		assert!(matches!(scoped.request_broadcast("test").await, Err(Error::Unroutable)));
4877		assert!(
4878			dynamic.requested_broadcast().now_or_never().is_none(),
4879			"the dynamic handler was asked to route around the exclusion"
4880		);
4881
4882		// An unannounced path still reaches the handler, exclusion or not.
4883		let _pending = scoped.request_broadcast("other");
4884		settle().await;
4885		assert!(
4886			dynamic.requested_broadcast().now_or_never().is_some(),
4887			"a genuinely missing path must still fall back"
4888		);
4889	}
4890
4891	/// When every route flows through the requester, the path is unroutable for
4892	/// them: serving it would hand them their own bytes back.
4893	#[tokio::test]
4894	async fn test_dispatch_all_tainted_unroutable() {
4895		tokio::time::pause();
4896
4897		let origin = Origin::random().produce();
4898		let consumer = origin.consume();
4899
4900		let peer = Origin::new(5).unwrap();
4901		let tainted = OriginList::try_from(vec![Origin::new(1).unwrap(), peer]).unwrap();
4902		let _source = origin.create_broadcast("test", announce().with_hops(tainted)).unwrap();
4903		settle().await;
4904		settle().await;
4905
4906		let scoped = consumer.clone().excluding(peer);
4907		match scoped.request_broadcast("test").await {
4908			Err(Error::Unroutable) => {}
4909			Err(err) => panic!("expected Unroutable, got {err:?}"),
4910			Ok(_) => panic!("expected Unroutable, got a broadcast"),
4911		}
4912
4913		// Everyone else still resolves the shared front.
4914		consumer.request_broadcast("test").await.unwrap();
4915	}
4916
4917	#[tokio::test]
4918	async fn test_duplicate_reverse() {
4919		tokio::time::pause();
4920
4921		let origin = Origin::random().produce();
4922
4923		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
4924		let mut broadcast2 = origin.create_broadcast("test", announce()).unwrap();
4925		settle().await;
4926		assert!(origin.consume().get_broadcast("test").is_some());
4927
4928		// This is harder, finishing the newer source first.
4929		broadcast2.finish();
4930		settle().await;
4931		assert!(origin.consume().get_broadcast("test").is_some());
4932
4933		broadcast1.finish();
4934		settle().await;
4935		assert!(origin.consume().get_broadcast("test").is_none());
4936	}
4937
4938	#[tokio::test]
4939	async fn test_deterministic_tiebreak() {
4940		tokio::time::pause();
4941
4942		fn hops(ids: &[u64]) -> OriginList {
4943			OriginList::try_from(
4944				ids.iter()
4945					.copied()
4946					.map(|id| Origin::new(id).unwrap())
4947					.collect::<Vec<_>>(),
4948			)
4949			.unwrap()
4950		}
4951
4952		// Resolve the advertised route for "test" after creating both sources in
4953		// the given order.
4954		async fn winner(first: &[u64], second: &[u64]) -> OriginList {
4955			let origin = Origin::random().produce();
4956			let _a = origin
4957				.create_broadcast("test", announce().with_hops(hops(first)))
4958				.unwrap();
4959			let _b = origin
4960				.create_broadcast("test", announce().with_hops(hops(second)))
4961				.unwrap();
4962			settle().await;
4963			origin.consume().get_broadcast("test").unwrap().route().hops
4964		}
4965
4966		// Two routes with equal hop counts but distinct chains (sharing the first
4967		// hop, so both may attach). The winner is decided by the deterministic
4968		// key, not arrival order, so both publish orders converge.
4969		let forward = winner(&[5, 20], &[5, 40]).await;
4970		let reverse = winner(&[5, 40], &[5, 20]).await;
4971		assert_eq!(forward, reverse, "tie-break must not depend on publish order");
4972
4973		// A strictly shorter chain always wins regardless of the hash.
4974		assert_eq!(winner(&[5, 20], &[5]).await.len(), 1);
4975		assert_eq!(winner(&[5], &[5, 20]).await.len(), 1);
4976	}
4977
4978	// A previous mpsc-based implementation could only deliver the first 127 broadcasts
4979	// instantly via `assert_next` (which uses `now_or_never`). The kio-backed
4980	// implementation polls synchronously and can deliver all of them without yielding.
4981	// Names are zero-padded so lexicographic delivery order matches the loop index.
4982	#[tokio::test]
4983	async fn test_many_announces() {
4984		let origin = Origin::random().produce();
4985
4986		let mut consumer = origin.consume().announced();
4987		// Held for the duration: a dropped source unannounces immediately.
4988		let mut broadcasts = Vec::new();
4989		for i in 0..256 {
4990			broadcasts.push(origin.create_broadcast(format!("test{i:03}"), announce()).unwrap());
4991			settle().await;
4992		}
4993
4994		for i in 0..256 {
4995			consumer.assert_next_some(format!("test{i:03}"));
4996		}
4997		consumer.assert_next_wait();
4998	}
4999
5000	#[tokio::test]
5001	async fn test_many_announces_try() {
5002		let origin = Origin::random().produce();
5003
5004		let mut consumer = origin.consume().announced();
5005		// Held for the duration: a dropped source unannounces immediately.
5006		let mut broadcasts = Vec::new();
5007		for i in 0..256 {
5008			broadcasts.push(origin.create_broadcast(format!("test{i:03}"), announce()).unwrap());
5009			settle().await;
5010		}
5011
5012		for i in 0..256 {
5013			consumer.assert_try_next_some(format!("test{i:03}"));
5014		}
5015	}
5016
5017	#[tokio::test]
5018	async fn test_with_root_basic() {
5019		let origin = Origin::random().produce();
5020
5021		// Create a producer with root "/foo"
5022		let foo_producer = origin.with_root("foo").expect("should create root");
5023		assert_eq!(foo_producer.root().as_str(), "foo");
5024
5025		let mut consumer = origin.consume().announced();
5026
5027		// When publishing to "bar/baz", it should actually publish to "foo/bar/baz"
5028		let _broadcast = foo_producer
5029			.create_broadcast("bar/baz", announce())
5030			.expect("publish allowed");
5031		settle().await;
5032		// The original consumer should see the full path
5033		consumer.assert_next_some("foo/bar/baz");
5034
5035		// A consumer created from the rooted producer should see the stripped path
5036		let mut foo_consumer = foo_producer.consume().announced();
5037		foo_consumer.assert_next_some("bar/baz");
5038	}
5039
5040	#[tokio::test]
5041	async fn test_with_root_nested() {
5042		let origin = Origin::random().produce();
5043
5044		// Create nested roots
5045		let foo_producer = origin.with_root("foo").expect("should create foo root");
5046		let foo_bar_producer = foo_producer.with_root("bar").expect("should create bar root");
5047		assert_eq!(foo_bar_producer.root().as_str(), "foo/bar");
5048
5049		let mut consumer = origin.consume().announced();
5050
5051		// Publishing to "baz" should actually publish to "foo/bar/baz"
5052		let _broadcast = foo_bar_producer
5053			.create_broadcast("baz", announce())
5054			.expect("publish allowed");
5055		settle().await;
5056		// The original consumer sees the full path
5057		consumer.assert_next_some("foo/bar/baz");
5058
5059		// Consumer from foo_bar_producer sees just "baz"
5060		let mut foo_bar_consumer = foo_bar_producer.consume().announced();
5061		foo_bar_consumer.assert_next_some("baz");
5062	}
5063
5064	#[tokio::test]
5065	async fn test_publish_scope_allows() {
5066		let origin = Origin::random().produce();
5067
5068		// Create a producer that can only publish to "allowed" paths
5069		let limited_producer = origin
5070			.scope(&["allowed/path1".into(), "allowed/path2".into()])
5071			.expect("should create limited producer");
5072
5073		// Should be able to publish to allowed paths
5074		let _broadcast = limited_producer
5075			.create_broadcast("allowed/path1", announce())
5076			.expect("publish allowed");
5077		let _keep2 = limited_producer
5078			.create_broadcast("allowed/path1/nested", announce())
5079			.expect("publish allowed");
5080		let _keep3 = limited_producer
5081			.create_broadcast("allowed/path2", announce())
5082			.expect("publish allowed");
5083		settle().await;
5084
5085		// Should not be able to publish to disallowed paths
5086		assert!(limited_producer.create_broadcast("notallowed", announce()).is_err());
5087		assert!(limited_producer.create_broadcast("allowed", announce()).is_err()); // Parent of allowed path
5088		assert!(limited_producer.create_broadcast("other/path", announce()).is_err());
5089	}
5090
5091	#[tokio::test]
5092	async fn test_publish_max_parts() {
5093		let origin = Origin::random().produce();
5094
5095		let at_limit = (0..Path::MAX_PARTS)
5096			.map(|i| i.to_string())
5097			.collect::<Vec<_>>()
5098			.join("/");
5099		let _broadcast = origin
5100			.create_broadcast(at_limit.as_str(), announce())
5101			.expect("publish allowed");
5102		settle().await;
5103
5104		let too_deep = format!("{at_limit}/extra");
5105		assert!(origin.create_broadcast(too_deep.as_str(), announce()).is_err());
5106
5107		// The root counts toward the limit; a joined path past 32 parts is rejected.
5108		let rooted = origin.with_root("root").expect("wildcard allows any root");
5109		assert!(rooted.create_broadcast(at_limit.as_str(), announce()).is_err());
5110	}
5111
5112	#[tokio::test]
5113	async fn test_publish_scope_empty() {
5114		let origin = Origin::random().produce();
5115
5116		// Creating a producer with no allowed paths should return None
5117		assert!(origin.scope(&[]).is_none());
5118	}
5119
5120	#[tokio::test]
5121	async fn test_consume_scope_filters() {
5122		let origin = Origin::random().produce();
5123
5124		let mut consumer = origin.consume().announced();
5125
5126		// Publish to different paths
5127		let _broadcast1 = origin.create_broadcast("allowed", announce()).unwrap();
5128		let _broadcast2 = origin.create_broadcast("allowed/nested", announce()).unwrap();
5129		let _broadcast3 = origin.create_broadcast("notallowed", announce()).unwrap();
5130		settle().await;
5131
5132		// Create a consumer that only sees "allowed" paths
5133		let mut limited_consumer = origin
5134			.consume()
5135			.scope(&["allowed".into()])
5136			.expect("should create limited consumer")
5137			.announced();
5138
5139		// Should only receive broadcasts under "allowed"
5140		limited_consumer.assert_next_some("allowed");
5141		limited_consumer.assert_next_some("allowed/nested");
5142		limited_consumer.assert_next_wait(); // Should not see "notallowed"
5143
5144		// Unscoped consumer should see all
5145		consumer.assert_next_some("allowed");
5146		consumer.assert_next_some("allowed/nested");
5147		consumer.assert_next_some("notallowed");
5148	}
5149
5150	#[tokio::test]
5151	async fn test_consume_scope_multiple_prefixes() {
5152		let origin = Origin::random().produce();
5153
5154		let _broadcast1 = origin.create_broadcast("foo/test", announce()).unwrap();
5155		let _broadcast2 = origin.create_broadcast("bar/test", announce()).unwrap();
5156		let _broadcast3 = origin.create_broadcast("baz/test", announce()).unwrap();
5157		settle().await;
5158
5159		// Consumer that only sees "foo" and "bar" paths
5160		let mut limited_consumer = origin
5161			.consume()
5162			.scope(&["foo".into(), "bar".into()])
5163			.expect("should create limited consumer")
5164			.announced();
5165
5166		// Order depends on PathPrefixes canonical sort (lexicographic for same length)
5167		limited_consumer.assert_next_some("bar/test");
5168		limited_consumer.assert_next_some("foo/test");
5169		limited_consumer.assert_next_wait(); // Should not see "baz/test"
5170	}
5171
5172	#[tokio::test]
5173	async fn test_with_root_and_publish_scope() {
5174		let origin = Origin::random().produce();
5175
5176		// User connects to /foo root
5177		let foo_producer = origin.with_root("foo").expect("should create foo root");
5178
5179		// Limit them to publish only to "bar" and "goop/pee" within /foo
5180		let limited_producer = foo_producer
5181			.scope(&["bar".into(), "goop/pee".into()])
5182			.expect("should create limited producer");
5183
5184		let mut consumer = origin.consume().announced();
5185
5186		// Should be able to publish to foo/bar and foo/goop/pee (but user sees as bar and goop/pee)
5187		let _broadcast = limited_producer
5188			.create_broadcast("bar", announce())
5189			.expect("publish allowed");
5190		let _keep2 = limited_producer
5191			.create_broadcast("bar/nested", announce())
5192			.expect("publish allowed");
5193		let _keep3 = limited_producer
5194			.create_broadcast("goop/pee", announce())
5195			.expect("publish allowed");
5196		let _keep4 = limited_producer
5197			.create_broadcast("goop/pee/nested", announce())
5198			.expect("publish allowed");
5199		settle().await;
5200
5201		// Should not be able to publish outside allowed paths
5202		assert!(limited_producer.create_broadcast("baz", announce()).is_err());
5203		assert!(limited_producer.create_broadcast("goop", announce()).is_err()); // Parent of allowed
5204		assert!(limited_producer.create_broadcast("goop/other", announce()).is_err());
5205
5206		// Original consumer sees full paths
5207		consumer.assert_next_some("foo/bar");
5208		consumer.assert_next_some("foo/bar/nested");
5209		consumer.assert_next_some("foo/goop/pee");
5210		consumer.assert_next_some("foo/goop/pee/nested");
5211	}
5212
5213	#[tokio::test]
5214	async fn test_with_root_and_consume_scope() {
5215		let origin = Origin::random().produce();
5216
5217		// Publish broadcasts
5218		let _broadcast1 = origin.create_broadcast("foo/bar/test", announce()).unwrap();
5219		let _broadcast2 = origin.create_broadcast("foo/goop/pee/test", announce()).unwrap();
5220		let _broadcast3 = origin.create_broadcast("foo/other/test", announce()).unwrap();
5221		settle().await;
5222
5223		// User connects to /foo root
5224		let foo_producer = origin.with_root("foo").expect("should create foo root");
5225
5226		// Create consumer limited to "bar" and "goop/pee" within /foo
5227		let mut limited_consumer = foo_producer
5228			.consume()
5229			.scope(&["bar".into(), "goop/pee".into()])
5230			.expect("should create limited consumer")
5231			.announced();
5232
5233		// Should only see allowed paths (without foo prefix)
5234		limited_consumer.assert_next_some("bar/test");
5235		limited_consumer.assert_next_some("goop/pee/test");
5236		limited_consumer.assert_next_wait(); // Should not see "other/test"
5237	}
5238
5239	#[tokio::test]
5240	async fn test_with_root_unauthorized() {
5241		let origin = Origin::random().produce();
5242
5243		// First limit the producer to specific paths
5244		let limited_producer = origin
5245			.scope(&["allowed".into()])
5246			.expect("should create limited producer");
5247
5248		// Trying to create a root outside allowed paths should fail
5249		assert!(limited_producer.with_root("notallowed").is_none());
5250
5251		// But creating a root within allowed paths should work
5252		let allowed_root = limited_producer
5253			.with_root("allowed")
5254			.expect("should create allowed root");
5255		assert_eq!(allowed_root.root().as_str(), "allowed");
5256	}
5257
5258	#[tokio::test]
5259	async fn test_wildcard_permission() {
5260		let origin = Origin::random().produce();
5261
5262		// Producer with root access (empty string means wildcard)
5263		let root_producer = origin.clone();
5264
5265		// Should be able to publish anywhere
5266		let _broadcast = root_producer
5267			.create_broadcast("any/path", announce())
5268			.expect("publish allowed");
5269		let _keep2 = root_producer
5270			.create_broadcast("other/path", announce())
5271			.expect("publish allowed");
5272		settle().await;
5273
5274		// Can create any root
5275		let foo_producer = root_producer.with_root("foo").expect("should create any root");
5276		assert_eq!(foo_producer.root().as_str(), "foo");
5277	}
5278
5279	#[tokio::test]
5280	async fn test_consume_broadcast_with_permissions() {
5281		let origin = Origin::random().produce();
5282
5283		let _broadcast1 = origin.create_broadcast("allowed/test", announce()).unwrap();
5284		let _broadcast2 = origin.create_broadcast("notallowed/test", announce()).unwrap();
5285		settle().await;
5286
5287		// Create limited consumer
5288		let limited_consumer = origin
5289			.consume()
5290			.scope(&["allowed".into()])
5291			.expect("should create limited consumer");
5292
5293		// Should be able to get allowed broadcast
5294		let result = limited_consumer.get_broadcast("allowed/test");
5295		assert!(result.is_some());
5296		assert!(
5297			result
5298				.unwrap()
5299				.is_clone(&origin.consume().get_broadcast("allowed/test").unwrap())
5300		);
5301
5302		// Should not be able to get disallowed broadcast
5303		assert!(limited_consumer.get_broadcast("notallowed/test").is_none());
5304
5305		// Original consumer can get both
5306		let consumer = origin.consume();
5307		assert!(consumer.get_broadcast("allowed/test").is_some());
5308		assert!(consumer.get_broadcast("notallowed/test").is_some());
5309	}
5310
5311	#[tokio::test]
5312	async fn test_nested_paths_with_permissions() {
5313		let origin = Origin::random().produce();
5314
5315		// Create producer limited to "a/b/c"
5316		let limited_producer = origin.scope(&["a/b/c".into()]).expect("should create limited producer");
5317
5318		// Should be able to publish to exact path and nested paths
5319		let _broadcast = limited_producer
5320			.create_broadcast("a/b/c", announce())
5321			.expect("publish allowed");
5322		let _keep2 = limited_producer
5323			.create_broadcast("a/b/c/d", announce())
5324			.expect("publish allowed");
5325		let _keep3 = limited_producer
5326			.create_broadcast("a/b/c/d/e", announce())
5327			.expect("publish allowed");
5328		settle().await;
5329
5330		// Should not be able to publish to parent or sibling paths
5331		assert!(limited_producer.create_broadcast("a", announce()).is_err());
5332		assert!(limited_producer.create_broadcast("a/b", announce()).is_err());
5333		assert!(limited_producer.create_broadcast("a/b/other", announce()).is_err());
5334	}
5335
5336	#[tokio::test]
5337	async fn test_multiple_consumers_with_different_permissions() {
5338		let origin = Origin::random().produce();
5339
5340		// Publish to different paths
5341		let _broadcast1 = origin.create_broadcast("foo/test", announce()).unwrap();
5342		let _broadcast2 = origin.create_broadcast("bar/test", announce()).unwrap();
5343		let _broadcast3 = origin.create_broadcast("baz/test", announce()).unwrap();
5344		settle().await;
5345
5346		// Create consumers with different permissions
5347		let mut foo_consumer = origin
5348			.consume()
5349			.scope(&["foo".into()])
5350			.expect("should create foo consumer")
5351			.announced();
5352
5353		let mut bar_consumer = origin
5354			.consume()
5355			.scope(&["bar".into()])
5356			.expect("should create bar consumer")
5357			.announced();
5358
5359		let mut foobar_consumer = origin
5360			.consume()
5361			.scope(&["foo".into(), "bar".into()])
5362			.expect("should create foobar consumer")
5363			.announced();
5364
5365		// Each consumer should only see their allowed paths
5366		foo_consumer.assert_next_some("foo/test");
5367		foo_consumer.assert_next_wait();
5368
5369		bar_consumer.assert_next_some("bar/test");
5370		bar_consumer.assert_next_wait();
5371
5372		foobar_consumer.assert_next_some("bar/test");
5373		foobar_consumer.assert_next_some("foo/test");
5374		foobar_consumer.assert_next_wait();
5375	}
5376
5377	#[tokio::test]
5378	async fn test_select_with_empty_prefix() {
5379		let origin = Origin::random().produce();
5380
5381		// User with root "demo" allowed to subscribe to "worm-node" and "foobar"
5382		let demo_producer = origin.with_root("demo").expect("should create demo root");
5383		let limited_producer = demo_producer
5384			.scope(&["worm-node".into(), "foobar".into()])
5385			.expect("should create limited producer");
5386
5387		// Publish some broadcasts
5388		let _broadcast1 = limited_producer
5389			.create_broadcast("worm-node/test", announce())
5390			.expect("publish allowed");
5391		let _broadcast2 = limited_producer
5392			.create_broadcast("foobar/test", announce())
5393			.expect("publish allowed");
5394		settle().await;
5395
5396		// scope with empty prefix should keep the exact same "worm-node" and "foobar" nodes
5397		let mut consumer = limited_producer
5398			.consume()
5399			.scope(&["".into()])
5400			.expect("should create consumer with empty prefix")
5401			.announced();
5402
5403		// Should see both broadcasts (order depends on PathPrefixes sort)
5404		let a1 = consumer.try_next().expect("expected first announcement");
5405		let a2 = consumer.try_next().expect("expected second announcement");
5406		consumer.assert_next_wait();
5407
5408		let mut paths: Vec<_> = [&a1, &a2].iter().map(|a| a.path.to_string()).collect();
5409		paths.sort();
5410		assert_eq!(paths, ["foobar/test", "worm-node/test"]);
5411	}
5412
5413	#[tokio::test]
5414	async fn test_select_narrowing_scope() {
5415		let origin = Origin::random().produce();
5416
5417		// User with root "demo" allowed to subscribe to "worm-node" and "foobar"
5418		let demo_producer = origin.with_root("demo").expect("should create demo root");
5419		let limited_producer = demo_producer
5420			.scope(&["worm-node".into(), "foobar".into()])
5421			.expect("should create limited producer");
5422
5423		// Publish broadcasts at different levels
5424		let _broadcast1 = limited_producer
5425			.create_broadcast("worm-node", announce())
5426			.expect("publish allowed");
5427		let _broadcast2 = limited_producer
5428			.create_broadcast("worm-node/foo", announce())
5429			.expect("publish allowed");
5430		let _broadcast3 = limited_producer
5431			.create_broadcast("foobar/bar", announce())
5432			.expect("publish allowed");
5433		settle().await;
5434
5435		// Test 1: scope("worm-node") should result in a single "" node with contents of "worm-node" ONLY
5436		let mut worm_consumer = limited_producer
5437			.consume()
5438			.scope(&["worm-node".into()])
5439			.expect("should create worm-node consumer")
5440			.announced();
5441
5442		// Should see worm-node content with paths stripped to ""
5443		worm_consumer.assert_next_some("worm-node");
5444		worm_consumer.assert_next_some("worm-node/foo");
5445		worm_consumer.assert_next_wait(); // Should NOT see foobar content
5446
5447		// Test 2: scope("worm-node/foo") should result in a "" node with contents of "worm-node/foo"
5448		let mut foo_consumer = limited_producer
5449			.consume()
5450			.scope(&["worm-node/foo".into()])
5451			.expect("should create worm-node/foo consumer")
5452			.announced();
5453
5454		foo_consumer.assert_next_some("worm-node/foo");
5455		foo_consumer.assert_next_wait(); // Should NOT see other content
5456	}
5457
5458	#[tokio::test]
5459	async fn test_select_multiple_roots_with_empty_prefix() {
5460		let origin = Origin::random().produce();
5461
5462		// Producer with multiple allowed roots
5463		let limited_producer = origin
5464			.scope(&["app1".into(), "app2".into(), "shared".into()])
5465			.expect("should create limited producer");
5466
5467		// Publish to each root
5468		let _broadcast1 = limited_producer
5469			.create_broadcast("app1/data", announce())
5470			.expect("publish allowed");
5471		let _broadcast2 = limited_producer
5472			.create_broadcast("app2/config", announce())
5473			.expect("publish allowed");
5474		let _broadcast3 = limited_producer
5475			.create_broadcast("shared/resource", announce())
5476			.expect("publish allowed");
5477		settle().await;
5478
5479		// scope with empty prefix should maintain all roots
5480		let mut consumer = limited_producer
5481			.consume()
5482			.scope(&["".into()])
5483			.expect("should create consumer with empty prefix")
5484			.announced();
5485
5486		// Should see all broadcasts from all roots
5487		consumer.assert_next_some("app1/data");
5488		consumer.assert_next_some("app2/config");
5489		consumer.assert_next_some("shared/resource");
5490		consumer.assert_next_wait();
5491	}
5492
5493	#[tokio::test]
5494	async fn test_publish_scope_with_empty_prefix() {
5495		let origin = Origin::random().produce();
5496
5497		// Producer with specific allowed paths
5498		let limited_producer = origin
5499			.scope(&["services/api".into(), "services/web".into()])
5500			.expect("should create limited producer");
5501
5502		// scope with empty prefix should keep the same restrictions
5503		let same_producer = limited_producer
5504			.scope(&["".into()])
5505			.expect("should create producer with empty prefix");
5506
5507		// Should still have the same publishing restrictions
5508		let _broadcast = same_producer
5509			.create_broadcast("services/api", announce())
5510			.expect("publish allowed");
5511		let _keep2 = same_producer
5512			.create_broadcast("services/web", announce())
5513			.expect("publish allowed");
5514		assert!(same_producer.create_broadcast("services/db", announce()).is_err());
5515		assert!(same_producer.create_broadcast("other", announce()).is_err());
5516	}
5517
5518	#[tokio::test]
5519	async fn test_select_narrowing_to_deeper_path() {
5520		let origin = Origin::random().produce();
5521
5522		// Producer with broad permission
5523		let limited_producer = origin.scope(&["org".into()]).expect("should create limited producer");
5524
5525		// Publish at various depths
5526		let _broadcast1 = limited_producer
5527			.create_broadcast("org/team1/project1", announce())
5528			.expect("publish allowed");
5529		let _broadcast2 = limited_producer
5530			.create_broadcast("org/team1/project2", announce())
5531			.expect("publish allowed");
5532		let _broadcast3 = limited_producer
5533			.create_broadcast("org/team2/project1", announce())
5534			.expect("publish allowed");
5535		settle().await;
5536
5537		// Narrow down to team2 only
5538		let mut team2_consumer = limited_producer
5539			.consume()
5540			.scope(&["org/team2".into()])
5541			.expect("should create team2 consumer")
5542			.announced();
5543
5544		team2_consumer.assert_next_some("org/team2/project1");
5545		team2_consumer.assert_next_wait(); // Should NOT see team1 content
5546
5547		// Further narrow down to team1/project1
5548		let mut project1_consumer = limited_producer
5549			.consume()
5550			.scope(&["org/team1/project1".into()])
5551			.expect("should create project1 consumer")
5552			.announced();
5553
5554		// Should only see project1 content at root
5555		project1_consumer.assert_next_some("org/team1/project1");
5556		project1_consumer.assert_next_wait();
5557	}
5558
5559	#[tokio::test]
5560	async fn test_select_with_non_matching_prefix() {
5561		let origin = Origin::random().produce();
5562
5563		// Producer with specific allowed paths
5564		let limited_producer = origin
5565			.scope(&["allowed/path".into()])
5566			.expect("should create limited producer");
5567
5568		// Trying to scope with a completely different prefix should return None
5569		assert!(limited_producer.consume().scope(&["different/path".into()]).is_none());
5570
5571		// Similarly for scope
5572		assert!(limited_producer.scope(&["other/path".into()]).is_none());
5573	}
5574
5575	// Regression test for https://github.com/moq-dev/moq/issues/910
5576	// with_root panics when String has trailing slash (AsPath for String skips normalization)
5577	#[tokio::test]
5578	async fn test_with_root_trailing_slash_consumer() {
5579		let origin = Origin::random().produce();
5580
5581		// Use an owned String so the trailing slash is NOT normalized away.
5582		let prefix = "some_prefix/".to_string();
5583		let mut consumer = origin.consume().with_root(prefix).unwrap().announced();
5584
5585		let _b = origin.create_broadcast("some_prefix/test", announce()).unwrap();
5586		settle().await;
5587		consumer.assert_next_some("test");
5588	}
5589
5590	// Same issue but for the producer side of with_root
5591	#[tokio::test]
5592	async fn test_with_root_trailing_slash_producer() {
5593		let origin = Origin::random().produce();
5594
5595		// Use an owned String so the trailing slash is NOT normalized away.
5596		let prefix = "some_prefix/".to_string();
5597		let rooted = origin.with_root(prefix).unwrap();
5598
5599		let _b = rooted.create_broadcast("test", announce()).unwrap();
5600		settle().await;
5601
5602		let mut consumer = rooted.consume().announced();
5603		consumer.assert_next_some("test");
5604	}
5605
5606	// Verify unannounce also doesn't panic with trailing slash
5607	#[tokio::test]
5608	async fn test_with_root_trailing_slash_unannounce() {
5609		tokio::time::pause();
5610
5611		let origin = Origin::random().produce();
5612
5613		let prefix = "some_prefix/".to_string();
5614		let mut consumer = origin.consume().with_root(prefix).unwrap().announced();
5615
5616		let mut b = origin.create_broadcast("some_prefix/test", announce()).unwrap();
5617		settle().await;
5618		consumer.assert_next_some("test");
5619
5620		// Finish the broadcast to trigger an immediate unannounce.
5621		b.finish();
5622		settle().await;
5623
5624		// unannounce also calls strip_prefix(&self.root).unwrap()
5625		consumer.assert_next_none("test");
5626	}
5627
5628	#[tokio::test]
5629	async fn test_select_maintains_access_with_wider_prefix() {
5630		let origin = Origin::random().produce();
5631
5632		// Setup: user with root "demo" allowed to subscribe to specific paths
5633		let demo_producer = origin.with_root("demo").expect("should create demo root");
5634		let user_producer = demo_producer
5635			.scope(&["worm-node".into(), "foobar".into()])
5636			.expect("should create user producer");
5637
5638		// Publish some data
5639		let _broadcast1 = user_producer
5640			.create_broadcast("worm-node/data", announce())
5641			.expect("publish allowed");
5642		let _broadcast2 = user_producer
5643			.create_broadcast("foobar", announce())
5644			.expect("publish allowed");
5645		settle().await;
5646
5647		// Key test: scope with "" should maintain access to allowed roots
5648		let mut consumer = user_producer
5649			.consume()
5650			.scope(&["".into()])
5651			.expect("scope with empty prefix should not fail when user has specific permissions")
5652			.announced();
5653
5654		// Should still receive broadcasts from allowed paths (order not guaranteed)
5655		let a1 = consumer.try_next().expect("expected first announcement");
5656		let a2 = consumer.try_next().expect("expected second announcement");
5657		consumer.assert_next_wait();
5658
5659		let mut paths: Vec<_> = [&a1, &a2].iter().map(|a| a.path.to_string()).collect();
5660		paths.sort();
5661		assert_eq!(paths, ["foobar", "worm-node/data"]);
5662
5663		// Also test that we can still narrow the scope
5664		let mut narrow_consumer = user_producer
5665			.consume()
5666			.scope(&["worm-node".into()])
5667			.expect("should be able to narrow scope to worm-node")
5668			.announced();
5669
5670		narrow_consumer.assert_next_some("worm-node/data");
5671		narrow_consumer.assert_next_wait(); // Should not see foobar
5672	}
5673
5674	#[tokio::test]
5675	async fn test_duplicate_prefixes_deduped() {
5676		let origin = Origin::random().produce();
5677
5678		// scope with duplicate prefixes should work (deduped internally)
5679		let producer = origin
5680			.scope(&["demo".into(), "demo".into()])
5681			.expect("should create producer");
5682
5683		let _broadcast = producer
5684			.create_broadcast("demo/stream", announce())
5685			.expect("publish allowed");
5686		settle().await;
5687
5688		let mut consumer = producer.consume().announced();
5689		consumer.assert_next_some("demo/stream");
5690		consumer.assert_next_wait();
5691	}
5692
5693	#[tokio::test]
5694	async fn test_overlapping_prefixes_deduped() {
5695		let origin = Origin::random().produce();
5696
5697		// "demo" and "demo/foo". "demo/foo" is redundant, only "demo" should remain
5698		let producer = origin
5699			.scope(&["demo".into(), "demo/foo".into()])
5700			.expect("should create producer");
5701
5702		// Can still publish under "demo/bar" since "demo" covers everything
5703		let _broadcast = producer
5704			.create_broadcast("demo/bar/stream", announce())
5705			.expect("publish allowed");
5706		settle().await;
5707
5708		let mut consumer = producer.consume().announced();
5709		consumer.assert_next_some("demo/bar/stream");
5710		consumer.assert_next_wait();
5711	}
5712
5713	#[tokio::test]
5714	async fn test_overlapping_prefixes_no_duplicate_announcements() {
5715		let origin = Origin::random().produce();
5716
5717		// Both "demo" and "demo/foo" are requested. Should only have one node
5718		let producer = origin
5719			.scope(&["demo".into(), "demo/foo".into()])
5720			.expect("should create producer");
5721
5722		let _broadcast = producer
5723			.create_broadcast("demo/foo/stream", announce())
5724			.expect("publish allowed");
5725		settle().await;
5726
5727		let mut consumer = producer.consume().announced();
5728		// Should only get ONE announcement (not two from overlapping nodes)
5729		consumer.assert_next_some("demo/foo/stream");
5730		consumer.assert_next_wait();
5731	}
5732
5733	#[tokio::test]
5734	async fn test_allowed_returns_deduped_prefixes() {
5735		let origin = Origin::random().produce();
5736
5737		let producer = origin
5738			.scope(&["demo".into(), "demo/foo".into(), "anon".into()])
5739			.expect("should create producer");
5740
5741		let allowed: Vec<_> = producer.allowed().collect();
5742		assert_eq!(allowed.len(), 2, "demo/foo should be subsumed by demo");
5743	}
5744
5745	#[tokio::test]
5746	async fn test_announced_broadcast_already_announced() {
5747		let origin = Origin::random().produce();
5748
5749		let _broadcast = origin.create_broadcast("test", announce()).unwrap();
5750		settle().await;
5751
5752		let consumer = origin.consume();
5753		let result = consumer.announced_broadcast("test").await.expect("should find it");
5754		assert!(result.is_clone(&consumer.get_broadcast("test").unwrap()));
5755	}
5756
5757	#[tokio::test]
5758	async fn test_announced_broadcast_delayed() {
5759		tokio::time::pause();
5760
5761		let origin = Origin::random().produce();
5762
5763		let consumer = origin.consume();
5764
5765		// Start waiting before it's announced.
5766		let wait = tokio::spawn({
5767			let consumer = consumer.clone();
5768			async move { consumer.announced_broadcast("test").await }
5769		});
5770
5771		// Give the spawned task a chance to subscribe.
5772		tokio::task::yield_now().await;
5773
5774		let _broadcast = origin.create_broadcast("test", announce()).unwrap();
5775		settle().await;
5776
5777		let result = wait.await.unwrap().expect("should find it");
5778		assert!(result.is_clone(&consumer.get_broadcast("test").unwrap()));
5779	}
5780
5781	#[tokio::test]
5782	async fn test_announced_broadcast_ignores_unrelated_paths() {
5783		tokio::time::pause();
5784
5785		let origin = Origin::random().produce();
5786
5787		let consumer = origin.consume();
5788
5789		let wait = tokio::spawn({
5790			let consumer = consumer.clone();
5791			async move { consumer.announced_broadcast("target").await }
5792		});
5793
5794		tokio::task::yield_now().await;
5795
5796		// Publish an unrelated broadcast first. announced_broadcast should skip it.
5797		let _other = origin.create_broadcast("other", announce()).unwrap();
5798		settle().await;
5799		tokio::task::yield_now().await;
5800		assert!(!wait.is_finished(), "must not resolve on unrelated path");
5801
5802		let _target = origin.create_broadcast("target", announce()).unwrap();
5803		settle().await;
5804		let result = wait.await.unwrap().expect("should find target");
5805		assert!(result.is_clone(&consumer.get_broadcast("target").unwrap()));
5806	}
5807
5808	#[tokio::test]
5809	async fn test_announced_broadcast_skips_nested_paths() {
5810		tokio::time::pause();
5811
5812		let origin = Origin::random().produce();
5813
5814		let consumer = origin.consume();
5815
5816		let wait = tokio::spawn({
5817			let consumer = consumer.clone();
5818			async move { consumer.announced_broadcast("foo").await }
5819		});
5820
5821		tokio::task::yield_now().await;
5822
5823		// "foo/bar" is under the prefix scope, but it's not the exact path. Skip it.
5824		let _nested = origin.create_broadcast("foo/bar", announce()).unwrap();
5825		settle().await;
5826		tokio::task::yield_now().await;
5827		assert!(!wait.is_finished(), "must not resolve on a nested path");
5828
5829		let _exact = origin.create_broadcast("foo", announce()).unwrap();
5830		settle().await;
5831		let result = wait.await.unwrap().expect("should find foo exactly");
5832		assert!(result.is_clone(&consumer.get_broadcast("foo").unwrap()));
5833	}
5834
5835	#[tokio::test]
5836	async fn test_announced_broadcast_disallowed() {
5837		let origin = Origin::random().produce();
5838		let limited = origin
5839			.consume()
5840			.scope(&["allowed".into()])
5841			.expect("should create limited");
5842
5843		// Path is outside allowed prefixes. Should return None immediately.
5844		assert!(limited.announced_broadcast("notallowed").await.is_none());
5845	}
5846
5847	#[tokio::test]
5848	async fn test_announced_broadcast_scope_too_narrow() {
5849		// Consumer's scope is narrower than the requested path: asking for `foo` on a consumer
5850		// limited to `foo/specific` can never resolve. Must return None, not loop forever.
5851		let origin = Origin::random().produce();
5852		let limited = origin
5853			.consume()
5854			.scope(&["foo/specific".into()])
5855			.expect("should create limited");
5856
5857		// now_or_never so we fail fast instead of hanging if the guard regresses.
5858		let result = limited
5859			.announced_broadcast("foo")
5860			.now_or_never()
5861			.expect("must not block");
5862		assert!(result.is_none());
5863	}
5864
5865	// Coalescing tests: a slow cursor that doesn't drain between updates
5866	// should observe a bounded number of deliveries.
5867
5868	#[tokio::test]
5869	async fn test_coalesce_announce_then_unannounce() {
5870		// announce + unannounce that the cursor hasn't observed yet collapses to nothing.
5871		tokio::time::pause();
5872
5873		let origin = Origin::random().produce();
5874		let mut announced = origin.consume().announced();
5875
5876		let mut broadcast = origin.create_broadcast("test", announce()).unwrap();
5877		settle().await;
5878		broadcast.finish();
5879
5880		settle().await;
5881
5882		announced.assert_next_wait();
5883	}
5884
5885	#[tokio::test]
5886	async fn test_coalesce_announce_unannounce_announce() {
5887		// announce, unannounce, announce that the cursor hasn't drained collapses
5888		// to a single Announce of the latest broadcast.
5889		tokio::time::pause();
5890
5891		let origin = Origin::random().produce();
5892		let mut announced = origin.consume().announced();
5893
5894		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
5895		settle().await;
5896		broadcast1.finish();
5897		settle().await;
5898		let _broadcast2 = origin.create_broadcast("test", announce()).unwrap();
5899		settle().await;
5900
5901		announced.assert_next_some("test");
5902		announced.assert_next_wait();
5903	}
5904
5905	#[tokio::test]
5906	async fn test_coalesce_unannounce_announce_preserved() {
5907		// unannounce followed by announce of a different broadcast must be preserved
5908		// as two deliveries so the cursor learns the origin changed.
5909		tokio::time::pause();
5910
5911		let origin = Origin::random().produce();
5912		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
5913		settle().await;
5914
5915		let mut announced = origin.consume().announced();
5916		announced.assert_next_some("test");
5917
5918		// Finish, then publish a fresh broadcast at the same path.
5919		broadcast1.finish();
5920		settle().await;
5921
5922		let _broadcast2 = origin.create_broadcast("test", announce()).unwrap();
5923		settle().await;
5924
5925		// The cursor must see the unannounce before the new announce.
5926		announced.assert_next_none("test");
5927		announced.assert_next_some("test");
5928		announced.assert_next_wait();
5929	}
5930
5931	#[tokio::test]
5932	async fn test_coalesce_unannounce_announce_unannounce() {
5933		// unannounce + announce + unannounce collapses to a single unannounce: the
5934		// embedded announce was never observed.
5935		tokio::time::pause();
5936
5937		let origin = Origin::random().produce();
5938		let mut broadcast1 = origin.create_broadcast("test", announce()).unwrap();
5939		settle().await;
5940
5941		let mut announced = origin.consume().announced();
5942		announced.assert_next_some("test");
5943
5944		broadcast1.finish();
5945		settle().await;
5946
5947		let mut broadcast2 = origin.create_broadcast("test", announce()).unwrap();
5948		settle().await;
5949		broadcast2.finish();
5950		settle().await;
5951
5952		announced.assert_next_none("test");
5953		announced.assert_next_wait();
5954	}
5955
5956	#[tokio::test]
5957	async fn test_coalesce_churn_bounded() {
5958		// A churn loop on a single path should keep the pending set bounded.
5959		// Backup promotion during cleanup can leave the cursor with zero or one
5960		// pending update for "test" depending on the order tasks run; we only
5961		// require that churn doesn't accumulate across iterations.
5962		tokio::time::pause();
5963
5964		let origin = Origin::random().produce();
5965		let mut announced = origin.consume().announced();
5966
5967		for _ in 0..1000 {
5968			let mut broadcast = origin.create_broadcast("test", announce()).unwrap();
5969			settle().await;
5970			broadcast.finish();
5971		}
5972		settle().await;
5973
5974		let mut collected = Vec::new();
5975		while let Some(update) = announced.try_next() {
5976			collected.push(update);
5977		}
5978		assert!(
5979			collected.len() <= 1,
5980			"expected at most one pending update, got {}",
5981			collected.len()
5982		);
5983		assert!(
5984			collected.iter().all(|a| a.path == Path::new("test")),
5985			"unexpected path in pending updates",
5986		);
5987	}
5988
5989	// Consumer should be cheap to clone: cloning must NOT drain any
5990	// other cursor's announce channel. A freshly-built AnnounceConsumer
5991	// still receives the active backlog.
5992	#[tokio::test]
5993	async fn test_consumer_clone_is_side_effect_free() {
5994		let origin = Origin::random().produce();
5995
5996		let _broadcast1 = origin.create_broadcast("test1", announce()).unwrap();
5997		let _broadcast2 = origin.create_broadcast("test2", announce()).unwrap();
5998		settle().await;
5999
6000		let consumer = origin.consume();
6001		let mut announced = consumer.announced();
6002
6003		// Cloning the Consumer many times and looking up broadcasts
6004		// must not consume any events from the existing cursor.
6005		for _ in 0..16 {
6006			let cloned = consumer.clone();
6007			assert!(cloned.get_broadcast("test1").is_some());
6008			assert!(cloned.get_broadcast("test2").is_some());
6009		}
6010
6011		// The original cursor still sees both announcements in their
6012		// natural order, undisturbed by the clones above.
6013		let a1 = announced.try_next().expect("first announcement");
6014		let a2 = announced.try_next().expect("second announcement");
6015		announced.assert_next_wait();
6016
6017		let mut paths: Vec<_> = [&a1, &a2].iter().map(|a| a.path.to_string()).collect();
6018		paths.sort();
6019		assert_eq!(paths, ["test1", "test2"]);
6020
6021		// A freshly-built AnnounceConsumer still receives the active backlog.
6022		let mut fresh = consumer.announced();
6023		let b1 = fresh.try_next().expect("backlog: first");
6024		let b2 = fresh.try_next().expect("backlog: second");
6025		fresh.assert_next_wait();
6026
6027		let mut paths: Vec<_> = [&b1, &b2].iter().map(|a| a.path.to_string()).collect();
6028		paths.sort();
6029		assert_eq!(paths, ["test1", "test2"]);
6030	}
6031
6032	// With no Dynamic handler, an unannounced path resolves to Unroutable.
6033	#[tokio::test]
6034	async fn dynamic_request_unroutable_without_handler() {
6035		let origin = Origin::random().produce();
6036		let consumer = origin.consume();
6037		assert!(matches!(
6038			consumer.request_broadcast("missing").await,
6039			Err(Error::Unroutable)
6040		));
6041	}
6042
6043	// A dynamically served broadcast resolves the requester and serves tracks, but is
6044	// never announced.
6045	#[tokio::test(start_paused = true)]
6046	async fn dynamic_request_served_not_announced() {
6047		let origin = Origin::random().produce();
6048		let mut dynamic = origin.dynamic();
6049		let consumer = origin.consume();
6050
6051		// A separate announce cursor must never observe the dynamic broadcast.
6052		let mut announced = origin.consume().announced();
6053		announced.assert_next_wait();
6054
6055		let served = broadcast::Info::new().produce();
6056		// Request a path that nobody announced; the future stays pending until served.
6057		// Registration happens up front, so the handler sees the request immediately.
6058		let request_fut = consumer.request_broadcast("fallback");
6059
6060		// The handler serves it with a live broadcast it keeps producing into.
6061		let mut served_dynamic = served.dynamic();
6062
6063		let request = dynamic.requested_broadcast().await.unwrap();
6064		assert_eq!(request.path(), &Path::new("fallback"));
6065		request.accept(&served);
6066
6067		let broadcast = request_fut.await.unwrap();
6068		assert!(broadcast.is_clone(&served.consume()));
6069
6070		// The served broadcast is live: a track subscription resolves via its handler.
6071		let track_fut = broadcast.track("video").unwrap().subscribe(None);
6072		let mut producer = served_dynamic.requested_track().await.unwrap().accept(None);
6073		let mut track = track_fut.await.unwrap();
6074		producer.append_group().unwrap();
6075		track.assert_group();
6076
6077		// Still nothing announced.
6078		announced.assert_next_wait();
6079	}
6080
6081	// Concurrent requests for the same queued path coalesce onto one handler request.
6082	#[tokio::test(start_paused = true)]
6083	async fn dynamic_request_coalesces() {
6084		let origin = Origin::random().produce();
6085		let mut dynamic = origin.dynamic();
6086		let consumer = origin.consume();
6087
6088		// Both register before the handler drains either.
6089		let f1 = consumer.request_broadcast("dup");
6090		let f2 = consumer.request_broadcast("dup");
6091
6092		// Exactly one request reaches the handler.
6093		let request = dynamic.requested_broadcast().await.unwrap();
6094		assert_eq!(request.path(), &Path::new("dup"));
6095		assert!(
6096			dynamic.requested_broadcast().now_or_never().is_none(),
6097			"a coalesced request must not be served twice"
6098		);
6099
6100		// Accepting resolves both awaiting requesters with the same broadcast.
6101		let served = broadcast::Info::new().produce();
6102		request.accept(&served);
6103		assert!(f1.await.unwrap().is_clone(&served.consume()));
6104		assert!(f2.await.unwrap().is_clone(&served.consume()));
6105	}
6106
6107	// A repeat request for an already-served, still-live path shares the same broadcast
6108	// instead of asking the handler again (no duplicate upstream subscription).
6109	#[tokio::test(start_paused = true)]
6110	async fn dynamic_request_dedups_served() {
6111		let origin = Origin::random().produce();
6112		let mut dynamic = origin.dynamic();
6113		let consumer = origin.consume();
6114
6115		let request_fut = consumer.request_broadcast("fallback");
6116		let request = dynamic.requested_broadcast().await.unwrap();
6117		let served = broadcast::Info::new().produce();
6118		request.accept(&served);
6119		let first = request_fut.await.unwrap();
6120		assert!(first.is_clone(&served.consume()));
6121
6122		// The repeat resolves immediately to the same broadcast...
6123		let second = consumer.request_broadcast("fallback").await.unwrap();
6124		assert!(second.is_clone(&served.consume()));
6125
6126		// ...and the handler never sees a second request.
6127		assert!(
6128			dynamic.requested_broadcast().now_or_never().is_none(),
6129			"a still-live served broadcast must not be re-requested from the handler"
6130		);
6131	}
6132
6133	// Once a served broadcast closes, its cache entry is stale, so the next request re-serves.
6134	#[tokio::test(start_paused = true)]
6135	async fn dynamic_request_reserves_after_close() {
6136		let origin = Origin::random().produce();
6137		let mut dynamic = origin.dynamic();
6138		let consumer = origin.consume();
6139
6140		let request_fut = consumer.request_broadcast("fallback");
6141		let request = dynamic.requested_broadcast().await.unwrap();
6142		let served = broadcast::Info::new().produce();
6143		request.accept(&served);
6144		request_fut.await.unwrap();
6145
6146		// Close the first served broadcast; the weak cache entry goes stale.
6147		drop(served);
6148
6149		// A fresh request must reach the handler again and resolve to the new broadcast.
6150		let request_fut = consumer.request_broadcast("fallback");
6151		let request = dynamic.requested_broadcast().await.unwrap();
6152		assert_eq!(request.path(), &Path::new("fallback"));
6153		let served = broadcast::Info::new().produce();
6154		request.accept(&served);
6155		assert!(request_fut.await.unwrap().is_clone(&served.consume()));
6156	}
6157
6158	// Serving many distinct one-shot paths that each close must not grow the `served` cache
6159	// unboundedly: the amortized GC on `accept` reclaims the stale entries left by closed ones.
6160	#[tokio::test(start_paused = true)]
6161	async fn dynamic_request_served_cache_bounded() {
6162		let origin = Origin::random().produce();
6163		let mut dynamic = origin.dynamic();
6164		let consumer = origin.consume();
6165
6166		for i in 0..100 {
6167			let path = format!("one-shot/{i}");
6168			let request_fut = consumer.request_broadcast(&path);
6169			let request = dynamic.requested_broadcast().await.unwrap();
6170			let served = broadcast::Info::new().produce();
6171			request.accept(&served);
6172			request_fut.await.unwrap();
6173			// Close the served broadcast; its cache entry is now stale.
6174			drop(served);
6175		}
6176
6177		// The GC keeps the map bounded by the live count (zero here) plus a small probe window,
6178		// rather than one entry per distinct path.
6179		assert!(
6180			origin.dynamic.read().served.len() <= 4,
6181			"stale served entries must be reclaimed, not accumulate per distinct path: {}",
6182			origin.dynamic.read().served.len()
6183		);
6184	}
6185
6186	// A repeat request in the window after the handler picks one up but before it accepts
6187	// coalesces onto the in-flight request instead of queuing a duplicate.
6188	#[tokio::test(start_paused = true)]
6189	async fn dynamic_request_coalesces_after_handoff() {
6190		let origin = Origin::random().produce();
6191		let mut dynamic = origin.dynamic();
6192		let consumer = origin.consume();
6193
6194		let f1 = consumer.request_broadcast("fallback");
6195		// Handler drains the request but has not accepted yet.
6196		let request = dynamic.requested_broadcast().await.unwrap();
6197
6198		// A second request in this window must not queue another handler request.
6199		let f2 = consumer.request_broadcast("fallback");
6200		assert!(
6201			dynamic.requested_broadcast().now_or_never().is_none(),
6202			"a repeat request during hand-off must coalesce, not re-queue"
6203		);
6204
6205		// Accepting resolves both awaiting requesters with the same broadcast.
6206		let served = broadcast::Info::new().produce();
6207		request.accept(&served);
6208		assert!(f1.await.unwrap().is_clone(&served.consume()));
6209		assert!(f2.await.unwrap().is_clone(&served.consume()));
6210	}
6211
6212	// Dropping a handed-off request without accept/reject rejects every coalesced requester.
6213	#[tokio::test(start_paused = true)]
6214	async fn dynamic_request_dropped_after_handoff() {
6215		let origin = Origin::random().produce();
6216		let mut dynamic = origin.dynamic();
6217		let consumer = origin.consume();
6218
6219		let f1 = consumer.request_broadcast("fallback");
6220		let request = dynamic.requested_broadcast().await.unwrap();
6221		let f2 = consumer.request_broadcast("fallback");
6222
6223		// Abandon it; both requesters resolve to Unroutable instead of hanging.
6224		drop(request);
6225		assert!(matches!(f1.await, Err(Error::Unroutable)));
6226		assert!(matches!(f2.await, Err(Error::Unroutable)));
6227	}
6228
6229	// Rejecting a request resolves the requester with the error.
6230	#[tokio::test(start_paused = true)]
6231	async fn dynamic_request_rejected() {
6232		let origin = Origin::random().produce();
6233		let mut dynamic = origin.dynamic();
6234		let consumer = origin.consume();
6235
6236		let request_fut = consumer.request_broadcast("fallback");
6237
6238		let request = dynamic.requested_broadcast().await.unwrap();
6239		request.reject(Error::Cancel);
6240
6241		assert!(matches!(request_fut.await, Err(Error::Cancel)));
6242	}
6243
6244	// After a rejected hand-off, a fresh request for the same path reaches the handler again:
6245	// the rejected `Request`'s removal + `Drop` leave the request queue consistent
6246	// (a stale/clobbered entry would strand this request or panic the handler).
6247	#[tokio::test(start_paused = true)]
6248	async fn dynamic_request_rerequest_after_reject() {
6249		let origin = Origin::random().produce();
6250		let mut dynamic = origin.dynamic();
6251		let consumer = origin.consume();
6252
6253		let f1 = consumer.request_broadcast("fallback");
6254		dynamic.requested_broadcast().await.unwrap().reject(Error::Unroutable);
6255		assert!(matches!(f1.await, Err(Error::Unroutable)));
6256
6257		let served = broadcast::Info::new().produce();
6258		// A fresh request re-reaches the handler and can be served.
6259		let f2 = consumer.request_broadcast("fallback");
6260		let request = dynamic.requested_broadcast().await.unwrap();
6261		assert_eq!(request.path(), &Path::new("fallback"));
6262		request.accept(&served);
6263		assert!(f2.await.unwrap().is_clone(&served.consume()));
6264	}
6265
6266	// Dropping the last handler resolves queued requests with an error and reverts to
6267	// resolving Unroutable.
6268	#[tokio::test(start_paused = true)]
6269	async fn dynamic_request_handler_dropped() {
6270		let origin = Origin::random().produce();
6271		let dynamic = origin.dynamic();
6272		let consumer = origin.consume();
6273
6274		let request_fut = consumer.request_broadcast("fallback");
6275		drop(dynamic);
6276		assert!(matches!(request_fut.await, Err(Error::Unroutable)));
6277
6278		// With no handler left, a fresh request resolves Unroutable.
6279		assert!(matches!(
6280			consumer.request_broadcast("again").await,
6281			Err(Error::Unroutable)
6282		));
6283	}
6284
6285	// `accept` is decoupled from the dynamic count: once a handler has picked a request up,
6286	// it can still serve it even if every handler (including itself) drops first, flipping the
6287	// count to zero. The in-flight request must not be rejected as `Unroutable`.
6288	#[tokio::test(start_paused = true)]
6289	async fn dynamic_request_accept_after_handler_dropped() {
6290		let origin = Origin::random().produce();
6291		let mut dynamic = origin.dynamic();
6292		let consumer = origin.consume();
6293
6294		let request_fut = consumer.request_broadcast("fallback");
6295
6296		// The handler picks the request up, then every handler drops (count -> 0).
6297		let request = dynamic.requested_broadcast().await.unwrap();
6298		drop(dynamic);
6299
6300		let served = broadcast::Info::new().produce();
6301		// Accept still resolves the awaiting requester with the served broadcast.
6302		request.accept(&served);
6303		assert!(request_fut.await.unwrap().is_clone(&served.consume()));
6304	}
6305
6306	// A published broadcast wins over the dynamic fallback; no request is queued.
6307	#[tokio::test(start_paused = true)]
6308	async fn dynamic_request_prefers_announced() {
6309		let origin = Origin::random().produce();
6310		let mut dynamic = origin.dynamic();
6311		let consumer = origin.consume();
6312
6313		let _broadcast = origin.create_broadcast("live", announce()).unwrap();
6314		settle().await;
6315
6316		let got = consumer.request_broadcast("live").await.unwrap();
6317		assert!(
6318			got.is_clone(&consumer.get_broadcast("live").unwrap()),
6319			"should return the published broadcast"
6320		);
6321		assert!(
6322			dynamic.requested_broadcast().now_or_never().is_none(),
6323			"a published path must not queue a fallback request"
6324		);
6325	}
6326
6327	// Cloning a handler and dropping the clone must not flip the count to zero.
6328	#[tokio::test(start_paused = true)]
6329	async fn dynamic_clone_keeps_alive() {
6330		let origin = Origin::random().produce();
6331		let dynamic = origin.dynamic();
6332		let consumer = origin.consume();
6333
6334		drop(dynamic.clone());
6335
6336		// The original handle is still live, so the request registers (stays pending)
6337		// instead of resolving Unroutable.
6338		let request_fut = consumer.request_broadcast("fallback");
6339		assert!(
6340			request_fut.now_or_never().is_none(),
6341			"request should stay pending until served"
6342		);
6343	}
6344}