Skip to main content

moqtap_client/
dispatch.rs

1//! Unified multi-draft entry-point types.
2//!
3//! This module is the facade downstream consumers (e.g. a CLI or desktop app)
4//! use to hold a MoQT 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//! Three 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//!
16//! Draft-specific protocol methods (e.g. `subscribe`, `fetch`) are not on
17//! `AnyConnection` because their signatures differ across drafts — match
18//! on the variant to reach them.
19
20use std::sync::Arc;
21
22use moqtap_codec::kvp::KeyValuePair;
23use moqtap_codec::version::DraftVersion;
24
25/// Generates the `AnyConnection` and `AnyClientEvent` enums plus the per-draft
26/// observer adapter, with one variant per enabled draft feature.
27macro_rules! dispatch_all {
28    (
29        $(
30            #[cfg(feature = $feat:literal)]
31            $variant:ident => $module:ident,
32        )+
33    ) => {
34        /// A MoQT client connection of any enabled draft version.
35        ///
36        /// Wraps the draft-specific `Connection` type. Methods common to all
37        /// drafts are forwarded; for draft-specific protocol calls, match on
38        /// the variant.
39        pub enum AnyConnection {
40            $(
41                #[cfg(feature = $feat)]
42                #[doc = concat!("A draft-", $feat, " connection.")]
43                $variant(crate::$module::connection::Connection),
44            )+
45        }
46
47        impl AnyConnection {
48            /// Returns the draft version this connection is using.
49            #[allow(unreachable_code)]
50            pub fn draft(&self) -> DraftVersion {
51                match self {
52                    $(
53                        #[cfg(feature = $feat)]
54                        Self::$variant(_) => DraftVersion::$variant,
55                    )+
56                    #[allow(unreachable_patterns)]
57                    _ => unreachable!("AnyConnection has no enabled variants"),
58                }
59            }
60
61            /// Attach an observer. The observer is adapted into the
62            /// draft-specific observer trait and installed on the inner
63            /// connection; events are forwarded as [`AnyClientEvent`].
64            ///
65            /// Replaces any previously attached observer.
66            #[allow(unused_variables)]
67            pub fn set_observer(&mut self, observer: Arc<dyn AnyConnectionObserver>) {
68                match self {
69                    $(
70                        #[cfg(feature = $feat)]
71                        Self::$variant(c) => {
72                            c.set_observer(Box::new($variant::Adapter(observer)));
73                        }
74                    )+
75                    #[allow(unreachable_patterns)]
76                    _ => {}
77                }
78            }
79
80            /// Remove any attached observer.
81            pub fn clear_observer(&mut self) {
82                match self {
83                    $(
84                        #[cfg(feature = $feat)]
85                        Self::$variant(c) => c.clear_observer(),
86                    )+
87                    #[allow(unreachable_patterns)]
88                    _ => {}
89                }
90            }
91
92            /// Close the connection with the given application error code
93            /// and reason.
94            #[allow(unused_variables)]
95            pub fn close(&self, code: u32, reason: &[u8]) {
96                match self {
97                    $(
98                        #[cfg(feature = $feat)]
99                        Self::$variant(c) => c.close(code, reason),
100                    )+
101                    #[allow(unreachable_patterns)]
102                    _ => {}
103                }
104            }
105        }
106
107        /// An event from a MoQT connection of any enabled draft version.
108        ///
109        /// Event shapes differ across drafts (e.g. draft-17's
110        /// `SubgroupObjectReceived` carries header types, while earlier
111        /// drafts carry decoded objects). Match on the variant to inspect
112        /// the draft-specific event.
113        #[non_exhaustive]
114        #[derive(Debug, Clone)]
115        pub enum AnyClientEvent {
116            $(
117                #[cfg(feature = $feat)]
118                #[doc = concat!("A draft-", $feat, " event.")]
119                $variant(crate::$module::event::ClientEvent),
120            )+
121        }
122
123        impl AnyClientEvent {
124            /// Returns the draft version this event belongs to.
125            #[allow(unreachable_code)]
126            pub fn draft(&self) -> DraftVersion {
127                match self {
128                    $(
129                        #[cfg(feature = $feat)]
130                        Self::$variant(_) => DraftVersion::$variant,
131                    )+
132                    #[allow(unreachable_patterns)]
133                    _ => unreachable!("AnyClientEvent has no enabled variants"),
134                }
135            }
136        }
137
138        // Per-draft adapter modules. Each holds an `Adapter` struct that
139        // implements the draft's `ConnectionObserver` trait by forwarding to
140        // an `AnyConnectionObserver`.
141        $(
142            #[cfg(feature = $feat)]
143            #[allow(non_snake_case)]
144            mod $variant {
145                use super::{AnyClientEvent, AnyConnectionObserver};
146                use std::sync::Arc;
147
148                pub(super) struct Adapter(pub(super) Arc<dyn AnyConnectionObserver>);
149
150                impl crate::$module::observer::ConnectionObserver for Adapter {
151                    fn on_event(&self, event: &crate::$module::event::ClientEvent) {
152                        self.0.on_event(&AnyClientEvent::$variant(event.clone()));
153                    }
154
155                    fn on_event_owned(&self, event: crate::$module::event::ClientEvent) {
156                        self.0.on_event(&AnyClientEvent::$variant(event));
157                    }
158                }
159            }
160        )+
161    };
162}
163
164dispatch_all! {
165    #[cfg(feature = "draft07")]
166    Draft07 => draft07,
167    #[cfg(feature = "draft08")]
168    Draft08 => draft08,
169    #[cfg(feature = "draft09")]
170    Draft09 => draft09,
171    #[cfg(feature = "draft10")]
172    Draft10 => draft10,
173    #[cfg(feature = "draft11")]
174    Draft11 => draft11,
175    #[cfg(feature = "draft12")]
176    Draft12 => draft12,
177    #[cfg(feature = "draft13")]
178    Draft13 => draft13,
179    #[cfg(feature = "draft14")]
180    Draft14 => draft14,
181    #[cfg(feature = "draft15")]
182    Draft15 => draft15,
183    #[cfg(feature = "draft16")]
184    Draft16 => draft16,
185    #[cfg(feature = "draft17")]
186    Draft17 => draft17,
187    #[cfg(feature = "draft18")]
188    Draft18 => draft18,
189    #[cfg(feature = "draft19")]
190    Draft19 => draft19,
191}
192
193/// Draft-agnostic transport choice for [`AnyConnection::connect`].
194#[derive(Debug, Clone)]
195pub enum AnyTransportType {
196    /// Raw QUIC via quinn. The `addr` passed to `connect` should be `host:port`.
197    Quic,
198    /// WebTransport via wtransport. The `url` is the WebTransport endpoint.
199    WebTransport {
200        /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
201        url: String,
202    },
203}
204
205/// Draft-agnostic client configuration. The exact per-draft `ClientConfig`
206/// is constructed internally by [`AnyConnection::connect`] based on `draft`.
207///
208/// Fields that aren't meaningful for the selected draft are ignored:
209/// `additional_versions` is not carried by drafts 15–17 (single-version
210/// setup) and drafts 07–13 always offer their own draft first.
211#[derive(Debug, Clone)]
212pub struct AnyClientConfig {
213    /// Primary draft version for the connection.
214    pub draft: DraftVersion,
215    /// Additional draft versions to offer in CLIENT_SETUP.
216    pub additional_versions: Vec<DraftVersion>,
217    /// Transport type (QUIC or WebTransport).
218    pub transport: AnyTransportType,
219    /// Whether to skip TLS certificate verification (for testing).
220    pub skip_cert_verification: bool,
221    /// Custom CA certificates to trust (DER-encoded).
222    pub ca_certs: Vec<Vec<u8>>,
223    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
224    pub setup_parameters: Vec<KeyValuePair>,
225}
226
227/// Error returned by [`AnyConnection::connect`] and
228/// [`AnyConnection::recv_and_dispatch`]. Draft-specific errors are flattened
229/// to strings so callers don't have to branch on draft to inspect errors.
230#[derive(Debug, thiserror::Error)]
231#[error("{0}")]
232pub struct AnyConnectionError(pub String);
233
234impl AnyConnection {
235    /// Connect to a MoQT server using the requested draft. Builds the
236    /// draft-specific `ClientConfig` from the provided [`AnyClientConfig`]
237    /// and dispatches to the appropriate `Connection::connect`.
238    pub async fn connect(addr: &str, config: AnyClientConfig) -> Result<Self, AnyConnectionError> {
239        match config.draft {
240            #[cfg(feature = "draft07")]
241            DraftVersion::Draft07 => {
242                use crate::draft07::connection::{ClientConfig, Connection, TransportType};
243                let transport = match config.transport {
244                    AnyTransportType::Quic => TransportType::Quic,
245                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
246                };
247                let inner = ClientConfig {
248                    additional_versions: config.additional_versions,
249                    transport,
250                    skip_cert_verification: config.skip_cert_verification,
251                    ca_certs: config.ca_certs,
252                    setup_parameters: config.setup_parameters,
253                };
254                let c = Connection::connect(addr, inner)
255                    .await
256                    .map_err(|e| AnyConnectionError(e.to_string()))?;
257                Ok(AnyConnection::Draft07(c))
258            }
259            #[cfg(feature = "draft08")]
260            DraftVersion::Draft08 => {
261                use crate::draft08::connection::{ClientConfig, Connection, TransportType};
262                let transport = match config.transport {
263                    AnyTransportType::Quic => TransportType::Quic,
264                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
265                };
266                let inner = ClientConfig {
267                    additional_versions: config.additional_versions,
268                    transport,
269                    skip_cert_verification: config.skip_cert_verification,
270                    ca_certs: config.ca_certs,
271                    setup_parameters: config.setup_parameters,
272                };
273                let c = Connection::connect(addr, inner)
274                    .await
275                    .map_err(|e| AnyConnectionError(e.to_string()))?;
276                Ok(AnyConnection::Draft08(c))
277            }
278            #[cfg(feature = "draft09")]
279            DraftVersion::Draft09 => {
280                use crate::draft09::connection::{ClientConfig, Connection, TransportType};
281                let transport = match config.transport {
282                    AnyTransportType::Quic => TransportType::Quic,
283                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
284                };
285                let inner = ClientConfig {
286                    additional_versions: config.additional_versions,
287                    transport,
288                    skip_cert_verification: config.skip_cert_verification,
289                    ca_certs: config.ca_certs,
290                    setup_parameters: config.setup_parameters,
291                };
292                let c = Connection::connect(addr, inner)
293                    .await
294                    .map_err(|e| AnyConnectionError(e.to_string()))?;
295                Ok(AnyConnection::Draft09(c))
296            }
297            #[cfg(feature = "draft10")]
298            DraftVersion::Draft10 => {
299                use crate::draft10::connection::{ClientConfig, Connection, TransportType};
300                let transport = match config.transport {
301                    AnyTransportType::Quic => TransportType::Quic,
302                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
303                };
304                let inner = ClientConfig {
305                    additional_versions: config.additional_versions,
306                    transport,
307                    skip_cert_verification: config.skip_cert_verification,
308                    ca_certs: config.ca_certs,
309                    setup_parameters: config.setup_parameters,
310                };
311                let c = Connection::connect(addr, inner)
312                    .await
313                    .map_err(|e| AnyConnectionError(e.to_string()))?;
314                Ok(AnyConnection::Draft10(c))
315            }
316            #[cfg(feature = "draft11")]
317            DraftVersion::Draft11 => {
318                use crate::draft11::connection::{ClientConfig, Connection, TransportType};
319                let transport = match config.transport {
320                    AnyTransportType::Quic => TransportType::Quic,
321                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
322                };
323                let inner = ClientConfig {
324                    additional_versions: config.additional_versions,
325                    transport,
326                    skip_cert_verification: config.skip_cert_verification,
327                    ca_certs: config.ca_certs,
328                    setup_parameters: config.setup_parameters,
329                };
330                let c = Connection::connect(addr, inner)
331                    .await
332                    .map_err(|e| AnyConnectionError(e.to_string()))?;
333                Ok(AnyConnection::Draft11(c))
334            }
335            #[cfg(feature = "draft12")]
336            DraftVersion::Draft12 => {
337                use crate::draft12::connection::{ClientConfig, Connection, TransportType};
338                let transport = match config.transport {
339                    AnyTransportType::Quic => TransportType::Quic,
340                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
341                };
342                let inner = ClientConfig {
343                    additional_versions: config.additional_versions,
344                    transport,
345                    skip_cert_verification: config.skip_cert_verification,
346                    ca_certs: config.ca_certs,
347                    setup_parameters: config.setup_parameters,
348                };
349                let c = Connection::connect(addr, inner)
350                    .await
351                    .map_err(|e| AnyConnectionError(e.to_string()))?;
352                Ok(AnyConnection::Draft12(c))
353            }
354            #[cfg(feature = "draft13")]
355            DraftVersion::Draft13 => {
356                use crate::draft13::connection::{ClientConfig, Connection, TransportType};
357                let transport = match config.transport {
358                    AnyTransportType::Quic => TransportType::Quic,
359                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
360                };
361                let inner = ClientConfig {
362                    additional_versions: config.additional_versions,
363                    transport,
364                    skip_cert_verification: config.skip_cert_verification,
365                    ca_certs: config.ca_certs,
366                    setup_parameters: config.setup_parameters,
367                };
368                let c = Connection::connect(addr, inner)
369                    .await
370                    .map_err(|e| AnyConnectionError(e.to_string()))?;
371                Ok(AnyConnection::Draft13(c))
372            }
373            #[cfg(feature = "draft14")]
374            DraftVersion::Draft14 => {
375                use crate::draft14::connection::{ClientConfig, Connection, TransportType};
376                let transport = match config.transport {
377                    AnyTransportType::Quic => TransportType::Quic,
378                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
379                };
380                let inner = ClientConfig {
381                    draft: config.draft,
382                    additional_versions: config.additional_versions,
383                    transport,
384                    skip_cert_verification: config.skip_cert_verification,
385                    ca_certs: config.ca_certs,
386                    setup_parameters: config.setup_parameters,
387                };
388                let c = Connection::connect(addr, inner)
389                    .await
390                    .map_err(|e| AnyConnectionError(e.to_string()))?;
391                Ok(AnyConnection::Draft14(c))
392            }
393            #[cfg(feature = "draft15")]
394            DraftVersion::Draft15 => {
395                use crate::draft15::connection::{ClientConfig, Connection, TransportType};
396                let transport = match config.transport {
397                    AnyTransportType::Quic => TransportType::Quic,
398                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
399                };
400                let inner = ClientConfig {
401                    draft: config.draft,
402                    transport,
403                    skip_cert_verification: config.skip_cert_verification,
404                    ca_certs: config.ca_certs,
405                    setup_parameters: config.setup_parameters,
406                };
407                let c = Connection::connect(addr, inner)
408                    .await
409                    .map_err(|e| AnyConnectionError(e.to_string()))?;
410                Ok(AnyConnection::Draft15(c))
411            }
412            #[cfg(feature = "draft16")]
413            DraftVersion::Draft16 => {
414                use crate::draft16::connection::{ClientConfig, Connection, TransportType};
415                let transport = match config.transport {
416                    AnyTransportType::Quic => TransportType::Quic,
417                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
418                };
419                let inner = ClientConfig {
420                    draft: config.draft,
421                    transport,
422                    skip_cert_verification: config.skip_cert_verification,
423                    ca_certs: config.ca_certs,
424                    setup_parameters: config.setup_parameters,
425                };
426                let c = Connection::connect(addr, inner)
427                    .await
428                    .map_err(|e| AnyConnectionError(e.to_string()))?;
429                Ok(AnyConnection::Draft16(c))
430            }
431            #[cfg(feature = "draft17")]
432            DraftVersion::Draft17 => {
433                use crate::draft17::connection::{ClientConfig, Connection, TransportType};
434                let transport = match config.transport {
435                    AnyTransportType::Quic => TransportType::Quic,
436                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
437                };
438                let inner = ClientConfig {
439                    draft: config.draft,
440                    transport,
441                    skip_cert_verification: config.skip_cert_verification,
442                    ca_certs: config.ca_certs,
443                    setup_parameters: config.setup_parameters,
444                };
445                let c = Connection::connect(addr, inner)
446                    .await
447                    .map_err(|e| AnyConnectionError(e.to_string()))?;
448                Ok(AnyConnection::Draft17(c))
449            }
450            #[cfg(feature = "draft18")]
451            DraftVersion::Draft18 => {
452                use crate::draft18::connection::{ClientConfig, Connection, TransportType};
453                let transport = match config.transport {
454                    AnyTransportType::Quic => TransportType::Quic,
455                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
456                };
457                let inner = ClientConfig {
458                    draft: config.draft,
459                    transport,
460                    skip_cert_verification: config.skip_cert_verification,
461                    ca_certs: config.ca_certs,
462                    setup_parameters: config.setup_parameters,
463                };
464                let c = Connection::connect(addr, inner)
465                    .await
466                    .map_err(|e| AnyConnectionError(e.to_string()))?;
467                Ok(AnyConnection::Draft18(c))
468            }
469            #[cfg(feature = "draft19")]
470            DraftVersion::Draft19 => {
471                use crate::draft19::connection::{ClientConfig, Connection, TransportType};
472                let transport = match config.transport {
473                    AnyTransportType::Quic => TransportType::Quic,
474                    AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
475                };
476                let inner = ClientConfig {
477                    draft: config.draft,
478                    transport,
479                    skip_cert_verification: config.skip_cert_verification,
480                    ca_certs: config.ca_certs,
481                    setup_parameters: config.setup_parameters,
482                };
483                let c = Connection::connect(addr, inner)
484                    .await
485                    .map_err(|e| AnyConnectionError(e.to_string()))?;
486                Ok(AnyConnection::Draft19(c))
487            }
488            #[allow(unreachable_patterns)]
489            other => Err(AnyConnectionError(format!("draft {other:?} not enabled in this build",))),
490        }
491    }
492
493    /// Read and dispatch one control message on the active draft. Draft-specific
494    /// control-message return values are discarded because event delivery goes
495    /// through the attached observer; callers only care about success/failure.
496    pub async fn recv_and_dispatch(&mut self) -> Result<(), AnyConnectionError> {
497        match self {
498            #[cfg(feature = "draft07")]
499            Self::Draft07(c) => c
500                .recv_and_dispatch()
501                .await
502                .map(|_| ())
503                .map_err(|e| AnyConnectionError(e.to_string())),
504            #[cfg(feature = "draft08")]
505            Self::Draft08(c) => c
506                .recv_and_dispatch()
507                .await
508                .map(|_| ())
509                .map_err(|e| AnyConnectionError(e.to_string())),
510            #[cfg(feature = "draft09")]
511            Self::Draft09(c) => c
512                .recv_and_dispatch()
513                .await
514                .map(|_| ())
515                .map_err(|e| AnyConnectionError(e.to_string())),
516            #[cfg(feature = "draft10")]
517            Self::Draft10(c) => c
518                .recv_and_dispatch()
519                .await
520                .map(|_| ())
521                .map_err(|e| AnyConnectionError(e.to_string())),
522            #[cfg(feature = "draft11")]
523            Self::Draft11(c) => c
524                .recv_and_dispatch()
525                .await
526                .map(|_| ())
527                .map_err(|e| AnyConnectionError(e.to_string())),
528            #[cfg(feature = "draft12")]
529            Self::Draft12(c) => c
530                .recv_and_dispatch()
531                .await
532                .map(|_| ())
533                .map_err(|e| AnyConnectionError(e.to_string())),
534            #[cfg(feature = "draft13")]
535            Self::Draft13(c) => c
536                .recv_and_dispatch()
537                .await
538                .map(|_| ())
539                .map_err(|e| AnyConnectionError(e.to_string())),
540            #[cfg(feature = "draft14")]
541            Self::Draft14(c) => c
542                .recv_and_dispatch()
543                .await
544                .map(|_| ())
545                .map_err(|e| AnyConnectionError(e.to_string())),
546            #[cfg(feature = "draft15")]
547            Self::Draft15(c) => c
548                .recv_and_dispatch()
549                .await
550                .map(|_| ())
551                .map_err(|e| AnyConnectionError(e.to_string())),
552            #[cfg(feature = "draft16")]
553            Self::Draft16(c) => c
554                .recv_and_dispatch()
555                .await
556                .map(|_| ())
557                .map_err(|e| AnyConnectionError(e.to_string())),
558            #[cfg(feature = "draft17")]
559            Self::Draft17(c) => c
560                .recv_and_dispatch()
561                .await
562                .map(|_| ())
563                .map_err(|e| AnyConnectionError(e.to_string())),
564            #[cfg(feature = "draft18")]
565            Self::Draft18(c) => c
566                .recv_and_dispatch()
567                .await
568                .map(|_| ())
569                .map_err(|e| AnyConnectionError(e.to_string())),
570            #[cfg(feature = "draft19")]
571            Self::Draft19(c) => c
572                .recv_and_dispatch()
573                .await
574                .map(|_| ())
575                .map_err(|e| AnyConnectionError(e.to_string())),
576            #[allow(unreachable_patterns)]
577            _ => Err(AnyConnectionError("AnyConnection has no enabled variants".into())),
578        }
579    }
580
581    // ── Unified control-message helpers ──────────────────────────────────
582    //
583    // Draft-agnostic shorthands. Each dispatches to the active variant and
584    // defaults fields not expressible in the unified shape; drafts that
585    // lack the operation return an `AnyConnectionError`. Match on the
586    // variant directly when full per-draft control is needed.
587
588    /// Send an UNSUBSCRIBE for the given request ID. Identical across all drafts.
589    #[allow(unused_variables)]
590    pub async fn unsubscribe(
591        &mut self,
592        request_id: moqtap_codec::varint::VarInt,
593    ) -> Result<(), AnyConnectionError> {
594        match self {
595            #[cfg(feature = "draft07")]
596            Self::Draft07(c) => {
597                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
598            }
599            #[cfg(feature = "draft08")]
600            Self::Draft08(c) => {
601                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
602            }
603            #[cfg(feature = "draft09")]
604            Self::Draft09(c) => {
605                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
606            }
607            #[cfg(feature = "draft10")]
608            Self::Draft10(c) => {
609                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
610            }
611            #[cfg(feature = "draft11")]
612            Self::Draft11(c) => {
613                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
614            }
615            #[cfg(feature = "draft12")]
616            Self::Draft12(c) => {
617                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
618            }
619            #[cfg(feature = "draft13")]
620            Self::Draft13(c) => {
621                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
622            }
623            #[cfg(feature = "draft14")]
624            Self::Draft14(c) => {
625                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
626            }
627            #[cfg(feature = "draft15")]
628            Self::Draft15(c) => {
629                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
630            }
631            #[cfg(feature = "draft16")]
632            Self::Draft16(c) => {
633                c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
634            }
635            #[allow(unreachable_patterns)]
636            other => Err(AnyConnectionError(format!(
637                "unsubscribe: not yet wired up for draft {:?} via AnyConnection",
638                other.draft()
639            ))),
640        }
641    }
642
643    /// Send a SUBSCRIBE with the given filter, priority, and group order.
644    /// Supported on drafts 12–17. Drafts 15–17 carry priority/order/filter via
645    /// parameters; this helper passes an empty parameter list, so those fields
646    /// default to protocol-defined values on those drafts.
647    #[allow(unused_variables)]
648    pub async fn subscribe(
649        &mut self,
650        namespace: moqtap_codec::types::TrackNamespace,
651        track_name: Vec<u8>,
652        subscriber_priority: u8,
653        group_order: moqtap_codec::types::GroupOrder,
654        filter_type: moqtap_codec::types::FilterType,
655    ) -> Result<moqtap_codec::varint::VarInt, AnyConnectionError> {
656        use moqtap_codec::varint::VarInt;
657        match self {
658            #[cfg(feature = "draft12")]
659            Self::Draft12(c) => {
660                let go = VarInt::from_u64(group_order as u64)
661                    .map_err(|e| AnyConnectionError(e.to_string()))?;
662                let ft = VarInt::from_u64(filter_type as u64)
663                    .map_err(|e| AnyConnectionError(e.to_string()))?;
664                c.subscribe(namespace, track_name, subscriber_priority, go, ft)
665                    .await
666                    .map_err(|e| AnyConnectionError(e.to_string()))
667            }
668            #[cfg(feature = "draft13")]
669            Self::Draft13(c) => {
670                let go = VarInt::from_u64(group_order as u64)
671                    .map_err(|e| AnyConnectionError(e.to_string()))?;
672                let ft = VarInt::from_u64(filter_type as u64)
673                    .map_err(|e| AnyConnectionError(e.to_string()))?;
674                c.subscribe(namespace, track_name, subscriber_priority, go, ft)
675                    .await
676                    .map_err(|e| AnyConnectionError(e.to_string()))
677            }
678            #[cfg(feature = "draft14")]
679            Self::Draft14(c) => c
680                .subscribe(namespace, track_name, subscriber_priority, group_order, filter_type)
681                .await
682                .map_err(|e| AnyConnectionError(e.to_string())),
683            #[cfg(feature = "draft15")]
684            Self::Draft15(c) => c
685                .subscribe(namespace, track_name, Vec::new())
686                .await
687                .map_err(|e| AnyConnectionError(e.to_string())),
688            #[cfg(feature = "draft16")]
689            Self::Draft16(c) => c
690                .subscribe(namespace, track_name, Vec::new())
691                .await
692                .map_err(|e| AnyConnectionError(e.to_string())),
693            #[cfg(feature = "draft17")]
694            Self::Draft17(c) => c
695                .subscribe(namespace, track_name, Vec::new())
696                .await
697                .map_err(|e| AnyConnectionError(e.to_string())),
698            #[cfg(feature = "draft18")]
699            Self::Draft18(c) => c
700                .subscribe(namespace, track_name, Vec::new())
701                .await
702                .map_err(|e| AnyConnectionError(e.to_string())),
703            #[cfg(feature = "draft19")]
704            Self::Draft19(c) => c
705                .subscribe(namespace, track_name, Vec::new())
706                .await
707                .map_err(|e| AnyConnectionError(e.to_string())),
708            #[allow(unreachable_patterns)]
709            other => Err(AnyConnectionError(format!(
710                "subscribe: not yet wired up for draft {:?} via AnyConnection",
711                other.draft()
712            ))),
713        }
714    }
715
716    /// Send a standalone FETCH. Supported on drafts 14–17. On draft 14 the
717    /// `end_group`/`end_object` are ignored (draft 14's wrapper only accepts
718    /// a start location).
719    #[allow(unused_variables)]
720    pub async fn fetch(
721        &mut self,
722        namespace: moqtap_codec::types::TrackNamespace,
723        track_name: Vec<u8>,
724        start_group: moqtap_codec::varint::VarInt,
725        start_object: moqtap_codec::varint::VarInt,
726        end_group: moqtap_codec::varint::VarInt,
727        end_object: moqtap_codec::varint::VarInt,
728    ) -> Result<moqtap_codec::varint::VarInt, AnyConnectionError> {
729        match self {
730            #[cfg(feature = "draft14")]
731            Self::Draft14(c) => c
732                .fetch(namespace, track_name, start_group, start_object)
733                .await
734                .map_err(|e| AnyConnectionError(e.to_string())),
735            #[cfg(feature = "draft15")]
736            Self::Draft15(c) => c
737                .fetch(namespace, track_name, start_group, start_object, end_group, end_object)
738                .await
739                .map_err(|e| AnyConnectionError(e.to_string())),
740            #[cfg(feature = "draft16")]
741            Self::Draft16(c) => c
742                .fetch(namespace, track_name, start_group, start_object, end_group, end_object)
743                .await
744                .map_err(|e| AnyConnectionError(e.to_string())),
745            #[cfg(feature = "draft17")]
746            Self::Draft17(c) => c
747                .fetch(namespace, track_name, start_group, start_object, end_group, end_object)
748                .await
749                .map_err(|e| AnyConnectionError(e.to_string())),
750            #[cfg(feature = "draft18")]
751            Self::Draft18(c) => c
752                .fetch(namespace, track_name, start_group, start_object, end_group, end_object)
753                .await
754                .map_err(|e| AnyConnectionError(e.to_string())),
755            #[cfg(feature = "draft19")]
756            Self::Draft19(c) => c
757                .fetch(namespace, track_name, start_group, start_object, end_group, end_object)
758                .await
759                .map_err(|e| AnyConnectionError(e.to_string())),
760            #[allow(unreachable_patterns)]
761            other => Err(AnyConnectionError(format!(
762                "fetch: not yet wired up for draft {:?} via AnyConnection",
763                other.draft()
764            ))),
765        }
766    }
767
768    /// Send a TRACK_STATUS query for the given track. Supported on drafts 14–17.
769    /// On drafts 15–17, passes an empty parameter list.
770    #[allow(unused_variables)]
771    pub async fn track_status(
772        &mut self,
773        namespace: moqtap_codec::types::TrackNamespace,
774        track_name: Vec<u8>,
775    ) -> Result<moqtap_codec::varint::VarInt, AnyConnectionError> {
776        match self {
777            #[cfg(feature = "draft14")]
778            Self::Draft14(c) => c
779                .track_status(namespace, track_name)
780                .await
781                .map_err(|e| AnyConnectionError(e.to_string())),
782            #[cfg(feature = "draft15")]
783            Self::Draft15(c) => c
784                .track_status(namespace, track_name, Vec::new())
785                .await
786                .map_err(|e| AnyConnectionError(e.to_string())),
787            #[cfg(feature = "draft16")]
788            Self::Draft16(c) => c
789                .track_status(namespace, track_name, Vec::new())
790                .await
791                .map_err(|e| AnyConnectionError(e.to_string())),
792            #[cfg(feature = "draft17")]
793            Self::Draft17(c) => c
794                .track_status(namespace, track_name, Vec::new())
795                .await
796                .map_err(|e| AnyConnectionError(e.to_string())),
797            #[cfg(feature = "draft18")]
798            Self::Draft18(c) => c
799                .track_status(namespace, track_name, Vec::new())
800                .await
801                .map_err(|e| AnyConnectionError(e.to_string())),
802            #[cfg(feature = "draft19")]
803            Self::Draft19(c) => c
804                .track_status(namespace, track_name, Vec::new())
805                .await
806                .map_err(|e| AnyConnectionError(e.to_string())),
807            #[allow(unreachable_patterns)]
808            other => Err(AnyConnectionError(format!(
809                "track_status: not yet wired up for draft {:?} via AnyConnection",
810                other.draft()
811            ))),
812        }
813    }
814
815    /// Send a SUBSCRIBE_NAMESPACE (or SUBSCRIBE_ANNOUNCES on drafts 11–12).
816    /// Supported on drafts 11–17. Drafts 16–17 pass default subscribe options
817    /// and an empty parameter list.
818    #[allow(unused_variables)]
819    pub async fn subscribe_namespace(
820        &mut self,
821        namespace_prefix: moqtap_codec::types::TrackNamespace,
822    ) -> Result<moqtap_codec::varint::VarInt, AnyConnectionError> {
823        use moqtap_codec::varint::VarInt;
824        match self {
825            #[cfg(feature = "draft11")]
826            Self::Draft11(c) => c
827                .subscribe_announces(namespace_prefix)
828                .await
829                .map_err(|e| AnyConnectionError(e.to_string())),
830            #[cfg(feature = "draft12")]
831            Self::Draft12(c) => c
832                .subscribe_announces(namespace_prefix)
833                .await
834                .map_err(|e| AnyConnectionError(e.to_string())),
835            #[cfg(feature = "draft13")]
836            Self::Draft13(c) => c
837                .subscribe_namespace(namespace_prefix)
838                .await
839                .map_err(|e| AnyConnectionError(e.to_string())),
840            #[cfg(feature = "draft14")]
841            Self::Draft14(c) => c
842                .subscribe_namespace(namespace_prefix)
843                .await
844                .map_err(|e| AnyConnectionError(e.to_string())),
845            #[cfg(feature = "draft15")]
846            Self::Draft15(c) => c
847                .subscribe_namespace(namespace_prefix, Vec::new())
848                .await
849                .map_err(|e| AnyConnectionError(e.to_string())),
850            #[cfg(feature = "draft16")]
851            Self::Draft16(c) => {
852                let opts = VarInt::from_u64(0).expect("0 fits in VarInt");
853                c.subscribe_namespace(namespace_prefix, opts, Vec::new())
854                    .await
855                    .map_err(|e| AnyConnectionError(e.to_string()))
856            }
857            #[cfg(feature = "draft17")]
858            Self::Draft17(c) => {
859                let opts = VarInt::from_u64(0).expect("0 fits in VarInt");
860                c.subscribe_namespace(namespace_prefix, opts, Vec::new())
861                    .await
862                    .map_err(|e| AnyConnectionError(e.to_string()))
863            }
864            #[cfg(feature = "draft18")]
865            Self::Draft18(c) => c
866                .subscribe_namespace(namespace_prefix, Vec::new())
867                .await
868                .map_err(|e| AnyConnectionError(e.to_string())),
869            #[cfg(feature = "draft19")]
870            Self::Draft19(c) => c
871                .subscribe_namespace(namespace_prefix, Vec::new())
872                .await
873                .map_err(|e| AnyConnectionError(e.to_string())),
874            #[allow(unreachable_patterns)]
875            other => Err(AnyConnectionError(format!(
876                "subscribe_namespace: not yet wired up for draft {:?} via AnyConnection",
877                other.draft()
878            ))),
879        }
880    }
881
882    /// Send a SUBSCRIBE_UPDATE for an active subscription. Draft 14 only —
883    /// earlier/later drafts either don't expose a matching client wrapper or
884    /// use a different message shape.
885    #[allow(unused_variables)]
886    pub async fn subscribe_update(
887        &mut self,
888        subscription_request_id: moqtap_codec::varint::VarInt,
889        start_location: moqtap_codec::types::Location,
890        end_group: moqtap_codec::varint::VarInt,
891        subscriber_priority: u8,
892        forward: moqtap_codec::types::Forward,
893    ) -> Result<(), AnyConnectionError> {
894        match self {
895            #[cfg(feature = "draft14")]
896            Self::Draft14(c) => c
897                .subscribe_update(
898                    subscription_request_id,
899                    start_location,
900                    end_group,
901                    subscriber_priority,
902                    forward,
903                    Vec::new(),
904                )
905                .await
906                .map(|_| ())
907                .map_err(|e| AnyConnectionError(e.to_string())),
908            #[allow(unreachable_patterns)]
909            other => Err(AnyConnectionError(format!(
910                "subscribe_update: not yet wired up for draft {:?} via AnyConnection",
911                other.draft()
912            ))),
913        }
914    }
915}
916
917/// Trait for receiving events from an [`AnyConnection`].
918///
919/// Implementations must be `Send + Sync` because the adapter installed on
920/// the inner draft-specific connection may emit events from async tasks.
921/// `on_event` takes `&self` — implementations that need mutation should use
922/// interior mutability (e.g. `Mutex`, `mpsc::Sender`).
923///
924/// The per-draft adapter clones the draft-specific event into the matching
925/// [`AnyClientEvent`] variant before invoking `on_event`.
926pub trait AnyConnectionObserver: Send + Sync {
927    /// Called when a connection event occurs on any draft.
928    fn on_event(&self, event: &AnyClientEvent);
929}
930
931/// A no-op observer that discards all events.
932pub struct NoOpObserver;
933
934impl AnyConnectionObserver for NoOpObserver {
935    fn on_event(&self, _event: &AnyClientEvent) {}
936}