Skip to main content

moq_net/model/
origin.rs

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