Skip to main content

moq_net/
client.rs

1use crate::origin;
2use crate::{
3	ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05,
4	ALPN_LITE_06_WIP, Consume, Driver, Error, NEGOTIATED, Session, Version, Versions,
5	coding::{self, Decode, Encode, Stream},
6	ietf, lite, setup, stats,
7};
8
9/// A MoQ client session builder.
10#[derive(Default, Clone)]
11pub struct Client {
12	publish: Option<origin::Consumer>,
13	subscribe: Option<origin::Producer>,
14	stats: stats::Session,
15	versions: Versions,
16	setup_path: Option<String>,
17	cost: Option<u64>,
18}
19
20impl Client {
21	/// A client that neither publishes nor subscribes until configured.
22	pub fn new() -> Self {
23		Default::default()
24	}
25
26	/// Publish local broadcasts to the remote: the session reads from the given
27	/// origin (pass an [`origin::Producer`] or [`origin::Consumer`] by reference) and
28	/// forwards its announcements. Omit to publish nothing.
29	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
30		self.publish = Some(publish.consume());
31		self
32	}
33
34	/// Subscribe to remote broadcasts: the session writes the broadcasts the
35	/// remote announces into this [`origin::Producer`]. Omit to subscribe to nothing.
36	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
37		self.subscribe = Some(subscribe);
38		self
39	}
40
41	/// Attach a per-connection [`stats::Session`] context. The session's publish
42	/// (egress) and subscribe (ingress) origin handles are tagged with it, so all
43	/// traffic counters are attributed through the model for this session's lifetime.
44	/// Pass [`stats::Session::default`] (a no-op context) to opt out.
45	pub fn with_stats(mut self, stats: stats::Session) -> Self {
46		self.stats = stats;
47		self
48	}
49
50	/// Set both publish and subscribe from one shared [`origin::Producer`].
51	///
52	/// Equivalent to [`with_publisher`](Self::with_publisher) and
53	/// [`with_subscriber`](Self::with_subscriber) with the same origin.
54	pub fn with_origin(self, origin: origin::Producer) -> Self {
55		self.with_publisher(&origin).with_subscriber(origin)
56	}
57
58	/// Restrict which protocol versions to offer, in preference order.
59	/// Defaults to every version this crate supports.
60	pub fn with_versions(mut self, versions: Versions) -> Self {
61		self.versions = versions;
62		self
63	}
64
65	/// Set the request path to advertise in the SETUP (moq-lite-05 and every
66	/// moq-transport draft we speak).
67	///
68	/// Only for transports that carry no request URI of their own (native QUIC, qmux
69	/// over TCP/TLS, unix sockets), so the server learns which path the client wants.
70	/// Bindings that already carry a URI (WebTransport, qmux over WebSocket) convey
71	/// the path there and MUST NOT send this; a server is entitled to treat it as a
72	/// protocol violation. An empty path is equivalent to omitting it. Ignored by
73	/// versions with no in-band request path (lite 01-04).
74	pub fn with_path(mut self, path: impl Into<String>) -> Self {
75		self.setup_path = Some(path.into());
76		self
77	}
78
79	/// Price this link, in the units the rest of the mesh uses (moq-lite-06+).
80	///
81	/// Every announcement crossing the connection adds this to its route cost, so
82	/// routing prefers cheap paths over short ones. Use `0` for a link that should
83	/// look free (a sibling in the same datacenter), and something large for one that
84	/// should be a last resort (a metered backbone). An unpriced link costs `1`,
85	/// which makes the cost track the hop count and so reproduces plain
86	/// shortest-path routing.
87	///
88	/// The dialing side owns the price: it is declared in our SETUP so the server
89	/// charges the same link the same amount. A server never sets one.
90	pub fn with_cost(mut self, cost: u64) -> Self {
91		self.cost = Some(cost);
92		self
93	}
94
95	/// Perform the MoQ handshake, returning the [`Session`] and the [`Driver`] that
96	/// runs its protocol work. The driver must be polled (spawned or awaited) for
97	/// the session to make progress.
98	pub async fn connect<S: web_transport_trait::Session>(&self, session: S) -> Result<(Session, Driver), Error> {
99		if self.publish.is_none() && self.subscribe.is_none() {
100			tracing::warn!("not publishing or consuming anything");
101		}
102
103		// Tag the origin pair with the stats context: reads through the publish
104		// (egress) consumer and writes through the subscribe (ingress) producer are
105		// then attributed by the model. One shared context, so presence and viewer
106		// counts are never double-attributed across the two halves.
107		let publish = self.publish.clone().map(|origin| origin.with_stats(self.stats.clone()));
108		let subscribe = self
109			.subscribe
110			.clone()
111			.map(|origin| origin.with_stats(self.stats.clone()));
112
113		// If ALPN was used to negotiate the version, use the appropriate encoding.
114		// Default to IETF 14 if no ALPN was used and we'll negotiate the version later.
115		let (encoding, supported) = match session.protocol() {
116			Some(ALPN_19) => {
117				let v = self
118					.versions
119					.select(Version::Ietf(ietf::Version::Draft19))
120					.ok_or(Error::Version)?;
121
122				// Draft-17+: SETUP is exchanged by the connection driver.
123				let protocol = ietf::start(
124					session.clone(),
125					None,
126					None,
127					true,
128					publish.clone(),
129					subscribe.clone(),
130					ietf::Version::Draft19,
131					self.setup_path.clone(),
132					None,
133				)?;
134
135				tracing::debug!(version = ?v, "connected");
136				return Ok(Session::new(session, v, None, protocol));
137			}
138			Some(ALPN_18) => {
139				let v = self
140					.versions
141					.select(Version::Ietf(ietf::Version::Draft18))
142					.ok_or(Error::Version)?;
143
144				// Draft-17+: SETUP is exchanged by the connection driver.
145				// We advertise the request path in our SETUP for URL-less transports.
146				let protocol = ietf::start(
147					session.clone(),
148					None,
149					None,
150					true,
151					publish.clone(),
152					subscribe.clone(),
153					ietf::Version::Draft18,
154					self.setup_path.clone(),
155					None,
156				)?;
157
158				tracing::debug!(version = ?v, "connected");
159				return Ok(Session::new(session, v, None, protocol));
160			}
161			Some(ALPN_17) => {
162				let v = self
163					.versions
164					.select(Version::Ietf(ietf::Version::Draft17))
165					.ok_or(Error::Version)?;
166
167				// Draft-17+: SETUP is exchanged by the connection driver.
168				// We advertise the request path in our SETUP for URL-less transports.
169				let protocol = ietf::start(
170					session.clone(),
171					None,
172					None,
173					true,
174					publish.clone(),
175					subscribe.clone(),
176					ietf::Version::Draft17,
177					self.setup_path.clone(),
178					None,
179				)?;
180
181				tracing::debug!(version = ?v, "connected");
182				return Ok(Session::new(session, v, None, protocol));
183			}
184			Some(ALPN_16) => {
185				let v = self
186					.versions
187					.select(Version::Ietf(ietf::Version::Draft16))
188					.ok_or(Error::Version)?;
189				(v, v.into())
190			}
191			Some(ALPN_15) => {
192				let v = self
193					.versions
194					.select(Version::Ietf(ietf::Version::Draft15))
195					.ok_or(Error::Version)?;
196				(v, v.into())
197			}
198			Some(ALPN_14) => {
199				let v = self
200					.versions
201					.select(Version::Ietf(ietf::Version::Draft14))
202					.ok_or(Error::Version)?;
203				(v, v.into())
204			}
205			Some(alpn @ (ALPN_LITE_05 | ALPN_LITE_06_WIP)) => {
206				let version = match alpn {
207					ALPN_LITE_06_WIP => lite::Version::Lite06Wip,
208					_ => lite::Version::Lite05,
209				};
210				self.versions.select(Version::Lite(version)).ok_or(Error::Version)?;
211
212				// Advertise our capabilities (we report send bitrate; we don't pad) plus
213				// the request path on URI-less transports, and the direction we intend to
214				// use so the server can reject a token that lacks the matching scope during
215				// the handshake instead of silently carrying no media.
216				let our_setup = lite::Setup {
217					probe: lite::ProbeLevel::Report,
218					path: self.setup_path.clone(),
219					role: lite::Role::from_origins(self.publish.is_some(), self.subscribe.is_some()),
220					cost: self.cost,
221				};
222
223				let start = lite::start(
224					session.clone(),
225					None,
226					publish.clone(),
227					subscribe.clone(),
228					version,
229					our_setup,
230					None,
231				)?;
232
233				// Block until the initial announce set has landed (Lite05+ reports it
234				// via AnnounceOk + N), so a `request_broadcast()` for a live path resolves
235				// immediately instead of racing announcement gossip.
236				let (session, mut driver) = Session::new(session, version.into(), start.recv_bandwidth, start.driver);
237				driver.wait_ready(start.connecting.ready()).await;
238
239				return Ok((session, driver));
240			}
241			Some(ALPN_LITE_04) => {
242				self.versions
243					.select(Version::Lite(lite::Version::Lite04))
244					.ok_or(Error::Version)?;
245
246				let start = lite::start(
247					session.clone(),
248					None,
249					publish.clone(),
250					subscribe.clone(),
251					lite::Version::Lite04,
252					lite::Setup::default(),
253					None,
254				)?;
255
256				// Lite04 has no initial-set boundary, so this resolves immediately.
257				let (session, mut driver) = Session::new(
258					session,
259					lite::Version::Lite04.into(),
260					start.recv_bandwidth,
261					start.driver,
262				);
263				driver.wait_ready(start.connecting.ready()).await;
264
265				return Ok((session, driver));
266			}
267			Some(ALPN_LITE_03) => {
268				self.versions
269					.select(Version::Lite(lite::Version::Lite03))
270					.ok_or(Error::Version)?;
271
272				// Starting with draft-03, there's no more SETUP control stream.
273				let start = lite::start(
274					session.clone(),
275					None,
276					publish.clone(),
277					subscribe.clone(),
278					lite::Version::Lite03,
279					lite::Setup::default(),
280					None,
281				)?;
282
283				// Lite03 has no initial-set boundary, so this resolves immediately.
284				let (session, mut driver) = Session::new(
285					session,
286					lite::Version::Lite03.into(),
287					start.recv_bandwidth,
288					start.driver,
289				);
290				driver.wait_ready(start.connecting.ready()).await;
291
292				return Ok((session, driver));
293			}
294			Some(ALPN_LITE) | None => {
295				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
296				(Version::Ietf(ietf::Version::Draft14), supported)
297			}
298			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
299		};
300
301		let mut stream = Stream::open(&session, encoding).await?;
302
303		// The encoding is always an IETF version for SETUP negotiation.
304		let ietf_encoding = ietf::Version::try_from(encoding).map_err(|_| Error::Version)?;
305
306		let mut parameters = ietf::Parameters::default();
307		parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
308		parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
309		// Advertise the request path in-band (draft 14-16), same as the lite-05 SETUP.
310		if let Some(path) = &self.setup_path {
311			parameters.set_bytes(ietf::ParameterBytes::Path, path.clone().into_bytes());
312		}
313		let parameters = parameters.encode_bytes(ietf_encoding)?;
314
315		let client = setup::Client {
316			versions: supported.clone().into(),
317			parameters,
318		};
319
320		stream.writer.encode(&client).await?;
321
322		let mut server: setup::Server = stream.reader.decode().await?;
323
324		let version = supported
325			.iter()
326			.find(|v| coding::Version::from(**v) == server.version)
327			.copied()
328			.ok_or(Error::Version)?;
329
330		let (recv_bw, protocol, connecting) = match version {
331			Version::Lite(v) => {
332				let stream = stream.with_version(v);
333				let start = lite::start(
334					session.clone(),
335					Some(stream),
336					publish.clone(),
337					subscribe.clone(),
338					v,
339					// This path only handles versions negotiated via the bidi SETUP exchange
340					// (pre-lite-05), which have no Setup Stream.
341					lite::Setup::default(),
342					None,
343				)?;
344
345				(start.recv_bandwidth, start.driver, Some(start.connecting))
346			}
347			Version::Ietf(v) => {
348				// Decode the parameters to get the initial request ID.
349				let parameters = ietf::Parameters::decode(&mut server.parameters, v)?;
350				let request_id_max = parameters
351					.get_varint(ietf::ParameterVarInt::MaxRequestId)
352					.map(ietf::RequestId);
353
354				let stream = stream.with_version(v);
355				// Draft 14-16: the path rode in the bidi SETUP above, not the uni one.
356				let protocol = ietf::start(
357					session.clone(),
358					Some(stream),
359					request_id_max,
360					true,
361					publish.clone(),
362					subscribe.clone(),
363					v,
364					None,
365					None,
366				)?;
367				(None, protocol, None)
368			}
369		};
370
371		let (session, mut driver) = Session::new(session, version, recv_bw, protocol);
372		if let Some(connecting) = connecting {
373			// Block until the initial announce set has landed (for versions that
374			// report one); resolves immediately otherwise.
375			driver.wait_ready(connecting.ready()).await;
376		}
377
378		Ok((session, driver))
379	}
380}
381
382#[cfg(test)]
383mod tests {
384	use super::*;
385	use std::{
386		collections::VecDeque,
387		sync::{Arc, Mutex},
388	};
389
390	use crate::coding::{Decode, Encode};
391	use bytes::{BufMut, Bytes};
392
393	#[derive(Debug, Clone, Default)]
394	struct FakeError;
395
396	impl std::fmt::Display for FakeError {
397		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398			write!(f, "fake transport error")
399		}
400	}
401
402	impl std::error::Error for FakeError {}
403
404	impl web_transport_trait::Error for FakeError {
405		fn session_error(&self) -> Option<(u32, String)> {
406			Some((0, "closed".to_string()))
407		}
408	}
409
410	#[derive(Clone, Default)]
411	struct FakeSession {
412		state: Arc<FakeSessionState>,
413	}
414
415	#[derive(Default)]
416	struct FakeSessionState {
417		protocol: Option<&'static str>,
418		control_stream: Mutex<Option<(FakeSendStream, FakeRecvStream)>>,
419		close_events: Mutex<Vec<(u32, String)>>,
420		close_notify: tokio::sync::Notify,
421		control_writes: Arc<Mutex<Vec<u8>>>,
422		send_rate: Mutex<Option<u64>>,
423	}
424
425	impl FakeSession {
426		fn new(protocol: Option<&'static str>, server_control_bytes: Vec<u8>) -> Self {
427			let writes = Arc::new(Mutex::new(Vec::new()));
428			let send = FakeSendStream { writes: writes.clone() };
429			let recv = FakeRecvStream {
430				data: VecDeque::from(server_control_bytes),
431			};
432			let state = FakeSessionState {
433				protocol,
434				control_stream: Mutex::new(Some((send, recv))),
435				close_events: Mutex::new(Vec::new()),
436				close_notify: tokio::sync::Notify::new(),
437				control_writes: writes,
438				send_rate: Mutex::new(None),
439			};
440			Self { state: Arc::new(state) }
441		}
442
443		fn set_send_rate(&self, rate: Option<u64>) {
444			*self.state.send_rate.lock().unwrap() = rate;
445		}
446
447		fn control_writes(&self) -> Vec<u8> {
448			self.state.control_writes.lock().unwrap().clone()
449		}
450
451		async fn wait_for_first_close(&self) -> (u32, String) {
452			loop {
453				let notified = self.state.close_notify.notified();
454				if let Some(close) = self.state.close_events.lock().unwrap().first().cloned() {
455					return close;
456				}
457				notified.await;
458			}
459		}
460	}
461
462	impl web_transport_trait::Session for FakeSession {
463		type SendStream = FakeSendStream;
464		type RecvStream = FakeRecvStream;
465		type Error = FakeError;
466
467		async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
468			std::future::pending().await
469		}
470
471		async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
472			std::future::pending().await
473		}
474
475		async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
476			self.state.control_stream.lock().unwrap().take().ok_or(FakeError)
477		}
478
479		async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
480			std::future::pending().await
481		}
482
483		fn send_datagram(&self, _payload: Bytes) -> Result<(), Self::Error> {
484			Ok(())
485		}
486
487		async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
488			std::future::pending().await
489		}
490
491		fn max_datagram_size(&self) -> usize {
492			1200
493		}
494
495		fn protocol(&self) -> Option<&str> {
496			self.state.protocol
497		}
498
499		fn close(&self, code: u32, reason: &str) {
500			self.state.close_events.lock().unwrap().push((code, reason.to_string()));
501			self.state.close_notify.notify_waiters();
502		}
503
504		async fn closed(&self) -> Self::Error {
505			loop {
506				let notified = self.state.close_notify.notified();
507				if !self.state.close_events.lock().unwrap().is_empty() {
508					return FakeError;
509				}
510				notified.await;
511			}
512		}
513
514		fn stats(&self) -> impl web_transport_trait::Stats {
515			FakeStats {
516				send_rate: *self.state.send_rate.lock().unwrap(),
517			}
518		}
519	}
520
521	struct FakeStats {
522		send_rate: Option<u64>,
523	}
524
525	impl web_transport_trait::Stats for FakeStats {
526		fn estimated_send_rate(&self) -> Option<u64> {
527			self.send_rate
528		}
529	}
530
531	#[derive(Clone, Default)]
532	struct FakeSendStream {
533		writes: Arc<Mutex<Vec<u8>>>,
534	}
535
536	impl web_transport_trait::SendStream for FakeSendStream {
537		type Error = FakeError;
538
539		async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
540			self.writes.lock().unwrap().put_slice(buf);
541			Ok(buf.len())
542		}
543
544		fn set_priority(&mut self, _order: u8) {}
545
546		fn finish(&mut self) -> Result<(), Self::Error> {
547			Ok(())
548		}
549
550		fn reset(&mut self, _code: u32) {}
551
552		async fn closed(&mut self) -> Result<(), Self::Error> {
553			Ok(())
554		}
555	}
556
557	struct FakeRecvStream {
558		data: VecDeque<u8>,
559	}
560
561	impl web_transport_trait::RecvStream for FakeRecvStream {
562		type Error = FakeError;
563
564		async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
565			if self.data.is_empty() {
566				return Ok(None);
567			}
568
569			let size = dst.len().min(self.data.len());
570			for slot in dst.iter_mut().take(size) {
571				*slot = self.data.pop_front().unwrap();
572			}
573			Ok(Some(size))
574		}
575
576		fn stop(&mut self, _code: u32) {}
577
578		async fn closed(&mut self) -> Result<(), Self::Error> {
579			Ok(())
580		}
581	}
582
583	fn mock_server_setup(negotiated: Version) -> Vec<u8> {
584		let mut encoded = Vec::new();
585		let server = setup::Server {
586			version: negotiated.into(),
587			parameters: Bytes::new(),
588		};
589		server
590			.encode(&mut encoded, Version::Ietf(ietf::Version::Draft14))
591			.unwrap();
592
593		// Add a setup-stream SessionInfo frame using the negotiated Lite version.
594		let info = lite::SessionInfo { bitrate: Some(1) };
595		let lite_v = lite::Version::try_from(negotiated).unwrap();
596		info.encode(&mut encoded, lite_v).unwrap();
597
598		encoded
599	}
600
601	async fn run_alpn_lite_fallback_case(protocol: Option<&'static str>) {
602		let fake = FakeSession::new(protocol, mock_server_setup(Version::Lite(lite::Version::Lite01)));
603		let client = Client::new().with_versions(
604			[
605				Version::Lite(lite::Version::Lite03),
606				Version::Lite(lite::Version::Lite02),
607				Version::Lite(lite::Version::Lite01),
608				Version::Ietf(ietf::Version::Draft14),
609			]
610			.into(),
611		);
612
613		let _connection = client.connect(fake.clone()).await.unwrap();
614
615		// Verify the client setup was encoded using Draft14 framing (ALPN_LITE fallback path).
616		let mut setup_bytes = Bytes::from(fake.control_writes());
617		let setup = setup::Client::decode(&mut setup_bytes, Version::Ietf(ietf::Version::Draft14)).unwrap();
618		let advertised: Vec<Version> = setup.versions.iter().map(|v| Version::try_from(*v).unwrap()).collect();
619		assert_eq!(
620			advertised,
621			vec![
622				Version::Lite(lite::Version::Lite02),
623				Version::Lite(lite::Version::Lite01),
624				Version::Ietf(ietf::Version::Draft14),
625			]
626		);
627
628		// The first close comes from the lite connection driver.
629		// Any non-Version error here means SessionInfo decoded successfully
630		// after set_version(). This test cares about the SETUP framing
631		// fallback, not the specific close code. Cancel is what we'd see
632		// with no origin; RequiredExtension (or similar) is what an
633		// auto-created origin's first interaction with a Lite01 peer trips.
634		let (code, _) = fake.wait_for_first_close().await;
635		assert_ne!(code, Error::Version.to_code(), "SessionInfo failed to decode");
636	}
637
638	#[tokio::test(start_paused = true)]
639	async fn alpn_lite_falls_back_to_draft14_and_switches_version_post_setup() {
640		run_alpn_lite_fallback_case(Some(ALPN_LITE)).await;
641	}
642
643	#[tokio::test(start_paused = true)]
644	async fn no_alpn_falls_back_to_draft14_and_switches_version_post_setup() {
645		run_alpn_lite_fallback_case(None).await;
646	}
647
648	// This fake reports no send-rate estimate, so it never reaches the tokio timer in
649	// the bandwidth loop. A driver is NOT runtime-free in general; see the Async
650	// docs in lib.rs.
651	//
652	// The driver must hold no Session clone (the #2286 leak), so the transport still
653	// closes when the caller drops their last session handle, which is what lets a
654	// spawned driver task finish.
655	#[test]
656	fn driver_is_caller_polled_and_holds_no_session() {
657		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
658		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
659
660		let (session, mut driver) = futures::executor::block_on(client.connect(fake.clone())).unwrap();
661		assert_eq!(session.version(), Version::Lite(lite::Version::Lite04));
662
663		// An arbitrary waiter drives it kio-style: nothing was spawned onto a runtime.
664		assert!(driver.poll(&kio::Waiter::noop()).is_pending());
665
666		// The driver is also a plain future (stand in for spawning it).
667		let mut context = std::task::Context::from_waker(std::task::Waker::noop());
668		assert!(std::future::Future::poll(std::pin::Pin::new(&mut driver), &mut context).is_pending());
669
670		// The caller drops their only session clone, so the transport closes even
671		// though the driver is still alive.
672		drop(session);
673		assert_eq!(fake.state.close_events.lock().unwrap()[0].0, Error::Cancel.to_code());
674	}
675
676	// Clones share the connection: the transport closes on the LAST drop, and
677	// abort() closes it explicitly (first close wins).
678	#[test]
679	fn session_clones_share_the_close() {
680		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
681		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
682
683		let (session, _driver) = futures::executor::block_on(client.connect(fake.clone())).unwrap();
684		let clone = session.clone();
685
686		// One clone dropping does nothing while another is alive.
687		drop(session);
688		assert!(fake.state.close_events.lock().unwrap().is_empty());
689
690		clone.abort(Error::Cancel);
691		assert_eq!(fake.state.close_events.lock().unwrap()[0].0, Error::Cancel.to_code());
692
693		// The final drop is a no-op thanks to close-once.
694		drop(clone);
695		assert_eq!(fake.state.close_events.lock().unwrap().len(), 1);
696	}
697
698	// The send-bandwidth sampler lives inside the driver: it samples as soon as a
699	// consumer exists and keeps sampling on its interval. Paused tokio time makes
700	// the interval fire deterministically.
701	#[tokio::test(start_paused = true)]
702	async fn send_bandwidth_samples_while_the_driver_runs() {
703		let fake = FakeSession::new(Some(ALPN_LITE_04), Vec::new());
704		fake.set_send_rate(Some(1_000_000));
705
706		let client = Client::new().with_versions(Version::Lite(lite::Version::Lite04).into());
707		let (session, driver) = client.connect(fake.clone()).await.unwrap();
708		tokio::spawn(driver);
709
710		let mut bandwidth = session.send_bandwidth().expect("backend reports an estimate");
711		assert_eq!(bandwidth.changed().await.unwrap(), Some(1_000_000));
712
713		// A later change is picked up by the next interval tick.
714		fake.set_send_rate(Some(2_000_000));
715		assert_eq!(bandwidth.changed().await.unwrap(), Some(2_000_000));
716	}
717}