Skip to main content

moqtap_client/
dispatch.rs

1//! Unified multi-draft entry-point types.
2//!
3//! This module is the facade downstream consumers use to hold a MoQT
4//! connection without caring which draft was negotiated.
5//! It mirrors [`moqtap_codec::dispatch`]: one enum variant per enabled draft,
6//! gated on its feature flag.
7//!
8//! Four types live here:
9//!
10//! - `AnyConnection` — wraps a draft-specific `Connection`.
11//! - `AnyClientEvent` — wraps a draft-specific `ClientEvent`.
12//! - `AnyConnectionObserver` — a trait that receives `AnyClientEvent`s.
13//!   Attached to an `AnyConnection` via `AnyConnection::set_observer`,
14//!   which installs a per-draft adapter on the inner connection.
15//! - `AnyRequest` — what a request made through `AnyConnection` leaves the
16//!   caller holding. Drafts 07-16 put every request on the one control
17//!   stream and hand back only a request ID; from draft-17 on each request
18//!   owns a bidirectional stream, and the handle owns that stream.
19//!
20//! `AnyConnection` carries only the handful of protocol methods whose
21//! arguments can be reconciled across every draft (`subscribe`, `fetch`,
22//! `track_status`, `subscribe_namespace`). The rest differ too much in
23//! signature — match on the variant to reach them.
24
25use std::sync::Arc;
26
27use moqtap_codec::kvp::KeyValuePair;
28use moqtap_codec::version::DraftVersion;
29
30/// Generates the `AnyConnection` and `AnyClientEvent` enums plus the per-draft
31/// observer adapter, with one variant per enabled draft feature.
32macro_rules! dispatch_all {
33    (
34        $(
35            #[cfg(feature = $feat:literal)]
36            $variant:ident => $module:ident,
37        )+
38    ) => {
39        /// A MoQT client connection of any enabled draft version.
40        ///
41        /// Wraps the draft-specific `Connection` type. Methods common to all
42        /// drafts are forwarded; for draft-specific protocol calls, match on
43        /// the variant.
44        pub enum AnyConnection {
45            $(
46                #[cfg(feature = $feat)]
47                #[doc = concat!("A draft-", $feat, " connection.")]
48                $variant(crate::$module::connection::Connection),
49            )+
50        }
51
52        impl AnyConnection {
53            /// Returns the draft version this connection is using.
54            #[allow(unreachable_code)]
55            pub fn draft(&self) -> DraftVersion {
56                match self {
57                    $(
58                        #[cfg(feature = $feat)]
59                        Self::$variant(_) => DraftVersion::$variant,
60                    )+
61                    #[allow(unreachable_patterns)]
62                    _ => unreachable!("AnyConnection has no enabled variants"),
63                }
64            }
65
66            /// Attach an observer. The observer is adapted into the
67            /// draft-specific observer trait and installed on the inner
68            /// connection; events are forwarded as [`AnyClientEvent`].
69            ///
70            /// Replaces any previously attached observer.
71            #[allow(unused_variables)]
72            pub fn set_observer(&mut self, observer: Arc<dyn AnyConnectionObserver>) {
73                match self {
74                    $(
75                        #[cfg(feature = $feat)]
76                        Self::$variant(c) => {
77                            c.set_observer(Box::new($variant::Adapter(observer)));
78                        }
79                    )+
80                    #[allow(unreachable_patterns)]
81                    _ => {}
82                }
83            }
84
85            /// Remove any attached observer.
86            pub fn clear_observer(&mut self) {
87                match self {
88                    $(
89                        #[cfg(feature = $feat)]
90                        Self::$variant(c) => c.clear_observer(),
91                    )+
92                    #[allow(unreachable_patterns)]
93                    _ => {}
94                }
95            }
96
97            /// Close the connection with the given application error code
98            /// and reason.
99            #[allow(unused_variables)]
100            pub fn close(&self, code: u32, reason: &[u8]) {
101                match self {
102                    $(
103                        #[cfg(feature = $feat)]
104                        Self::$variant(c) => c.close(code, reason),
105                    )+
106                    #[allow(unreachable_patterns)]
107                    _ => {}
108                }
109            }
110        }
111
112        /// An event from a MoQT connection of any enabled draft version.
113        ///
114        /// Event shapes differ across drafts (e.g. draft-17's
115        /// `SubgroupObjectReceived` carries header types, while earlier
116        /// drafts carry decoded objects). Match on the variant to inspect
117        /// the draft-specific event.
118        #[non_exhaustive]
119        #[derive(Debug, Clone)]
120        pub enum AnyClientEvent {
121            $(
122                #[cfg(feature = $feat)]
123                #[doc = concat!("A draft-", $feat, " event.")]
124                $variant(crate::$module::event::ClientEvent),
125            )+
126        }
127
128        impl AnyClientEvent {
129            /// Returns the draft version this event belongs to.
130            #[allow(unreachable_code)]
131            pub fn draft(&self) -> DraftVersion {
132                match self {
133                    $(
134                        #[cfg(feature = $feat)]
135                        Self::$variant(_) => DraftVersion::$variant,
136                    )+
137                    #[allow(unreachable_patterns)]
138                    _ => unreachable!("AnyClientEvent has no enabled variants"),
139                }
140            }
141        }
142
143        // Per-draft adapter modules. Each holds an `Adapter` struct that
144        // implements the draft's `ConnectionObserver` trait by forwarding to
145        // an `AnyConnectionObserver`.
146        $(
147            #[cfg(feature = $feat)]
148            #[allow(non_snake_case)]
149            mod $variant {
150                use super::{AnyClientEvent, AnyConnectionObserver};
151                use std::sync::Arc;
152
153                pub(super) struct Adapter(pub(super) Arc<dyn AnyConnectionObserver>);
154
155                impl crate::$module::observer::ConnectionObserver for Adapter {
156                    fn on_event(&self, event: &crate::$module::event::ClientEvent) {
157                        self.0.on_event(&AnyClientEvent::$variant(event.clone()));
158                    }
159
160                    fn on_event_owned(&self, event: crate::$module::event::ClientEvent) {
161                        self.0.on_event(&AnyClientEvent::$variant(event));
162                    }
163                }
164            }
165        )+
166    };
167}
168
169dispatch_all! {
170    #[cfg(feature = "draft07")]
171    Draft07 => draft07,
172    #[cfg(feature = "draft08")]
173    Draft08 => draft08,
174    #[cfg(feature = "draft09")]
175    Draft09 => draft09,
176    #[cfg(feature = "draft10")]
177    Draft10 => draft10,
178    #[cfg(feature = "draft11")]
179    Draft11 => draft11,
180    #[cfg(feature = "draft12")]
181    Draft12 => draft12,
182    #[cfg(feature = "draft13")]
183    Draft13 => draft13,
184    #[cfg(feature = "draft14")]
185    Draft14 => draft14,
186    #[cfg(feature = "draft15")]
187    Draft15 => draft15,
188    #[cfg(feature = "draft16")]
189    Draft16 => draft16,
190    #[cfg(feature = "draft17")]
191    Draft17 => draft17,
192    #[cfg(feature = "draft18")]
193    Draft18 => draft18,
194    #[cfg(feature = "draft19")]
195    Draft19 => draft19,
196    #[cfg(feature = "draft20")]
197    Draft20 => draft20,
198}
199
200/// Draft-agnostic transport choice for [`AnyConnection::connect`].
201#[derive(Debug, Clone)]
202pub enum AnyTransportType {
203    /// Raw QUIC via quinn. The `addr` passed to `connect` should be `host:port`.
204    Quic,
205    /// WebTransport via wtransport. The `url` is the WebTransport endpoint.
206    WebTransport {
207        /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
208        url: String,
209    },
210}
211
212/// Draft-agnostic client configuration. The exact per-draft `ClientConfig`
213/// is constructed internally by [`AnyConnection::connect`] based on `draft`.
214///
215/// Fields that aren't meaningful for the selected draft are ignored:
216/// `additional_versions` is not carried by drafts 15–17 (single-version
217/// setup) and drafts 07–13 always offer their own draft first.
218#[derive(Debug, Clone)]
219pub struct AnyClientConfig {
220    /// Primary draft version for the connection.
221    pub draft: DraftVersion,
222    /// Additional draft versions to offer in CLIENT_SETUP.
223    pub additional_versions: Vec<DraftVersion>,
224    /// Transport type (QUIC or WebTransport).
225    pub transport: AnyTransportType,
226    /// Whether to skip TLS certificate verification (for testing).
227    pub skip_cert_verification: bool,
228    /// Custom CA certificates to trust (DER-encoded).
229    pub ca_certs: Vec<Vec<u8>>,
230    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
231    pub setup_parameters: Vec<KeyValuePair>,
232}
233
234/// Error returned by [`AnyConnection::connect`] and
235/// [`AnyConnection::recv_and_dispatch`]. Draft-specific errors are flattened
236/// to strings so callers don't have to branch on draft to inspect errors.
237#[derive(Debug, thiserror::Error)]
238#[error("{0}")]
239pub struct AnyConnectionError(pub String);
240
241/// Where a FETCH's range ends, said once in a way no draft can read two ways.
242///
243/// The drafts do not agree about what a number in an "end object" field means,
244/// and the disagreement is silent: draft-19 Section 10.13 defines `End
245/// Location` as "the last Object, plus 1; or 0 to indicate the entire Group",
246/// while draft-20 Sections 5.1.2 and 10.13 make the `LOCATION_FILTER` range
247/// "inclusive" at both ends and delete both conventions without a note in the
248/// change log. The same `end_object = 10` therefore asks for objects 0 through
249/// 9 on one draft and 0 through 10 on the other, and nothing on the wire says
250/// which was meant.
251///
252/// So this enum, not a number. [`FetchEnd::Object`] is the last Object the
253/// fetch covers and the range **holds** it; [`FetchEnd::EntireGroup`] is
254/// draft-19's `0` spelled out. [`AnyConnection::fetch`] converts to whichever
255/// the negotiated draft writes.
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum FetchEnd {
258    /// Through this Object ID, **inclusive** — it is the last Object the fetch
259    /// covers, and the range holds it.
260    ///
261    /// `Object(0)` is a range ending at Object 0, one object long if it also
262    /// starts there. It is not "the whole group"; that is
263    /// [`FetchEnd::EntireGroup`].
264    Object(u64),
265    /// Through the last Object of the end Group, however many it turns out to
266    /// hold.
267    ///
268    /// Drafts 14-19 write this as `End Location.Object = 0`. Draft-20 writes it
269    /// as a three-field `LOCATION_FILTER`, which Section 5.1.2 defines as
270    /// covering all Objects in the end Group.
271    EntireGroup,
272}
273
274/// The range one [`AnyConnection::fetch`] asks for.
275///
276/// Four numbers on drafts 14 through 19, a `LOCATION_FILTER` parameter on
277/// draft-20, and one meaning here. The end Group is **absolute** on both sides
278/// of that split — draft-20's `EndGroupDelta` is derived from it, not asked for
279/// — and the end Object is [`FetchEnd`], which is the whole point of the type.
280///
281/// # Porting a draft-19 call
282///
283/// A caller that wrote `end_object` as "the last Object plus one" writes
284/// [`FetchEnd::Object`] with the last Object, and one that wrote `0` for a whole
285/// group writes [`FetchEnd::EntireGroup`]. Both send exactly the bytes they sent
286/// before on drafts 14 through 19, and the same request on draft-20.
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub struct FetchRange {
289    /// The Group the range starts in.
290    pub start_group: u64,
291    /// The Object within `start_group` the range starts at, inclusive.
292    pub start_object: u64,
293    /// The Group the range ends in, absolute. Must not be below
294    /// `start_group`: draft-20 encodes it as an unsigned delta from the start
295    /// and has no way to say "backwards".
296    pub end_group: u64,
297    /// Where the range stops inside `end_group`.
298    pub end: FetchEnd,
299}
300
301impl FetchRange {
302    /// A range ending at `end_object` in `end_group`, **inclusive** — that
303    /// Object is fetched.
304    pub fn through_object(
305        start_group: u64,
306        start_object: u64,
307        end_group: u64,
308        end_object: u64,
309    ) -> Self {
310        Self { start_group, start_object, end_group, end: FetchEnd::Object(end_object) }
311    }
312
313    /// A range covering every Object of `end_group`, however many there are.
314    pub fn through_end_of_group(start_group: u64, start_object: u64, end_group: u64) -> Self {
315        Self { start_group, start_object, end_group, end: FetchEnd::EntireGroup }
316    }
317
318    /// A range holding exactly one Object.
319    ///
320    /// The shape an off-by-one is loudest in: a draft-19 encoder writing
321    /// `end_object = object` rather than `object + 1` asks for nothing at all,
322    /// and one that ports that arithmetic to draft-20 unchanged asks for two.
323    pub fn one_object(group: u64, object: u64) -> Self {
324        Self::through_object(group, object, group, object)
325    }
326
327    /// The `End Location.Object` drafts 14 through 19 carry inline in FETCH.
328    ///
329    /// Their field is "The end Location, plus 1. A Location.Object value of 0
330    /// means the entire group is requested." — draft-19 Section 10.12.1, and
331    /// drafts 14 through 18 in the same words. So [`FetchEnd::Object`] gains
332    /// one here and
333    /// [`FetchEnd::EntireGroup`] is `0`. **This is the only place that `+ 1`
334    /// lives**, which is what keeps it from reaching draft-20.
335    ///
336    /// # Errors
337    ///
338    /// `FetchEnd::Object(u64::MAX)` has no encoding in that field — the plus
339    /// one leaves the number space — and is the one range these drafts cannot
340    /// express that draft-20 can. Refused rather than wrapped to `0`, which
341    /// would silently ask for the whole group.
342    pub fn inline_end_object(&self) -> Result<u64, AnyConnectionError> {
343        match self.end {
344            FetchEnd::EntireGroup => Ok(0),
345            FetchEnd::Object(last) => last.checked_add(1).ok_or_else(|| {
346                AnyConnectionError(format!(
347                    "fetch: a range ending at Object {last} cannot be expressed on drafts 14 \
348                     through 19, whose End Location.Object is the last Object plus 1"
349                ))
350            }),
351        }
352    }
353
354    /// The draft-20 `LOCATION_FILTER` that carries this range.
355    ///
356    /// Draft-20 Section 10.13 deleted `Start Location` and `End Location` from
357    /// FETCH and moved the range into the parameter, whose ranges Section 5.1.2
358    /// calls inclusive. So [`FetchEnd::Object`] is written **as it stands** —
359    /// nothing here adds one — and [`FetchEnd::EntireGroup`] becomes the
360    /// three-field filter, which Section 5.1.2 defines as covering all Objects
361    /// in the end Group. The end Group travels as `EndGroupDelta`, "delta
362    /// encoded from StartGroup", so it is `end_group - start_group`.
363    ///
364    /// # Errors
365    ///
366    /// [`AnyConnectionError`] when `end_group` is below `start_group`: the
367    /// delta is unsigned and there is no such filter. Drafts 14 through 19 have
368    /// two absolute fields and would put such a range on the wire, where the
369    /// publisher answers it with `INVALID_RANGE`; the difference is where the
370    /// refusal happens, not whether the fetch is legal.
371    #[cfg(feature = "draft20")]
372    pub fn location_filter(
373        &self,
374    ) -> Result<crate::draft20::fill::LocationFilter, AnyConnectionError> {
375        use crate::draft20::fill::LocationFilter;
376
377        let delta = self.end_group.checked_sub(self.start_group).ok_or_else(|| {
378            AnyConnectionError(format!(
379                "fetch: a range from Group {} to Group {} runs backwards, and draft-20 \
380                 Section 5.1.2 encodes the end Group as an unsigned delta from the start",
381                self.start_group, self.end_group
382            ))
383        })?;
384        let filter = match self.end {
385            FetchEnd::EntireGroup => {
386                LocationFilter::range(self.start_group, self.start_object, delta)
387            }
388            FetchEnd::Object(last) => {
389                LocationFilter::range_to(self.start_group, self.start_object, delta, last)
390            }
391        };
392        filter.map_err(|e| AnyConnectionError(e.to_string()))
393    }
394}
395
396/// A request made through [`AnyConnection`], in whichever form the negotiated
397/// draft carries it.
398///
399/// Drafts 07-15 put every request on the single bidirectional control stream,
400/// so all a requester keeps is the request ID it was allocated. Draft-16 moved
401/// one request off it: Section 3.3 there names "two uses of bidirectional
402/// streams, the control stream, which begins with CLIENT_SETUP, and
403/// SUBSCRIBE_NAMESPACE", so a namespace subscription on that draft owns a
404/// stream and everything else does not. From draft-17 on every request does,
405/// and the stream *is* the correlation — responses on those drafts carry no
406/// request ID at all. The variants reflect that split rather than hiding it.
407///
408/// Hold on to this value for as long as the request is live. Dropping a
409/// per-request variant resets its stream, which the peer reads as a
410/// cancellation; dropping [`AnyRequest::ControlPlane`] does nothing, because
411/// there is no stream to reset.
412///
413/// The per-request variants are large — a `RequestStream` holds both halves of
414/// a QUIC stream — and `ControlPlane` is two small fields. That disparity is
415/// only visible when a single draft from 17 on is the only one enabled; with
416/// more than one, the largest variants are the same size as each other.
417/// Boxing them to close it would put an allocation on every request of every
418/// draft to flatter a build no released configuration uses.
419#[allow(clippy::large_enum_variant)]
420#[must_use = "dropping a request stream cancels the request"]
421pub enum AnyRequest {
422    /// Drafts 07-16: the request was written on the control stream and is
423    /// identified only by its request ID.
424    ControlPlane {
425        /// The request ID the endpoint allocated.
426        request_id: moqtap_codec::varint::VarInt,
427        /// The draft that allocated it.
428        draft: DraftVersion,
429    },
430    /// Draft-16: a namespace subscription, the one request on that draft that
431    /// owns a bidirectional stream. Every other draft-16 request comes back as
432    /// [`AnyRequest::ControlPlane`].
433    #[cfg(feature = "draft16")]
434    Draft16(crate::draft16::connection::NamespaceStream),
435    /// Draft-17: the request owns a bidirectional stream.
436    #[cfg(feature = "draft17")]
437    Draft17(crate::draft17::connection::RequestStream),
438    /// Draft-18: the request owns a bidirectional stream.
439    #[cfg(feature = "draft18")]
440    Draft18(crate::draft18::connection::RequestStream),
441    /// Draft-19: the request owns a bidirectional stream.
442    #[cfg(feature = "draft19")]
443    Draft19(crate::draft19::connection::RequestStream),
444    /// Draft-20: the request owns a bidirectional stream.
445    #[cfg(feature = "draft20")]
446    Draft20(crate::draft20::connection::RequestStream),
447}
448
449impl AnyRequest {
450    /// The request ID this request was allocated.
451    pub fn request_id(&self) -> moqtap_codec::varint::VarInt {
452        match self {
453            Self::ControlPlane { request_id, .. } => *request_id,
454            #[cfg(feature = "draft16")]
455            Self::Draft16(r) => r.request_id(),
456            #[cfg(feature = "draft17")]
457            Self::Draft17(r) => r.request_id(),
458            #[cfg(feature = "draft18")]
459            Self::Draft18(r) => r.request_id(),
460            #[cfg(feature = "draft19")]
461            Self::Draft19(r) => r.request_id(),
462            #[cfg(feature = "draft20")]
463            Self::Draft20(r) => r.request_id(),
464        }
465    }
466
467    /// The draft that carries this request.
468    pub fn draft(&self) -> DraftVersion {
469        match self {
470            Self::ControlPlane { draft, .. } => *draft,
471            #[cfg(feature = "draft16")]
472            Self::Draft16(r) => r.draft(),
473            #[cfg(feature = "draft17")]
474            Self::Draft17(r) => r.draft(),
475            #[cfg(feature = "draft18")]
476            Self::Draft18(r) => r.draft(),
477            #[cfg(feature = "draft19")]
478            Self::Draft19(r) => r.draft(),
479            #[cfg(feature = "draft20")]
480            Self::Draft20(r) => r.draft(),
481        }
482    }
483
484    /// The transport stream ID this request owns, or `None` on a draft that
485    /// carries requests on the shared control stream.
486    ///
487    /// This is the observable difference between the two variants: a caller
488    /// that needs to correlate a response by stream — which is the only
489    /// correlation drafts 17-19 offer — gets `Some` exactly when the draft
490    /// provides one.
491    pub fn stream_id(&self) -> Option<u64> {
492        match self {
493            Self::ControlPlane { .. } => None,
494            #[cfg(feature = "draft16")]
495            Self::Draft16(r) => Some(r.stream_id()),
496            #[cfg(feature = "draft17")]
497            Self::Draft17(r) => Some(r.stream_id()),
498            #[cfg(feature = "draft18")]
499            Self::Draft18(r) => Some(r.stream_id()),
500            #[cfg(feature = "draft19")]
501            Self::Draft19(r) => Some(r.stream_id()),
502            #[cfg(feature = "draft20")]
503            Self::Draft20(r) => Some(r.stream_id()),
504        }
505    }
506
507    /// Cancel the request by resetting its stream with `code`.
508    ///
509    /// Only a request that owns a stream can do this, which is a draft-16
510    /// namespace subscription and every request from draft-17 on. Cancelling a
511    /// request that lives on the control stream means sending a message
512    /// (UNSUBSCRIBE, FETCH_CANCEL, and so on), which needs the connection and
513    /// is therefore not reachable from the request handle alone. There this
514    /// refuses rather than silently doing nothing.
515    #[allow(unused_variables)]
516    pub fn cancel(&mut self, code: u64) -> Result<(), AnyConnectionError> {
517        match self {
518            Self::ControlPlane { draft, .. } => Err(AnyConnectionError(format!(
519                "cancel: draft {draft:?} carries this request on the control stream, so the \
520                 request handle has no stream to reset; send the draft's own cancellation \
521                 message instead"
522            ))),
523            #[cfg(feature = "draft16")]
524            Self::Draft16(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
525            #[cfg(feature = "draft17")]
526            Self::Draft17(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
527            #[cfg(feature = "draft18")]
528            Self::Draft18(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
529            #[cfg(feature = "draft19")]
530            Self::Draft19(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
531            #[cfg(feature = "draft20")]
532            Self::Draft20(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
533        }
534    }
535}
536
537impl AnyConnection {
538    /// Connect to a MoQT server using the requested draft. Builds the
539    /// draft-specific `ClientConfig` from the provided [`AnyClientConfig`]
540    /// and dispatches to the appropriate `Connection::connect`.
541    pub async fn connect(addr: &str, config: AnyClientConfig) -> Result<Self, AnyConnectionError> {
542        // Every arm of the match below is `#[cfg(feature = "draftNN")]`. A build
543        // with no draft feature enabled keeps only the catch-all, which never
544        // dials anything, so `addr` is genuinely unread in exactly that build.
545        #[cfg(not(any(
546            feature = "draft07",
547            feature = "draft08",
548            feature = "draft09",
549            feature = "draft10",
550            feature = "draft11",
551            feature = "draft12",
552            feature = "draft13",
553            feature = "draft14",
554            feature = "draft15",
555            feature = "draft16",
556            feature = "draft17",
557            feature = "draft18",
558            feature = "draft19",
559            feature = "draft20"
560        )))]
561        let _ = addr;
562        match config.draft {
563            #[cfg(feature = "draft07")]
564            DraftVersion::Draft07 => {
565                use crate::draft07::connection::{ClientConfig, Connection, TransportType};
566                let transport = match config.transport {
567                    AnyTransportType::Quic => TransportType::Quic,
568                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
569                };
570                let inner = ClientConfig {
571                    additional_versions: config.additional_versions,
572                    transport,
573                    skip_cert_verification: config.skip_cert_verification,
574                    ca_certs: config.ca_certs,
575                    setup_parameters: config.setup_parameters,
576                };
577                let c = Connection::connect(addr, inner)
578                    .await
579                    .map_err(|e| AnyConnectionError(e.to_string()))?;
580                Ok(AnyConnection::Draft07(c))
581            }
582            #[cfg(feature = "draft08")]
583            DraftVersion::Draft08 => {
584                use crate::draft08::connection::{ClientConfig, Connection, TransportType};
585                let transport = match config.transport {
586                    AnyTransportType::Quic => TransportType::Quic,
587                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
588                };
589                let inner = ClientConfig {
590                    additional_versions: config.additional_versions,
591                    transport,
592                    skip_cert_verification: config.skip_cert_verification,
593                    ca_certs: config.ca_certs,
594                    setup_parameters: config.setup_parameters,
595                };
596                let c = Connection::connect(addr, inner)
597                    .await
598                    .map_err(|e| AnyConnectionError(e.to_string()))?;
599                Ok(AnyConnection::Draft08(c))
600            }
601            #[cfg(feature = "draft09")]
602            DraftVersion::Draft09 => {
603                use crate::draft09::connection::{ClientConfig, Connection, TransportType};
604                let transport = match config.transport {
605                    AnyTransportType::Quic => TransportType::Quic,
606                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
607                };
608                let inner = ClientConfig {
609                    additional_versions: config.additional_versions,
610                    transport,
611                    skip_cert_verification: config.skip_cert_verification,
612                    ca_certs: config.ca_certs,
613                    setup_parameters: config.setup_parameters,
614                };
615                let c = Connection::connect(addr, inner)
616                    .await
617                    .map_err(|e| AnyConnectionError(e.to_string()))?;
618                Ok(AnyConnection::Draft09(c))
619            }
620            #[cfg(feature = "draft10")]
621            DraftVersion::Draft10 => {
622                use crate::draft10::connection::{ClientConfig, Connection, TransportType};
623                let transport = match config.transport {
624                    AnyTransportType::Quic => TransportType::Quic,
625                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
626                };
627                let inner = ClientConfig {
628                    additional_versions: config.additional_versions,
629                    transport,
630                    skip_cert_verification: config.skip_cert_verification,
631                    ca_certs: config.ca_certs,
632                    setup_parameters: config.setup_parameters,
633                };
634                let c = Connection::connect(addr, inner)
635                    .await
636                    .map_err(|e| AnyConnectionError(e.to_string()))?;
637                Ok(AnyConnection::Draft10(c))
638            }
639            #[cfg(feature = "draft11")]
640            DraftVersion::Draft11 => {
641                use crate::draft11::connection::{ClientConfig, Connection, TransportType};
642                let transport = match config.transport {
643                    AnyTransportType::Quic => TransportType::Quic,
644                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
645                };
646                let inner = ClientConfig {
647                    additional_versions: config.additional_versions,
648                    transport,
649                    skip_cert_verification: config.skip_cert_verification,
650                    ca_certs: config.ca_certs,
651                    setup_parameters: config.setup_parameters,
652                };
653                let c = Connection::connect(addr, inner)
654                    .await
655                    .map_err(|e| AnyConnectionError(e.to_string()))?;
656                Ok(AnyConnection::Draft11(c))
657            }
658            #[cfg(feature = "draft12")]
659            DraftVersion::Draft12 => {
660                use crate::draft12::connection::{ClientConfig, Connection, TransportType};
661                let transport = match config.transport {
662                    AnyTransportType::Quic => TransportType::Quic,
663                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
664                };
665                let inner = ClientConfig {
666                    additional_versions: config.additional_versions,
667                    transport,
668                    skip_cert_verification: config.skip_cert_verification,
669                    ca_certs: config.ca_certs,
670                    setup_parameters: config.setup_parameters,
671                };
672                let c = Connection::connect(addr, inner)
673                    .await
674                    .map_err(|e| AnyConnectionError(e.to_string()))?;
675                Ok(AnyConnection::Draft12(c))
676            }
677            #[cfg(feature = "draft13")]
678            DraftVersion::Draft13 => {
679                use crate::draft13::connection::{ClientConfig, Connection, TransportType};
680                let transport = match config.transport {
681                    AnyTransportType::Quic => TransportType::Quic,
682                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
683                };
684                let inner = ClientConfig {
685                    additional_versions: config.additional_versions,
686                    transport,
687                    skip_cert_verification: config.skip_cert_verification,
688                    ca_certs: config.ca_certs,
689                    setup_parameters: config.setup_parameters,
690                };
691                let c = Connection::connect(addr, inner)
692                    .await
693                    .map_err(|e| AnyConnectionError(e.to_string()))?;
694                Ok(AnyConnection::Draft13(c))
695            }
696            #[cfg(feature = "draft14")]
697            DraftVersion::Draft14 => {
698                use crate::draft14::connection::{ClientConfig, Connection, TransportType};
699                let transport = match config.transport {
700                    AnyTransportType::Quic => TransportType::Quic,
701                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
702                };
703                let inner = ClientConfig {
704                    draft: config.draft,
705                    additional_versions: config.additional_versions,
706                    transport,
707                    skip_cert_verification: config.skip_cert_verification,
708                    ca_certs: config.ca_certs,
709                    setup_parameters: config.setup_parameters,
710                };
711                let c = Connection::connect(addr, inner)
712                    .await
713                    .map_err(|e| AnyConnectionError(e.to_string()))?;
714                Ok(AnyConnection::Draft14(c))
715            }
716            #[cfg(feature = "draft15")]
717            DraftVersion::Draft15 => {
718                use crate::draft15::connection::{ClientConfig, Connection, TransportType};
719                let transport = match config.transport {
720                    AnyTransportType::Quic => TransportType::Quic,
721                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
722                };
723                let inner = ClientConfig {
724                    draft: config.draft,
725                    transport,
726                    skip_cert_verification: config.skip_cert_verification,
727                    ca_certs: config.ca_certs,
728                    setup_parameters: config.setup_parameters,
729                };
730                let c = Connection::connect(addr, inner)
731                    .await
732                    .map_err(|e| AnyConnectionError(e.to_string()))?;
733                Ok(AnyConnection::Draft15(c))
734            }
735            #[cfg(feature = "draft16")]
736            DraftVersion::Draft16 => {
737                use crate::draft16::connection::{ClientConfig, Connection, TransportType};
738                let transport = match config.transport {
739                    AnyTransportType::Quic => TransportType::Quic,
740                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
741                };
742                let inner = ClientConfig {
743                    draft: config.draft,
744                    transport,
745                    skip_cert_verification: config.skip_cert_verification,
746                    ca_certs: config.ca_certs,
747                    setup_parameters: config.setup_parameters,
748                };
749                let c = Connection::connect(addr, inner)
750                    .await
751                    .map_err(|e| AnyConnectionError(e.to_string()))?;
752                Ok(AnyConnection::Draft16(c))
753            }
754            #[cfg(feature = "draft17")]
755            DraftVersion::Draft17 => {
756                use crate::draft17::connection::{ClientConfig, Connection, TransportType};
757                let transport = match config.transport {
758                    AnyTransportType::Quic => TransportType::Quic,
759                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
760                };
761                let inner = ClientConfig {
762                    draft: config.draft,
763                    transport,
764                    skip_cert_verification: config.skip_cert_verification,
765                    ca_certs: config.ca_certs,
766                    setup_parameters: config.setup_parameters,
767                };
768                let c = Connection::connect(addr, inner)
769                    .await
770                    .map_err(|e| AnyConnectionError(e.to_string()))?;
771                Ok(AnyConnection::Draft17(c))
772            }
773            #[cfg(feature = "draft18")]
774            DraftVersion::Draft18 => {
775                use crate::draft18::connection::{ClientConfig, Connection, TransportType};
776                let transport = match config.transport {
777                    AnyTransportType::Quic => TransportType::Quic,
778                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
779                };
780                let inner = ClientConfig {
781                    draft: config.draft,
782                    transport,
783                    skip_cert_verification: config.skip_cert_verification,
784                    ca_certs: config.ca_certs,
785                    setup_parameters: config.setup_parameters,
786                };
787                let c = Connection::connect(addr, inner)
788                    .await
789                    .map_err(|e| AnyConnectionError(e.to_string()))?;
790                Ok(AnyConnection::Draft18(c))
791            }
792            #[cfg(feature = "draft19")]
793            DraftVersion::Draft19 => {
794                use crate::draft19::connection::{ClientConfig, Connection, TransportType};
795                let transport = match config.transport {
796                    AnyTransportType::Quic => TransportType::Quic,
797                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
798                };
799                let inner = ClientConfig {
800                    draft: config.draft,
801                    transport,
802                    skip_cert_verification: config.skip_cert_verification,
803                    ca_certs: config.ca_certs,
804                    setup_parameters: config.setup_parameters,
805                };
806                let c = Connection::connect(addr, inner)
807                    .await
808                    .map_err(|e| AnyConnectionError(e.to_string()))?;
809                Ok(AnyConnection::Draft19(c))
810            }
811            #[cfg(feature = "draft20")]
812            DraftVersion::Draft20 => {
813                use crate::draft20::connection::{ClientConfig, Connection, TransportType};
814                let transport = match config.transport {
815                    AnyTransportType::Quic => TransportType::Quic,
816                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
817                };
818                let inner = ClientConfig {
819                    draft: config.draft,
820                    transport,
821                    skip_cert_verification: config.skip_cert_verification,
822                    ca_certs: config.ca_certs,
823                    setup_parameters: config.setup_parameters,
824                };
825                let c = Connection::connect(addr, inner)
826                    .await
827                    .map_err(|e| AnyConnectionError(e.to_string()))?;
828                Ok(AnyConnection::Draft20(c))
829            }
830            #[allow(unreachable_patterns)]
831            other => Err(AnyConnectionError(format!("draft {other:?} not enabled in this build",))),
832        }
833    }
834
835    /// Read and dispatch one control message on the active draft. Draft-specific
836    /// control-message return values are discarded because event delivery goes
837    /// through the attached observer; callers only care about success/failure.
838    pub async fn recv_and_dispatch(&mut self) -> Result<(), AnyConnectionError> {
839        match self {
840            #[cfg(feature = "draft07")]
841            Self::Draft07(c) => c
842                .recv_and_dispatch()
843                .await
844                .map(|_| ())
845                .map_err(|e| AnyConnectionError(e.to_string())),
846            #[cfg(feature = "draft08")]
847            Self::Draft08(c) => c
848                .recv_and_dispatch()
849                .await
850                .map(|_| ())
851                .map_err(|e| AnyConnectionError(e.to_string())),
852            #[cfg(feature = "draft09")]
853            Self::Draft09(c) => c
854                .recv_and_dispatch()
855                .await
856                .map(|_| ())
857                .map_err(|e| AnyConnectionError(e.to_string())),
858            #[cfg(feature = "draft10")]
859            Self::Draft10(c) => c
860                .recv_and_dispatch()
861                .await
862                .map(|_| ())
863                .map_err(|e| AnyConnectionError(e.to_string())),
864            #[cfg(feature = "draft11")]
865            Self::Draft11(c) => c
866                .recv_and_dispatch()
867                .await
868                .map(|_| ())
869                .map_err(|e| AnyConnectionError(e.to_string())),
870            #[cfg(feature = "draft12")]
871            Self::Draft12(c) => c
872                .recv_and_dispatch()
873                .await
874                .map(|_| ())
875                .map_err(|e| AnyConnectionError(e.to_string())),
876            #[cfg(feature = "draft13")]
877            Self::Draft13(c) => c
878                .recv_and_dispatch()
879                .await
880                .map(|_| ())
881                .map_err(|e| AnyConnectionError(e.to_string())),
882            #[cfg(feature = "draft14")]
883            Self::Draft14(c) => c
884                .recv_and_dispatch()
885                .await
886                .map(|_| ())
887                .map_err(|e| AnyConnectionError(e.to_string())),
888            #[cfg(feature = "draft15")]
889            Self::Draft15(c) => c
890                .recv_and_dispatch()
891                .await
892                .map(|_| ())
893                .map_err(|e| AnyConnectionError(e.to_string())),
894            #[cfg(feature = "draft16")]
895            Self::Draft16(c) => c
896                .recv_and_dispatch()
897                .await
898                .map(|_| ())
899                .map_err(|e| AnyConnectionError(e.to_string())),
900            #[cfg(feature = "draft17")]
901            Self::Draft17(c) => c
902                .recv_and_dispatch()
903                .await
904                .map(|_| ())
905                .map_err(|e| AnyConnectionError(e.to_string())),
906            #[cfg(feature = "draft18")]
907            Self::Draft18(c) => c
908                .recv_and_dispatch()
909                .await
910                .map(|_| ())
911                .map_err(|e| AnyConnectionError(e.to_string())),
912            #[cfg(feature = "draft19")]
913            Self::Draft19(c) => c
914                .recv_and_dispatch()
915                .await
916                .map(|_| ())
917                .map_err(|e| AnyConnectionError(e.to_string())),
918            #[cfg(feature = "draft20")]
919            Self::Draft20(c) => c
920                .recv_and_dispatch()
921                .await
922                .map(|_| ())
923                .map_err(|e| AnyConnectionError(e.to_string())),
924            #[allow(unreachable_patterns)]
925            _ => Err(AnyConnectionError("AnyConnection has no enabled variants".into())),
926        }
927    }
928
929    // ── Unified control-message helpers ──────────────────────────────────
930    //
931    // Draft-agnostic shorthands. Each dispatches to the active variant and
932    // defaults fields not expressible in the unified shape; drafts that
933    // lack the operation return an `AnyConnectionError`. Match on the
934    // variant directly when full per-draft control is needed.
935
936    /// Send an UNSUBSCRIBE for the given request ID. Drafts 07 through 16.
937    ///
938    /// # Drafts 17 through 20 have no such message
939    ///
940    /// Draft-17 deleted UNSUBSCRIBE, and drafts 18, 19 and 20 keep it deleted:
941    /// a subscriber ends a subscription by **resetting its request stream**,
942    /// which is [`AnyRequest::cancel`], or waits for PUBLISH_DONE. So the error
943    /// those four return is not a gap to be filled later — there is nothing to
944    /// wire — and a caller reaching for it on one of them wants `cancel` on the
945    /// handle `subscribe` returned.
946    #[allow(unused_variables)]
947    pub async fn unsubscribe(
948        &mut self,
949        request_id: moqtap_codec::varint::VarInt,
950    ) -> Result<(), AnyConnectionError> {
951        match self {
952            #[cfg(feature = "draft07")]
953            Self::Draft07(c) => {
954                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
955            }
956            #[cfg(feature = "draft08")]
957            Self::Draft08(c) => {
958                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
959            }
960            #[cfg(feature = "draft09")]
961            Self::Draft09(c) => {
962                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
963            }
964            #[cfg(feature = "draft10")]
965            Self::Draft10(c) => {
966                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
967            }
968            #[cfg(feature = "draft11")]
969            Self::Draft11(c) => {
970                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
971            }
972            #[cfg(feature = "draft12")]
973            Self::Draft12(c) => {
974                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
975            }
976            #[cfg(feature = "draft13")]
977            Self::Draft13(c) => {
978                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
979            }
980            #[cfg(feature = "draft14")]
981            Self::Draft14(c) => {
982                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
983            }
984            #[cfg(feature = "draft15")]
985            Self::Draft15(c) => {
986                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
987            }
988            #[cfg(feature = "draft16")]
989            Self::Draft16(c) => {
990                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
991            }
992            #[allow(unreachable_patterns)]
993            other => Err(AnyConnectionError(format!(
994                "unsubscribe: not yet wired up for draft {:?} via AnyConnection",
995                other.draft()
996            ))),
997        }
998    }
999
1000    /// Send a SUBSCRIBE with the given filter, priority, and group order.
1001    /// Supported on drafts 12 through 20. Drafts 15 onward carry
1002    /// priority/order/filter as parameters rather than fields; this helper
1003    /// passes an empty parameter list, so on those drafts all three take the
1004    /// protocol default and the three arguments here are ignored.
1005    ///
1006    /// There is no range to get wrong: a subscription with no filter is one
1007    /// that starts where the draft says it starts, and nothing is converted.
1008    /// A draft-20 caller that wants a Location filter, a fill, or anything else
1009    /// from Section 10.2 reaches
1010    /// [`draft20::connection::Connection::subscribe`](crate::draft20::connection::Connection::subscribe)
1011    /// through the variant with the parameters it wants.
1012    ///
1013    /// The returned [`AnyRequest`] must be held while the request is live: on
1014    /// drafts 17-20 it owns the bidirectional stream the request went out on
1015    /// and dropping it cancels the subscription. See [`AnyRequest`] for how
1016    /// the two kinds of handle differ.
1017    #[allow(unused_variables)]
1018    pub async fn subscribe(
1019        &mut self,
1020        namespace: moqtap_codec::types::TrackNamespace,
1021        track_name: Vec<u8>,
1022        subscriber_priority: u8,
1023        group_order: moqtap_codec::types::GroupOrder,
1024        filter_type: moqtap_codec::types::FilterType,
1025    ) -> Result<AnyRequest, AnyConnectionError> {
1026        // Only the draft-12 arm below converts `filter_type` by hand: draft-12
1027        // draws that field as a variable-length integer, where the drafts on
1028        // either side of it take the typed value straight through. So the
1029        // import is dead outside that build.
1030        #[cfg(feature = "draft12")]
1031        use moqtap_codec::varint::VarInt;
1032        let draft = self.draft();
1033        match self {
1034            #[cfg(feature = "draft12")]
1035            Self::Draft12(c) => {
1036                let ft = VarInt::from_u64(filter_type as u64)
1037                    .map_err(|e| AnyConnectionError(e.to_string()))?;
1038                c.subscribe(namespace, track_name, subscriber_priority, group_order, ft, Vec::new())
1039                    .await
1040                    .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1041                    .map_err(|e| AnyConnectionError(e.to_string()))
1042            }
1043            #[cfg(feature = "draft13")]
1044            Self::Draft13(c) => c
1045                .subscribe(
1046                    namespace,
1047                    track_name,
1048                    subscriber_priority,
1049                    group_order,
1050                    filter_type,
1051                    Vec::new(),
1052                )
1053                .await
1054                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1055                .map_err(|e| AnyConnectionError(e.to_string())),
1056            #[cfg(feature = "draft14")]
1057            Self::Draft14(c) => c
1058                .subscribe(
1059                    namespace,
1060                    track_name,
1061                    subscriber_priority,
1062                    group_order,
1063                    filter_type,
1064                    Vec::new(),
1065                )
1066                .await
1067                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1068                .map_err(|e| AnyConnectionError(e.to_string())),
1069            #[cfg(feature = "draft15")]
1070            Self::Draft15(c) => c
1071                .subscribe(namespace, track_name, Vec::new())
1072                .await
1073                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1074                .map_err(|e| AnyConnectionError(e.to_string())),
1075            #[cfg(feature = "draft16")]
1076            Self::Draft16(c) => c
1077                .subscribe(namespace, track_name, Vec::new())
1078                .await
1079                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1080                .map_err(|e| AnyConnectionError(e.to_string())),
1081            #[cfg(feature = "draft17")]
1082            Self::Draft17(c) => c
1083                .subscribe(namespace, track_name, Vec::new())
1084                .await
1085                .map(AnyRequest::Draft17)
1086                .map_err(|e| AnyConnectionError(e.to_string())),
1087            #[cfg(feature = "draft18")]
1088            Self::Draft18(c) => c
1089                .subscribe(namespace, track_name, Vec::new())
1090                .await
1091                .map(AnyRequest::Draft18)
1092                .map_err(|e| AnyConnectionError(e.to_string())),
1093            #[cfg(feature = "draft19")]
1094            Self::Draft19(c) => c
1095                .subscribe(namespace, track_name, Vec::new())
1096                .await
1097                .map(AnyRequest::Draft19)
1098                .map_err(|e| AnyConnectionError(e.to_string())),
1099            #[cfg(feature = "draft20")]
1100            Self::Draft20(c) => c
1101                .subscribe(namespace, track_name, Vec::new())
1102                .await
1103                .map(AnyRequest::Draft20)
1104                .map_err(|e| AnyConnectionError(e.to_string())),
1105            #[allow(unreachable_patterns)]
1106            other => Err(AnyConnectionError(format!(
1107                "subscribe: not yet wired up for draft {:?} via AnyConnection",
1108                other.draft()
1109            ))),
1110        }
1111    }
1112
1113    /// Send a standalone FETCH for `range`. Wired on drafts 14 through 20;
1114    /// every earlier draft returns the error at the end of the match.
1115    ///
1116    /// # What the range means here
1117    ///
1118    /// **[`FetchEnd::Object`] is the last Object the fetch covers, and the
1119    /// range holds it. [`FetchEnd::EntireGroup`] covers the whole end Group.
1120    /// `end_group` is absolute.** That is the whole contract, and it is stated
1121    /// in [`FetchEnd`] as well because it is the one thing about this call a
1122    /// caller can get wrong without being told.
1123    ///
1124    /// The drafts do not agree, which is why the argument is a
1125    /// [`FetchRange`] rather than four numbers. Drafts 14 through 19 carry
1126    /// `Start Location` and `End Location` inline in FETCH, and all six word
1127    /// the end alike — draft-19 Section 10.12.1: "The end Location, plus 1. A
1128    /// Location.Object value of 0 means the entire group is requested." The
1129    /// section number moves between drafts; the sentence does not.
1130    /// Draft-20 Section 10.13 deleted both fields and
1131    /// moved the range into the `LOCATION_FILTER` parameter, whose ranges
1132    /// Section 5.1.2 calls **inclusive** — the `+ 1` and the `0`-means-whole-
1133    /// group convention are both gone, and neither deletion is in the draft's
1134    /// own change log. A single `end_object: u64` at this boundary would have
1135    /// meant one of those two things and looked like the other.
1136    ///
1137    /// The conversion is [`FetchRange::inline_end_object`] for the first group
1138    /// and [`FetchRange::location_filter`] for draft-20. **The `+ 1` exists in
1139    /// exactly one place**, the first of those, so it cannot reach draft-20 by
1140    /// being ported.
1141    ///
1142    /// # What this does not carry
1143    ///
1144    /// An empty parameter list, on every draft. Draft-14's subscriber priority
1145    /// and group order are fields of its FETCH and of no later draft's, so they
1146    /// are sent as they always were. Reach a draft's own `Connection::fetch`
1147    /// through the variant for anything past a plain range.
1148    ///
1149    /// # Errors
1150    ///
1151    /// A range the negotiated draft cannot express, before anything is written:
1152    /// `FetchEnd::Object(u64::MAX)` on drafts 14 through 19, and an `end_group`
1153    /// below `start_group` on draft-20. See the two conversions for why each is
1154    /// inexpressible rather than merely unusual.
1155    ///
1156    /// The returned [`AnyRequest`] must be held while the request is live: on
1157    /// drafts 17-20 it owns the bidirectional stream the request went out on
1158    /// and dropping it cancels the fetch.
1159    #[allow(unused_variables)]
1160    pub async fn fetch(
1161        &mut self,
1162        namespace: moqtap_codec::types::TrackNamespace,
1163        track_name: Vec<u8>,
1164        range: FetchRange,
1165    ) -> Result<AnyRequest, AnyConnectionError> {
1166        // The three fields drafts 14 through 19 carry unchanged, built once for
1167        // whichever arm runs. `from_u64_moqt` rather than `from_u64` because
1168        // MoQT's varint reaches the full 64-bit range (Section 1.4.1) and the
1169        // newtype *is* the value: a `VarInt` a caller could have handed the old
1170        // four-argument form is the same `VarInt` this makes, so every draft
1171        // that was wired before writes the bytes it wrote before. The fourth
1172        // field is the one that differs, and it is built inside each arm from
1173        // `inline_end_object`.
1174        let start_group = moqtap_codec::varint::VarInt::from_u64_moqt(range.start_group);
1175        let start_object = moqtap_codec::varint::VarInt::from_u64_moqt(range.start_object);
1176        let end_group = moqtap_codec::varint::VarInt::from_u64_moqt(range.end_group);
1177        // The fourth field, which is the one that differs: draft-19 Section
1178        // 10.13 makes `End Location.Object` the last Object plus 1, or 0 for
1179        // the whole Group. Built here so the six arms that need it stay
1180        // identical, and lazily so that draft-20 — which can express a range
1181        // those six cannot — is not refused on their behalf.
1182        let inline_end_object = || -> Result<moqtap_codec::varint::VarInt, AnyConnectionError> {
1183            Ok(moqtap_codec::varint::VarInt::from_u64_moqt(range.inline_end_object()?))
1184        };
1185        let draft = self.draft();
1186        match self {
1187            #[cfg(feature = "draft14")]
1188            Self::Draft14(c) => c
1189                .fetch(
1190                    namespace,
1191                    track_name,
1192                    // The priority and the order are fields of draft-14's FETCH
1193                    // and of no later draft's, so this entry point does not
1194                    // carry them and sends what it always sent. The end of the
1195                    // range it does carry, and used to drop here.
1196                    128,
1197                    moqtap_codec::types::GroupOrder::Ascending,
1198                    start_group,
1199                    start_object,
1200                    end_group,
1201                    inline_end_object()?,
1202                    Vec::new(),
1203                )
1204                .await
1205                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1206                .map_err(|e| AnyConnectionError(e.to_string())),
1207            #[cfg(feature = "draft15")]
1208            Self::Draft15(c) => c
1209                .fetch(
1210                    namespace,
1211                    track_name,
1212                    start_group,
1213                    start_object,
1214                    end_group,
1215                    inline_end_object()?,
1216                    Vec::new(),
1217                )
1218                .await
1219                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1220                .map_err(|e| AnyConnectionError(e.to_string())),
1221            #[cfg(feature = "draft16")]
1222            Self::Draft16(c) => c
1223                .fetch(
1224                    namespace,
1225                    track_name,
1226                    start_group,
1227                    start_object,
1228                    end_group,
1229                    inline_end_object()?,
1230                    Vec::new(),
1231                )
1232                .await
1233                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1234                .map_err(|e| AnyConnectionError(e.to_string())),
1235            #[cfg(feature = "draft17")]
1236            Self::Draft17(c) => c
1237                .fetch(
1238                    namespace,
1239                    track_name,
1240                    start_group,
1241                    start_object,
1242                    end_group,
1243                    inline_end_object()?,
1244                    Vec::new(),
1245                )
1246                .await
1247                .map(AnyRequest::Draft17)
1248                .map_err(|e| AnyConnectionError(e.to_string())),
1249            #[cfg(feature = "draft18")]
1250            Self::Draft18(c) => c
1251                .fetch(
1252                    namespace,
1253                    track_name,
1254                    start_group,
1255                    start_object,
1256                    end_group,
1257                    inline_end_object()?,
1258                    Vec::new(),
1259                )
1260                .await
1261                .map(AnyRequest::Draft18)
1262                .map_err(|e| AnyConnectionError(e.to_string())),
1263            #[cfg(feature = "draft19")]
1264            Self::Draft19(c) => c
1265                .fetch(
1266                    namespace,
1267                    track_name,
1268                    start_group,
1269                    start_object,
1270                    end_group,
1271                    inline_end_object()?,
1272                    Vec::new(),
1273                )
1274                .await
1275                .map(AnyRequest::Draft19)
1276                .map_err(|e| AnyConnectionError(e.to_string())),
1277            #[cfg(feature = "draft20")]
1278            Self::Draft20(c) => {
1279                // Draft-20 Section 10.13 has no location fields to fill in:
1280                // `fetch_range` puts the whole range in the `LOCATION_FILTER`
1281                // parameter, at the position ascending Parameter Type order
1282                // requires. Nothing here adds one to the end — Section 5.1.2
1283                // makes the filter's range inclusive, and the `+ 1` the six
1284                // arms above apply lives in `inline_end_object` alone.
1285                let filter = range.location_filter()?;
1286                c.fetch_range(namespace, track_name, &filter, Vec::new())
1287                    .await
1288                    .map(AnyRequest::Draft20)
1289                    .map_err(|e| AnyConnectionError(e.to_string()))
1290            }
1291            #[allow(unreachable_patterns)]
1292            other => Err(AnyConnectionError(format!(
1293                "fetch: not yet wired up for draft {:?} via AnyConnection",
1294                other.draft()
1295            ))),
1296        }
1297    }
1298
1299    /// Send a TRACK_STATUS query for the given track. Supported on drafts 14
1300    /// through 20. From draft-15 on, passes an empty parameter list.
1301    ///
1302    /// The returned [`AnyRequest`] must be held until the answer arrives: on
1303    /// drafts 17-20 it owns the bidirectional stream the query went out on and
1304    /// dropping it cancels the query.
1305    #[allow(unused_variables)]
1306    pub async fn track_status(
1307        &mut self,
1308        namespace: moqtap_codec::types::TrackNamespace,
1309        track_name: Vec<u8>,
1310    ) -> Result<AnyRequest, AnyConnectionError> {
1311        let draft = self.draft();
1312        match self {
1313            #[cfg(feature = "draft14")]
1314            Self::Draft14(c) => c
1315                .track_status(
1316                    namespace,
1317                    track_name,
1318                    // Draft-14 words TRACK_STATUS like a SUBSCRIBE and no
1319                    // later draft does, so these four are sent as they always
1320                    // were rather than widening an entry point shared with six
1321                    // drafts that have no such fields.
1322                    128,
1323                    moqtap_codec::types::GroupOrder::Ascending,
1324                    moqtap_codec::types::Forward::Forward,
1325                    moqtap_codec::types::FilterType::LargestObject,
1326                    Vec::new(),
1327                )
1328                .await
1329                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1330                .map_err(|e| AnyConnectionError(e.to_string())),
1331            #[cfg(feature = "draft15")]
1332            Self::Draft15(c) => c
1333                .track_status(namespace, track_name, Vec::new())
1334                .await
1335                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1336                .map_err(|e| AnyConnectionError(e.to_string())),
1337            #[cfg(feature = "draft16")]
1338            Self::Draft16(c) => c
1339                .track_status(namespace, track_name, Vec::new())
1340                .await
1341                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1342                .map_err(|e| AnyConnectionError(e.to_string())),
1343            #[cfg(feature = "draft17")]
1344            Self::Draft17(c) => c
1345                .track_status(namespace, track_name, Vec::new())
1346                .await
1347                .map(AnyRequest::Draft17)
1348                .map_err(|e| AnyConnectionError(e.to_string())),
1349            #[cfg(feature = "draft18")]
1350            Self::Draft18(c) => c
1351                .track_status(namespace, track_name, Vec::new())
1352                .await
1353                .map(AnyRequest::Draft18)
1354                .map_err(|e| AnyConnectionError(e.to_string())),
1355            #[cfg(feature = "draft19")]
1356            Self::Draft19(c) => c
1357                .track_status(namespace, track_name, Vec::new())
1358                .await
1359                .map(AnyRequest::Draft19)
1360                .map_err(|e| AnyConnectionError(e.to_string())),
1361            #[cfg(feature = "draft20")]
1362            Self::Draft20(c) => c
1363                .track_status(namespace, track_name, Vec::new())
1364                .await
1365                .map(AnyRequest::Draft20)
1366                .map_err(|e| AnyConnectionError(e.to_string())),
1367            #[allow(unreachable_patterns)]
1368            other => Err(AnyConnectionError(format!(
1369                "track_status: not yet wired up for draft {:?} via AnyConnection",
1370                other.draft()
1371            ))),
1372        }
1373    }
1374
1375    /// Send a SUBSCRIBE_NAMESPACE (or SUBSCRIBE_ANNOUNCES on drafts 11–12).
1376    /// Supported on drafts 11 through 20. Drafts 16 and 17 pass default
1377    /// subscribe options; every draft from 12 on passes an empty parameter
1378    /// list.
1379    ///
1380    /// From draft-18 this is the renumbered SUBSCRIBE_NAMESPACE (0x50), which
1381    /// asks for NAMESPACE and NAMESPACE_DONE only. SUBSCRIBE_TRACKS, the other
1382    /// half of the draft-18 split, has no entry point here — reach a draft's
1383    /// own `Connection::subscribe_tracks` through the variant.
1384    ///
1385    /// The returned [`AnyRequest`] must be held while the request is live: on
1386    /// drafts 17-20 it owns the bidirectional stream the request went out on
1387    /// and dropping it cancels the namespace subscription.
1388    #[allow(unused_variables)]
1389    pub async fn subscribe_namespace(
1390        &mut self,
1391        namespace_prefix: moqtap_codec::types::TrackNamespace,
1392    ) -> Result<AnyRequest, AnyConnectionError> {
1393        // Only the draft-16 and draft-17 arms below build a subscribe-options
1394        // varint; the other drafts' wrappers take no such argument, so the
1395        // import is dead outside those two builds.
1396        #[cfg(any(feature = "draft16", feature = "draft17"))]
1397        use moqtap_codec::varint::VarInt;
1398        let draft = self.draft();
1399        match self {
1400            #[cfg(feature = "draft11")]
1401            Self::Draft11(c) => c
1402                .subscribe_announces(namespace_prefix)
1403                .await
1404                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1405                .map_err(|e| AnyConnectionError(e.to_string())),
1406            #[cfg(feature = "draft12")]
1407            Self::Draft12(c) => c
1408                .subscribe_announces(namespace_prefix, Vec::new())
1409                .await
1410                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1411                .map_err(|e| AnyConnectionError(e.to_string())),
1412            #[cfg(feature = "draft13")]
1413            Self::Draft13(c) => c
1414                .subscribe_namespace(namespace_prefix, Vec::new())
1415                .await
1416                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1417                .map_err(|e| AnyConnectionError(e.to_string())),
1418            #[cfg(feature = "draft14")]
1419            Self::Draft14(c) => c
1420                .subscribe_namespace(namespace_prefix, Vec::new())
1421                .await
1422                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1423                .map_err(|e| AnyConnectionError(e.to_string())),
1424            #[cfg(feature = "draft15")]
1425            Self::Draft15(c) => c
1426                .subscribe_namespace(namespace_prefix, Vec::new())
1427                .await
1428                .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1429                .map_err(|e| AnyConnectionError(e.to_string())),
1430            #[cfg(feature = "draft16")]
1431            Self::Draft16(c) => {
1432                let opts = VarInt::from_u64(0).expect("0 fits in VarInt");
1433                c.subscribe_namespace(namespace_prefix, opts, Vec::new())
1434                    .await
1435                    .map(AnyRequest::Draft16)
1436                    .map_err(|e| AnyConnectionError(e.to_string()))
1437            }
1438            #[cfg(feature = "draft17")]
1439            Self::Draft17(c) => {
1440                let opts = VarInt::from_u64(0).expect("0 fits in VarInt");
1441                c.subscribe_namespace(namespace_prefix, opts, Vec::new())
1442                    .await
1443                    .map(AnyRequest::Draft17)
1444                    .map_err(|e| AnyConnectionError(e.to_string()))
1445            }
1446            #[cfg(feature = "draft18")]
1447            Self::Draft18(c) => c
1448                .subscribe_namespace(namespace_prefix, Vec::new())
1449                .await
1450                .map(AnyRequest::Draft18)
1451                .map_err(|e| AnyConnectionError(e.to_string())),
1452            #[cfg(feature = "draft19")]
1453            Self::Draft19(c) => c
1454                .subscribe_namespace(namespace_prefix, Vec::new())
1455                .await
1456                .map(AnyRequest::Draft19)
1457                .map_err(|e| AnyConnectionError(e.to_string())),
1458            #[cfg(feature = "draft20")]
1459            Self::Draft20(c) => c
1460                .subscribe_namespace(namespace_prefix, Vec::new())
1461                .await
1462                .map(AnyRequest::Draft20)
1463                .map_err(|e| AnyConnectionError(e.to_string())),
1464            #[allow(unreachable_patterns)]
1465            other => Err(AnyConnectionError(format!(
1466                "subscribe_namespace: not yet wired up for draft {:?} via AnyConnection",
1467                other.draft()
1468            ))),
1469        }
1470    }
1471
1472    /// Send a SUBSCRIBE_UPDATE for an active subscription. Draft-14 only.
1473    ///
1474    /// # Why no later draft is wired, and why that is not this call's to fix
1475    ///
1476    /// Draft-15 renamed the message REQUEST_UPDATE and rebuilt it, and from
1477    /// draft-17 it travels **on the request's own bidirectional stream** rather
1478    /// than on a control stream — so an update needs the [`AnyRequest`] the
1479    /// original request returned, which this signature does not take and cannot
1480    /// be given without becoming a different call. Draft-20 goes further and
1481    /// has no `start_location` / `end_group` pair at all: Section 10.9 carries
1482    /// the new range as a `LOCATION_FILTER` parameter, on the same inclusive
1483    /// terms [`FetchRange`] describes. Reach a draft's own
1484    /// `Connection::send_on_request_stream` through the variant.
1485    #[allow(unused_variables)]
1486    pub async fn subscribe_update(
1487        &mut self,
1488        subscription_request_id: moqtap_codec::varint::VarInt,
1489        start_location: moqtap_codec::types::Location,
1490        end_group: moqtap_codec::varint::VarInt,
1491        subscriber_priority: u8,
1492        forward: moqtap_codec::types::Forward,
1493    ) -> Result<(), AnyConnectionError> {
1494        match self {
1495            #[cfg(feature = "draft14")]
1496            Self::Draft14(c) => c
1497                .subscribe_update(
1498                    subscription_request_id,
1499                    start_location,
1500                    end_group,
1501                    subscriber_priority,
1502                    forward,
1503                    Vec::new(),
1504                )
1505                .await
1506                .map(|_| ())
1507                .map_err(|e| AnyConnectionError(e.to_string())),
1508            #[allow(unreachable_patterns)]
1509            other => Err(AnyConnectionError(format!(
1510                "subscribe_update: not yet wired up for draft {:?} via AnyConnection",
1511                other.draft()
1512            ))),
1513        }
1514    }
1515}
1516
1517/// Trait for receiving events from an [`AnyConnection`].
1518///
1519/// Implementations must be `Send + Sync` because the adapter installed on
1520/// the inner draft-specific connection may emit events from async tasks.
1521/// `on_event` takes `&self` — implementations that need mutation should use
1522/// interior mutability (e.g. `Mutex`, `mpsc::Sender`).
1523///
1524/// The per-draft adapter clones the draft-specific event into the matching
1525/// [`AnyClientEvent`] variant before invoking `on_event`.
1526pub trait AnyConnectionObserver: Send + Sync {
1527    /// Called when a connection event occurs on any draft.
1528    fn on_event(&self, event: &AnyClientEvent);
1529}
1530
1531/// A no-op observer that discards all events.
1532pub struct NoOpObserver;
1533
1534impl AnyConnectionObserver for NoOpObserver {
1535    fn on_event(&self, _event: &AnyClientEvent) {}
1536}