Skip to main content

moqtap_client/draft11/
endpoint.rs

1use std::collections::HashMap;
2use std::sync::{Arc, Mutex};
3
4use crate::draft11::fetch::{FetchError, FetchState, FetchStateMachine};
5use crate::draft11::namespace::{
6    AnnounceState, AnnounceStateMachine, NamespaceError, SubscribeAnnouncesState,
7    SubscribeAnnouncesStateMachine,
8};
9use crate::draft11::session::request_id::{RequestIdAllocator, RequestIdError, Role};
10use crate::draft11::session::setup::{self, SetupError};
11use crate::draft11::session::state::{SessionError, SessionState, SessionStateMachine};
12use crate::draft11::subscription::{
13    SubscriptionError, SubscriptionState, SubscriptionStateMachine,
14};
15use crate::draft11::track_status::{TrackStatusError, TrackStatusState, TrackStatusStateMachine};
16use crate::forwarding_preference::{ObjectForwardingPreference, TrackForwardingPreferences};
17use crate::track_locations::{
18    EndOfTrackPlacement, ObjectLocation, ObjectRole, TrackLocations, TrackObjects,
19};
20use moqtap_codec::draft11::error_codes::{
21    FetchErrorCode, SessionErrorCode, SubscribeAnnouncesErrorCode, SubscribeErrorCode,
22};
23use moqtap_codec::draft11::message::{
24    self, Announce, AnnounceCancel, AnnounceError, AnnounceOk, ClientSetup, ControlMessage, Fetch,
25    FetchCancel, FetchPayload, FetchType, GoAway, MaxRequestId, RequestsBlocked, ServerSetup,
26    Subscribe, SubscribeAnnounces, SubscribeAnnouncesError, SubscribeAnnouncesOk, SubscribeDone,
27    SubscribeError, SubscribeOk, SubscribeUpdate, TrackStatus, TrackStatusRequest, Unannounce,
28    Unsubscribe, UnsubscribeAnnounces,
29};
30use moqtap_codec::kvp::{KeyValuePair, KvpValue};
31use moqtap_codec::types::*;
32use moqtap_codec::varint::VarInt;
33
34/// Key identifying a namespace (used for Announce maps).
35type NamespaceKey = Vec<Vec<u8>>;
36
37/// Errors that can occur during draft-11 endpoint operations.
38#[derive(Debug, thiserror::Error)]
39pub enum EndpointError {
40    /// A GOAWAY carrying a New Session URI arrived at a server.
41    ///
42    /// Section 8.4: "If a server receives a GOAWAY with a non-zero New
43    /// Session URI Length it MUST terminate the session with a Protocol
44    /// Violation." Migration is something a server offers a client, never the
45    /// other way round.
46    #[error("GOAWAY carrying a New Session URI received at a server")]
47    GoAwayUriAtServer,
48    /// A session-level state machine error.
49    #[error("session error: {0}")]
50    Session(#[from] SessionError),
51    /// A request ID allocation or validation error.
52    #[error("request ID error: {0}")]
53    RequestId(#[from] RequestIdError),
54    /// This endpoint was asked to advertise a Maximum Request ID that does
55    /// not increase, and refused. Nothing was written.
56    ///
57    /// The send-side mirror of the rule a peer breaks by sending one — Section 8.5:
58    /// "The Maximum Request ID MUST only increase within a session". No closing
59    /// mark, because the draft's sentence does not close there: it runs on
60    /// into the receipt half, which is the peer's side of this rule and not
61    /// this one's. Its own
62    /// variant, and not the received one, because the two are opposite
63    /// findings that would otherwise arrive as the same value: the received one
64    /// is a peer in violation and this one is a caller of this library asking
65    /// for a message that would put this endpoint in violation, and
66    /// [`EndpointError::session_error_code`] answers `Some` for it either way.
67    ///
68    /// Not fatal. The message is refused instead of built, the ceiling stays
69    /// where it was, and nothing reaches the peer to object to.
70    #[error(
71        "the Maximum Request ID already advertised is {advertised}, so {offered} would not increase it"
72    )]
73    MaxRequestIdWouldNotIncrease {
74        /// The ceiling this endpoint has already advertised.
75        advertised: u64,
76        /// The value it was asked to advertise instead.
77        offered: u64,
78    },
79    /// A subscription state machine error.
80    #[error("subscription error: {0}")]
81    Subscription(#[from] SubscriptionError),
82    /// A fetch state machine error.
83    #[error("fetch error: {0}")]
84    Fetch(#[from] FetchError),
85    /// A namespace state machine error.
86    #[error("namespace error: {0}")]
87    Namespace(#[from] NamespaceError),
88    /// A track status state machine error.
89    #[error("track status error: {0}")]
90    TrackStatus(#[from] TrackStatusError),
91    /// A setup negotiation error.
92    #[error("setup error: {0}")]
93    Setup(#[from] SetupError),
94    /// The request ID does not match any known state machine.
95    #[error("unknown request ID: {0}")]
96    UnknownRequest(u64),
97    /// The track namespace does not match any known state machine.
98    #[error("unknown namespace")]
99    UnknownNamespace,
100    /// The session is not in the Active state.
101    #[error("session not active")]
102    NotActive,
103    /// The session is draining and cannot accept new requests.
104    #[error("session is draining, no new requests allowed")]
105    Draining,
106    /// A filter that names a start location was asked for through a helper
107    /// that has no start location to give it.
108    #[error("this filter type needs a start location; use the range form of this call")]
109    FilterNeedsRange,
110    /// A second GOAWAY arrived on the control stream.
111    ///
112    /// The GOAWAY that says the peer is going away is one message, and the
113    /// draft answers a repeat of it with a session close rather than with an
114    /// error about the second message: there is no state a second one could
115    /// move that the first has not already moved.
116    #[error("a second GOAWAY arrived on the control stream")]
117    RepeatedGoAway,
118
119    /// A Track Alias names two tracks at once.
120    ///
121    /// Section 8.7, on the Track Alias the subscriber chooses in SUBSCRIBE:
122    /// "If the Track Alias is already being used for a different track, the
123    /// publisher MUST close the session with a Duplicate Track Alias error".
124    /// Section 8.9 states the other end of the same rule, on the alias a
125    /// SUBSCRIBE_ERROR may offer to retry with: "If this Track Alias is
126    /// already in use, the subscriber MUST close the connection with a
127    /// Duplicate Track Alias error".
128    ///
129    /// This is the last draft that puts the alias in SUBSCRIBE and so the last
130    /// that raises this at the publisher. From draft-12 the alias moves into
131    /// SUBSCRIBE_OK and PUBLISH, and the endpoint that must close moves with
132    /// it.
133    ///
134    /// The session is over: this endpoint's own state has moved to Closed and
135    /// the code the transport should close with is in
136    /// [`EndpointError::session_error_code`].
137    #[error("track alias {alias} already names request {established}'s track; request {offered} names a different one")]
138    DuplicateTrackAlias {
139        /// The alias both tracks are named by.
140        alias: u64,
141        /// The request whose live subscription holds the alias.
142        established: u64,
143        /// The request whose message arrived naming it for another track.
144        offered: u64,
145    },
146    /// This endpoint was asked to give a Track Alias to a second track.
147    ///
148    /// The same rule as [`EndpointError::DuplicateTrackAlias`] read at the end
149    /// that chooses the alias. Section 3.4 describes the code as "The endpoint
150    /// attempted to use a Track Alias that was already in use", and Section
151    /// 8.7 says what the receiving publisher does about it, so a SUBSCRIBE
152    /// built this way is one the peer must answer by ending the session.
153    ///
154    /// The message is refused instead, and nothing else moves: no Request ID
155    /// is spent, no subscription is created, and the session stays as it was.
156    /// The alias never reaches the peer, so there is nothing for the peer to
157    /// close over.
158    #[error("track alias {alias} already names request {held}'s track")]
159    TrackAliasInUse {
160        /// The alias that is already spoken for.
161        alias: u64,
162        /// The request whose live subscription holds it.
163        held: u64,
164    },
165    /// A track's objects were framed two different ways.
166    ///
167    /// Section 9: "Every Track has a single 'Object Forwarding Preference' and
168    /// the Original Publisher MUST NOT mix different forwarding preferences
169    /// within a single track. If a subscriber receives Objects via both
170    /// Subgroup streams and Datagrams in response to a SUBSCRIBE, it SHOULD
171    /// close the session with an error of 'Protocol Violation'"
172    ///
173    /// The framing is the preference: an object on a subgroup stream has the
174    /// Subgroup preference and an object in a datagram has the Datagram one,
175    /// so the track's first object settles the property and this is every
176    /// later object measured against it.
177    ///
178    /// # What this draft reworded
179    ///
180    /// The prohibition is the one drafts 07 through 10 state; the sentence
181    /// after it is not. This draft names the two framings the rule is about
182    /// rather than "different forwarding preferences", and scopes it to
183    /// objects answering a SUBSCRIBE — which is where its Section 9.1.1 puts
184    /// the definition as well: "When in response to a SUBSCRIBE, an Object
185    /// MUST be sent according to its Object Forwarding Preference". A fetch
186    /// stream is neither framing and settles nothing here either way.
187    #[error(
188        "track alias {alias} carries objects framed as {established}, and one is \
189         framed as {offered}"
190    )]
191    MixedForwardingPreference {
192        /// The Track Alias the offending object named.
193        alias: u64,
194        /// The framing the track's earlier objects settled on.
195        established: ObjectForwardingPreference,
196        /// The framing the offending object used.
197        offered: ObjectForwardingPreference,
198    },
199    /// An object saying the track ended somewhere the track has already passed.
200    ///
201    /// Section 9.1.1.1 describes Object Status 0x4, end of Track, as one whose
202    /// "GroupID is either the largest group produced in this track and the
203    /// ObjectID is one greater than the largest object produced in that
204    /// group, or GroupID is one greater than the largest group produced in
205    /// this track and the ObjectID is zero", and states the consequence:
206    /// "An object with this status that has a Group ID less than any other
207    /// GroupID, or an ObjectID less than or equal to the largest in the
208    /// specified group, is a protocol error, and the receiver MUST terminate
209    /// the session."
210    ///
211    /// # What this draft merged
212    ///
213    /// Drafts 08 through 10 spell this against a status that means end of
214    /// Track *and Group*, and carry a second status beside it for the end of
215    /// the Track alone. This draft folded the two into 0x4 and kept the
216    /// looser of the two conditions, which is why the Group ID may equal the
217    /// largest group here and may not there.
218    #[error(
219        "the end-of-track object at group {group}, object {object} on track alias \
220         {alias} is out of place: {placement}"
221    )]
222    EndOfTrackOutOfPlace {
223        /// The Track Alias the offending object named.
224        alias: u64,
225        /// The Group ID it named.
226        group: u64,
227        /// The Object ID it named.
228        object: u64,
229        /// Which half of the condition it broke, and what it was measured
230        /// against.
231        placement: EndOfTrackPlacement,
232    },
233    /// A SUBSCRIBE_UPDATE named an identifier no subscription the peer opened
234    /// has ever been given.
235    ///
236    /// Section 8.10: "A publisher SHOULD close the Session as a 'Protocol
237    /// Violation' if the SUBSCRIBE_UPDATE violates either rule or if the
238    /// subscriber specifies a Request ID that has not existed within the Session."
239    ///
240    /// **SHOULD**, so this is reported and the session is left running. From
241    /// draft-12 the same sentence says MUST, and there the session ends. An
242    /// endpoint that wants the close on these drafts has everything it needs
243    /// to make it: the error names the identifier that was not found.
244    ///
245    /// A subscription that has **ended** is not this: it existed. That is why
246    /// the record of an inbound SUBSCRIBE outlives the subscription, and why
247    /// an update naming an ended one is refused by the flow rather than by
248    /// this error.
249    #[error("SUBSCRIBE_UPDATE names request {0}, which no subscription the peer opened has had")]
250    UpdateForUnknownRequest(u64),
251
252    /// A Joining Fetch named a subscription this session cannot join.
253    ///
254    /// Section 8.13: "If a publisher receives a Joining Fetch with a Request ID that
255    /// does not correspond to an existing Subscribe in the same session, it
256    /// MUST respond with a Fetch Error with code Invalid Joining Subscribe ID."
257    ///
258    /// A refusal and not a session close, so the session runs on and the error
259    /// names both identifiers: the fetch to refuse, and the subscription it
260    /// asked to join.
261    #[error("FETCH {fetch} joins request {joining}, which is no live subscription of the peer's")]
262    UnjoinableSubscription {
263        /// The fetch that named it.
264        fetch: u64,
265        /// The identifier it named.
266        joining: u64,
267    },
268
269    /// A Joining Fetch was refused under a code other than the one the same
270    /// sentence names for it.
271    ///
272    /// The reason travels with the refusal, so a subscriber told the wrong one
273    /// retries the wrong thing: it can rebuild a fetch whose range was refused,
274    /// and cannot rebuild one whose subscription is gone.
275    #[error("refusing FETCH {fetch} for the subscription it joins takes error code {required}")]
276    WrongJoiningRefusal {
277        /// The fetch being refused.
278        fetch: u64,
279        /// The code the draft names for that refusal.
280        required: u64,
281    },
282    /// A message about an announcement named a namespace the peer has not
283    /// announced.
284    ///
285    /// Section 8.23 says what a cancellation is for: the subscriber "will stop
286    /// sending new subscriptions for tracks within the provided Track
287    /// Namespace". What a withdrawal ends and a cancellation revokes is an
288    /// announcement the **peer** made, so the record they reach for is the one
289    /// this endpoint keeps of the peer's announcements.
290    ///
291    /// Separate from [`EndpointError::UnknownNamespace`], which is the same
292    /// miss on the announcements this endpoint made, so a caller can tell which
293    /// of the two maps came up empty.
294    #[error("the peer has made no live announcement for this namespace")]
295    UnknownPeerNamespace,
296    /// A message about a namespace subscription named a prefix the peer has
297    /// not subscribed to.
298    ///
299    /// Section 5.1: "An UNSUBSCRIBE_ANNOUNCES withdraws a previous SUBSCRIBE_ANNOUNCES."
300    ///
301    /// What a withdrawal ends is a namespace subscription the **peer** made,
302    /// so the record it reaches for is the one this endpoint keeps of the
303    /// peer's. A namespace subscription this endpoint made is withdrawn by
304    /// [`Endpoint::unsubscribe_announces`], which is the same message travelling the other
305    /// way and answers with [`EndpointError::UnknownNamespace`].
306    #[error("the peer has made no live namespace subscription for this prefix")]
307    UnknownPeerNamespaceSubscription,
308    /// The peer subscribed to a namespace prefix overlapping one it is
309    /// already subscribed to.
310    ///
311    /// Section 8.24: "A subscriber cannot make overlapping namespace
312    /// subscriptions on a single session. Within a session, if a publisher
313    /// receives a SUBSCRIBE_ANNOUNCES with a Track Namespace Prefix that is a
314    /// prefix of an earlier SUBSCRIBE_ANNOUNCES or vice versa, it MUST
315    /// respond with SUBSCRIBE_ANNOUNCES_ERROR, with error code Namespace
316    /// Prefix Overlap."
317    ///
318    /// Taken when the message arrives, which is the moment the sentence
319    /// names, and read again when an answer is built: a request this endpoint
320    /// may not accept is one no later call can accept.
321    ///
322    /// The refusal itself is not this error. It is a message the peer is
323    /// owed, so the request is recorded like any other and refused through
324    /// the same call that refuses any other, under the code the sentence
325    /// names.
326    #[error(
327        "request {request} subscribes to a namespace prefix overlapping request {established}"
328    )]
329    PeerPrefixOverlap {
330        /// The request that arrived.
331        request: u64,
332        /// The namespace subscription it overlaps.
333        established: u64,
334    },
335    /// This endpoint was asked to subscribe to a namespace prefix overlapping
336    /// one it is already subscribed to.
337    ///
338    /// The first half of the same sentence, which is addressed to the
339    /// subscriber: "A subscriber cannot make overlapping namespace
340    /// subscriptions on a single session."
341    ///
342    /// The message is refused instead of built, and nothing else moves: no
343    /// Request ID is spent, no state machine is created and the session stays
344    /// as it was. The request never reaches the peer, so there is nothing for
345    /// the peer to refuse.
346    #[error("the namespace prefix overlaps request {established}, which this endpoint made")]
347    OwnPrefixOverlap {
348        /// The namespace subscription this endpoint already has.
349        established: u64,
350    },
351    /// A namespace subscription that overlaps another was refused under a
352    /// code other than the one the sentence names.
353    ///
354    /// The same shape as [`EndpointError::WrongJoiningRefusal`]: a rule that
355    /// names the code its refusal carries is not satisfied by a refusal under
356    /// any other, because the peer reads the code to learn what went wrong.
357    #[error("request {request} overlaps a namespace subscription and must be refused with code {required:#x}")]
358    WrongOverlapRefusal {
359        /// The request being refused.
360        request: u64,
361        /// The code the sentence names for it.
362        required: u64,
363    },
364}
365
366/// Whether two namespace prefixes overlap.
367///
368/// Section 8.24: "A subscriber cannot make overlapping namespace
369/// subscriptions on a single session. Within a session, if a publisher
370/// receives a SUBSCRIBE_ANNOUNCES with a Track Namespace Prefix that is a
371/// prefix of an earlier SUBSCRIBE_ANNOUNCES or vice versa, it MUST respond
372/// with SUBSCRIBE_ANNOUNCES_ERROR, with error code Namespace Prefix Overlap."
373///
374/// A namespace matches a namespace subscription when the subscription's
375/// prefix is a prefix of it, so two prefixes select overlapping sets of
376/// namespaces exactly when one of them is a prefix of the other. Equal
377/// prefixes are that case as well: every prefix is a prefix of itself, and
378/// two equal ones select the same set.
379fn prefixes_overlap(a: &[Vec<u8>], b: &[Vec<u8>]) -> bool {
380    let shared = a.len().min(b.len());
381    a[..shared] == b[..shared]
382}
383
384/// A SUBSCRIBE the peer sent, and how far the subscription it opened has got.
385struct InboundSubscribe {
386    /// The message as it arrived, which is what the application answers from.
387    message: Subscribe,
388    /// The subscription's state, driven from the publisher's end.
389    state: SubscriptionStateMachine,
390}
391/// A FETCH the peer sent, and how far the fetch it opened has got.
392struct InboundFetch {
393    /// The message as it arrived, which is what the application answers from.
394    message: Fetch,
395    /// The fetch's state, driven from the end that serves it.
396    state: FetchStateMachine,
397    /// The subscription a Joining Fetch named and this session had none live
398    /// for when the FETCH arrived, which is when the rule about it is read.
399    unjoinable: Option<u64>,
400}
401
402/// A Track Alias attached to a Full Track Name, and the request whose lifetime
403/// the attachment follows.
404///
405/// SUBSCRIBE carries the alias and the track in the one message, whichever end
406/// sends it, so a binding is complete from the moment it is made. That stops
407/// being true at draft-12, where the alias arrives in the answer instead.
408#[derive(Debug, Clone)]
409struct TrackBinding {
410    namespace: TrackNamespace,
411    name: Vec<u8>,
412    alias: u64,
413    kind: BindingKind,
414}
415
416/// Which end sent the SUBSCRIBE a binding came from, and so which record says
417/// whether its subscription is still standing.
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
419enum BindingKind {
420    /// A SUBSCRIBE this endpoint sent, carrying the alias it chose.
421    Ours,
422    /// A SUBSCRIBE the peer sent, carrying the alias the peer chose.
423    Peers,
424}
425
426impl EndpointError {
427    /// Whose doing this is — the peer's, or this endpoint's, or a variant that
428    /// cannot say.
429    ///
430    /// The companion of [`EndpointError::session_error_code`], which answers
431    /// *what the draft requires be done about it*. Neither answers the other's
432    /// question and the pair is what a caller needs: a code without a side
433    /// names nobody, and a side without a code is not grounds to publish
434    /// anything.
435    ///
436    /// Exhaustive, with no wildcard arm, so a variant added to this draft's
437    /// `EndpointError` is a compile error here rather than a silent arrival on
438    /// the wrong side of the answer. See
439    /// [`EndpointFault`](crate::above_codec_rules::EndpointFault) for the three
440    /// answers and for the collision that made the third one necessary.
441    pub fn fault(&self) -> crate::above_codec_rules::EndpointFault {
442        use crate::above_codec_rules::{AboveCodecRule as Rule, EndpointFault as Fault};
443
444        match self {
445            // Raised on both a receive path and a send path, so the
446            // variant cannot say which end is at fault. The state machines
447            // render as `invalid transition from X on event Y` whichever end
448            // asked for the transition, and the unknown-request errors name
449            // an id that may be one the peer sent or one a caller here made
450            // up.
451            EndpointError::Session(..)
452            | EndpointError::Subscription(..)
453            | EndpointError::Fetch(..)
454            | EndpointError::Namespace(..)
455            | EndpointError::TrackStatus(..)
456            | EndpointError::Setup(..)
457            | EndpointError::UnknownRequest(..)
458            | EndpointError::UnknownNamespace
459            | EndpointError::UnknownPeerNamespace
460            | EndpointError::UnknownPeerNamespaceSubscription => Fault::EitherEnd,
461
462            // Raised on the way out. Nothing reached the wire, so none of
463            // these is evidence about a peer — including the ones a peer
464            // caused, where what failed is this side's attempt to accept
465            // something the draft says to refuse.
466            EndpointError::MaxRequestIdWouldNotIncrease { .. }
467            | EndpointError::NotActive
468            | EndpointError::Draining
469            | EndpointError::FilterNeedsRange
470            | EndpointError::TrackAliasInUse { .. }
471            | EndpointError::UnjoinableSubscription { .. }
472            | EndpointError::WrongJoiningRefusal { .. }
473            | EndpointError::PeerPrefixOverlap { .. }
474            | EndpointError::OwnPrefixOverlap { .. }
475            | EndpointError::WrongOverlapRefusal { .. } => Fault::ThisEndpoint,
476
477            // Raised reading what the peer sent.
478            EndpointError::DuplicateTrackAlias { .. } => Fault::Peer(Rule::DuplicateTrackAlias),
479            EndpointError::EndOfTrackOutOfPlace { .. } => Fault::Peer(Rule::EndOfTrackOutOfPlace),
480            EndpointError::GoAwayUriAtServer => Fault::Peer(Rule::GoAwayAtServer),
481            EndpointError::MixedForwardingPreference { .. } => {
482                Fault::Peer(Rule::MixedForwardingPreference)
483            }
484            EndpointError::RepeatedGoAway => Fault::Peer(Rule::RepeatedGoAway),
485            EndpointError::UpdateForUnknownRequest(..) => {
486                Fault::Peer(Rule::RequestUpdateForTheWrongRequest)
487            }
488
489            // The Request ID rules, which are the peer's whenever they are
490            // read off the wire. The mirror — this endpoint asked to advertise
491            // a ceiling that does not increase — is
492            // `MaxRequestIdWouldNotIncrease` above, which is a variant of its
493            // own so that the two never arrive as one value.
494            EndpointError::RequestId(e) => match e {
495                RequestIdError::Decreased(..) => Fault::Peer(Rule::MaxRequestIdDecreased),
496                RequestIdError::ExceedsMax(..) => Fault::Peer(Rule::RequestIdCeiling),
497                RequestIdError::WrongParity(..) => Fault::Peer(Rule::RequestIdParity),
498                RequestIdError::OutOfSequence { .. } => Fault::Peer(Rule::RequestIdOutOfSequence),
499                // This endpoint has spent the budget the peer granted it.
500                RequestIdError::Blocked => Fault::ThisEndpoint,
501            },
502        }
503    }
504
505    /// The code to close the session with, when draft-11 answers this error
506    /// with a close rather than leaving it to the one request it concerns.
507    ///
508    /// `None` means the error is recoverable: the caller may report it, give
509    /// up on the request it concerns, and keep the session running. `Some`
510    /// means the draft ends the session, and the endpoint has already moved
511    /// its own state to Closed - the code is what the transport should carry.
512    ///
513    /// The table grows one rule at a time, and a rule joins it with a gate
514    /// that drives the bytes at a real connection and reads the close code
515    /// back off the wire. An arm added without one asserts nothing: from
516    /// inside the process the session ends either way, and only the peer can
517    /// tell the difference.
518    pub fn session_error_code(&self) -> Option<SessionErrorCode> {
519        match self {
520            // Section 8.5 answers a ceiling that does not increase with a
521            // close, and names this code for it.
522            EndpointError::RequestId(RequestIdError::Decreased(..)) => {
523                Some(SessionErrorCode::ProtocolViolation)
524            }
525            // The same section answers a Request ID that reaches the ceiling
526            // this endpoint advertised, and names a different code for it.
527            EndpointError::RequestId(RequestIdError::ExceedsMax(..)) => {
528                Some(SessionErrorCode::TooManyRequests)
529            }
530            // Section 8.1 answers a Request ID that is not the peer's to
531            // spend with a close, and names Invalid Request ID for it. Both
532            // halves of that sentence arrive here: "not valid for the peer"
533            // is an ID out of this endpoint's own half of the space, and "not
534            // expected" is a new request carrying an ID other than the next
535            // one the peer's sequence calls for.
536            EndpointError::RequestId(
537                RequestIdError::WrongParity(..) | RequestIdError::OutOfSequence { .. },
538            ) => Some(SessionErrorCode::InvalidRequestId),
539            // Section 8.4 answers a GOAWAY that repeats one already
540            // received, and names this code in the same sentence.
541            EndpointError::RepeatedGoAway => Some(SessionErrorCode::ProtocolViolation),
542            // The same section answers a migration URI arriving at a server
543            // with a close, and names this code for it. Only a server may
544            // offer one, so a client that sends one is telling a server where
545            // to reconnect, which it has no standing to do.
546            EndpointError::GoAwayUriAtServer => Some(SessionErrorCode::ProtocolViolation),
547            // Section 8.7 answers a SUBSCRIBE whose Track Alias already
548            // names a different track with a session close, and Section 8.9
549            // answers the retry alias a SUBSCRIBE_ERROR offers the same way.
550            // Section 3.4 names this code for both.
551            EndpointError::DuplicateTrackAlias { .. } => {
552                Some(SessionErrorCode::DuplicateTrackAlias)
553            }
554            // Section 9 answers a track whose objects mix forwarding
555            // preferences, and names this code in the same sentence: "it SHOULD
556            // close the session with an error of 'Protocol Violation'".
557            //
558            // SHOULD, so the close is the caller's to make. The code lives here
559            // and `Connection::close_for_data_stream` is what carries it, the
560            // same opt-in every other rule broken on a data stream takes.
561            EndpointError::MixedForwardingPreference { .. } => {
562                Some(SessionErrorCode::ProtocolViolation)
563            }
564            // Section 9.1.1.1 answers an end-of-track object in the wrong place
565            // with "the receiver MUST terminate the session", and names no code
566            // in that sentence. The paragraph it closes names one for the
567            // status field's other failure — "Any other value SHOULD be treated
568            // as a protocol error and terminate the session with a Protocol
569            // Violation" — and it is the code this draft's decoder already
570            // gives the half of the same rule it can settle from one header.
571            EndpointError::EndOfTrackOutOfPlace { .. } => Some(SessionErrorCode::ProtocolViolation),
572            _ => None,
573        }
574    }
575}
576
577/// Unified draft-11 MoQT endpoint wrapping session lifecycle, request ID
578/// allocation, and all per-flow state machines (subscriptions, fetches,
579/// announces, subscribe-announces, track statuses).
580pub struct Endpoint {
581    /// Which end of the session this is, which fixes the parity of the
582    /// request IDs it allocates.
583    role: Role,
584    session: SessionStateMachine,
585    request_ids: RequestIdAllocator,
586    /// Tracks the MAX_REQUEST_ID we have advertised to the peer.
587    advertised_max_id: u64,
588    subscriptions: HashMap<u64, SubscriptionStateMachine>,
589    /// Subscriptions the peer opened with SUBSCRIBE, each from the moment its
590    /// message arrived to the end of the flow.
591    ///
592    /// Separate from `subscriptions`, which holds the ones this endpoint
593    /// opened. The two never collide on an identifier - Request IDs carry the
594    /// allocating end's parity - but a subscription's state is read from
595    /// whichever end drives it, and the two ends drive different messages.
596    inbound_subscribes: HashMap<u64, InboundSubscribe>,
597    /// Every FETCH the peer has sent, from arrival to the end of the fetch.
598    ///
599    /// Separate from `fetches`, which holds the ones this endpoint made. The
600    /// record holds the FETCH itself and not only its state, because the
601    /// answer is built out of the request: a Joining Fetch has to be judged
602    /// against the subscription it names, and that name is nowhere else.
603    inbound_fetches: HashMap<u64, InboundFetch>,
604    /// Every Track Alias in use in this session, and the track each one names.
605    ///
606    /// "Already being used" is what makes this a table rather than a set: an
607    /// alias whose subscription has ended is free again. The table records the
608    /// binding and reads liveness back off the subscription's own state
609    /// machine, rather than keeping a second copy of it that every path ending
610    /// a subscription would have to remember to prune.
611    /// What each track's objects have been framed as, so far.
612    ///
613    /// Behind a lock because this is the one endpoint fact a *data* stream
614    /// settles, and the data plane reaches the endpoint through `&Connection`:
615    /// a caller may hold one across tasks while it reads streams and datagrams,
616    /// so there is no `&mut` to reach the rest of this struct with.
617    forwarding_preferences: Mutex<TrackForwardingPreferences>,
618    /// How far each track's objects have reached, so far.
619    ///
620    /// Behind an `Arc` rather than beside the rest of this struct because the
621    /// objects that settle it are read off stream handles the caller owns, one
622    /// at a time, with no way back to the endpoint. Each such stream is handed a
623    /// clone of the handle, and every clone measures against this one record.
624    locations: Arc<Mutex<TrackLocations>>,
625    track_bindings: HashMap<u64, TrackBinding>,
626    fetches: HashMap<u64, FetchStateMachine>,
627    subscribe_announces: HashMap<u64, SubscribeAnnouncesStateMachine>,
628    /// Namespace subscriptions the **peer** made, keyed by the Request ID it
629    /// opened each under.
630    ///
631    /// Kept apart from `subscribe_announces`, which holds the ones this endpoint made:
632    /// one Request ID names one request whichever end opened it, and the two
633    /// ends answer opposite halves of the flow.
634    inbound_subscribe_announces: HashMap<u64, InboundSubscribeAnnounces>,
635    announces: HashMap<u64, AnnounceStateMachine>,
636    /// Announcements the **peer** made, keyed by the Request ID it
637    /// opened each under.
638    inbound_announces: HashMap<u64, InboundAnnounce>,
639    /// Maps namespace tuple -> request_id, so callers can UNANNOUNCE / cancel
640    /// by namespace without threading the id through every API.
641    announce_ids: HashMap<NamespaceKey, u64>,
642    /// Maps namespace prefix tuple -> request_id for subscribe-announces.
643    subscribe_announces_ids: HashMap<NamespaceKey, u64>,
644    track_statuses: HashMap<u64, TrackStatusStateMachine>,
645    /// Track statuses the **peer** asked about, keyed by the Request ID it
646    /// asked under.
647    ///
648    /// Kept apart from `track_statuses`, which holds the ones this endpoint
649    /// asked about: the two are answered by opposite ends, and one Request ID
650    /// belongs to one request whichever end opened it.
651    inbound_track_statuses: HashMap<u64, InboundTrackStatus>,
652    negotiated_version: Option<VarInt>,
653    offered_versions: Vec<VarInt>,
654    goaway_uri: Option<Vec<u8>>,
655    /// The most recent `maximum_request_id` reported by the peer via a
656    /// `REQUESTS_BLOCKED` message.
657    peer_reported_max_request_id: Option<VarInt>,
658}
659
660/// An announcement the peer made with ANNOUNCE.
661///
662/// Kept apart from the announcements this endpoint made because the two are
663/// answered by opposite ends: this one is waiting for an answer from here,
664/// and the other for one from the peer.
665struct InboundAnnounce {
666    /// The message as it arrived, which is what the application answers from.
667    message: Announce,
668    /// How far the announcement it makes has got.
669    state: AnnounceStateMachine,
670}
671/// A track status the peer asked for with TRACK_STATUS_REQUEST.
672///
673/// Kept apart from the ones this endpoint asked for because the two are
674/// answered by opposite ends: this one is waiting for an answer from here,
675/// and the other for one from the peer.
676struct InboundTrackStatus {
677    /// The message as it arrived, which is what the answer is built from.
678    message: TrackStatusRequest,
679    /// How far the request it opened has got.
680    state: TrackStatusStateMachine,
681}
682/// A SUBSCRIBE_ANNOUNCES the peer sent, and how far the namespace subscription it opens
683/// has got.
684///
685/// Kept apart from `subscribe_announces`, which holds the ones this endpoint made: the
686/// two are answered by opposite ends, and this one is waiting for an answer
687/// from here.
688struct InboundSubscribeAnnounces {
689    /// The message as it arrived, which is what the answer is built from.
690    message: SubscribeAnnounces,
691    /// How far the namespace subscription it opens has got.
692    state: SubscribeAnnouncesStateMachine,
693    /// The namespace subscription this one overlapped when it arrived,
694    /// which is when the rule about it is read.
695    overlaps: Option<u64>,
696}
697impl Endpoint {
698    /// Create a new draft-11 endpoint for the given role.
699    pub fn new(role: Role) -> Self {
700        Self {
701            role,
702            session: SessionStateMachine::new(),
703            request_ids: RequestIdAllocator::new(role),
704            advertised_max_id: 0,
705            subscriptions: HashMap::new(),
706            inbound_subscribes: HashMap::new(),
707            inbound_fetches: HashMap::new(),
708            track_bindings: HashMap::new(),
709            forwarding_preferences: Mutex::new(TrackForwardingPreferences::new()),
710            locations: Arc::new(Mutex::new(TrackLocations::new())),
711            fetches: HashMap::new(),
712            subscribe_announces: HashMap::new(),
713            inbound_subscribe_announces: HashMap::new(),
714            announces: HashMap::new(),
715            inbound_announces: HashMap::new(),
716            announce_ids: HashMap::new(),
717            subscribe_announces_ids: HashMap::new(),
718            track_statuses: HashMap::new(),
719            inbound_track_statuses: HashMap::new(),
720            negotiated_version: None,
721            offered_versions: Vec::new(),
722            goaway_uri: None,
723            peer_reported_max_request_id: None,
724        }
725    }
726
727    // ── Track aliases ──────────────────────────────────────────
728
729    /// The request already using `alias` for a track other than (`namespace`,
730    /// `name`), or `None` when the alias is free for that track.
731    ///
732    /// # Why the set is read rather than kept
733    ///
734    /// Section 8.7 says "already being used", and a subscription that has
735    /// ended is not using anything. Asking each binding's own state machine is
736    /// what makes an alias free again the instant its track's subscription
737    /// ends, with nothing to prune on the way out - and a path that ended a
738    /// subscription without telling this table would otherwise hold the alias
739    /// forever and refuse the peer's next, conforming, use of it.
740    ///
741    /// # Why a binding for the same track is not a conflict
742    ///
743    /// The rule is about a Track Alias naming two tracks, not about naming one
744    /// track twice. A second subscription to the track an alias already names
745    /// breaks nothing this section states.
746    fn alias_holder(&self, alias: u64, namespace: &TrackNamespace, name: &[u8]) -> Option<u64> {
747        self.track_bindings.iter().find_map(|(&id, binding)| {
748            let other_track = binding.namespace != *namespace || binding.name != name;
749            (binding.alias == alias && other_track && self.binding_is_live(id, binding.kind))
750                .then_some(id)
751        })
752    }
753
754    /// The lowest Track Alias no live binding has given to a track.
755    ///
756    /// Drafts 07 through 11 make the **subscriber** choose the Track Alias, and
757    /// require it to name one track per session; draft-12 moved the field to
758    /// SUBSCRIBE_OK and made the choice the publisher's. A client spanning both
759    /// eras therefore has to supply a value on these drafts and cannot on the
760    /// later ones, so the value is read off the endpoint rather than asked of
761    /// the caller: that is what lets
762    /// [`crate::dispatch::AnyConnection::subscribe`] carry one signature across
763    /// all the drafts instead of an argument that does nothing on nine of
764    /// them.
765    ///
766    /// Read rather than kept, for the same reason the alias table beside it
767    /// gives: a binding whose request has ended holds nothing, so an alias
768    /// falls free when its subscription does and may name a different track
769    /// next. A caller that mixes this with aliases of its own choosing stays
770    /// correct by construction, because both read this one table — and
771    /// `subscribe` refuses a duplicate before the alias reaches the wire
772    /// either way.
773    pub fn next_free_track_alias(&self) -> VarInt {
774        let taken: std::collections::BTreeSet<u64> = self
775            .track_bindings
776            .iter()
777            .filter(|(&id, binding)| self.binding_is_live(id, binding.kind))
778            .map(|(_, binding)| binding.alias)
779            .collect();
780        let mut candidate = 0;
781        while taken.contains(&candidate) {
782            candidate += 1;
783        }
784        VarInt::from_u64_moqt(candidate)
785    }
786
787    /// The track a live binding has given `alias` to.
788    ///
789    /// Read rather than kept, for the reason the alias table beside it gives: a
790    /// binding whose request has ended holds nothing, and an alias that is free
791    /// again may name a different track next. That is exactly why the
792    /// forwarding-preference record below is keyed on the track this returns
793    /// and never on the alias itself.
794    fn track_for_alias(&self, alias: u64) -> Option<(&TrackNamespace, &[u8])> {
795        self.track_bindings.iter().find_map(|(&id, binding)| {
796            (binding.alias == alias && self.binding_is_live(id, binding.kind))
797                .then_some((&binding.namespace, binding.name.as_slice()))
798        })
799    }
800
801    /// The record a stream carrying `alias`'s objects measures them against.
802    ///
803    /// `None` for an alias no live binding names: an object for one breaks a
804    /// different rule, and measuring it against a track this endpoint never
805    /// asked for would answer that one with the wrong sentence.
806    pub fn track_objects(&self, alias: u64) -> Option<TrackObjects> {
807        let (namespace, name) = self.track_for_alias(alias)?;
808        Some(TrackObjects::new(
809            Arc::clone(&self.locations),
810            namespace.clone(),
811            name.to_vec(),
812            alias,
813        ))
814    }
815
816    /// Record or judge one object that arrived outside a subgroup stream, and
817    /// report Section 9.1.1.1's protocol error when it says the track ended
818    /// somewhere the track has already passed.
819    ///
820    /// `&self`, because the call site is the data plane's.
821    pub fn note_received_object(
822        &self,
823        alias: u64,
824        at: ObjectLocation,
825        role: ObjectRole,
826    ) -> Result<(), EndpointError> {
827        let Some(objects) = self.track_objects(alias) else { return Ok(()) };
828        objects.note(at, role).map_err(|placement| EndpointError::EndOfTrackOutOfPlace {
829            alias,
830            group: at.group,
831            object: at.object,
832            placement,
833        })
834    }
835
836    /// Record how a track's object was framed, and report Section 9's "MUST NOT
837    /// mix" when it disagrees with what that track's earlier objects used.
838    ///
839    /// `&self`, because the call sites are the data plane's: a subgroup header
840    /// arriving, a datagram arriving, and the two writers that produce them.
841    ///
842    /// An alias no live binding names records nothing and reports nothing. An
843    /// object for such an alias breaks a different rule — the one about objects
844    /// nobody asked for — and answering that one here would answer it with the
845    /// wrong sentence.
846    pub fn note_object_forwarding_preference(
847        &self,
848        alias: u64,
849        seen: ObjectForwardingPreference,
850    ) -> Result<(), EndpointError> {
851        let Some((namespace, name)) = self.track_for_alias(alias) else { return Ok(()) };
852        self.forwarding_preferences
853            .lock()
854            .unwrap_or_else(|poisoned| poisoned.into_inner())
855            .observe(namespace, name, seen)
856            .map_err(|established| EndpointError::MixedForwardingPreference {
857                alias,
858                established,
859                offered: seen,
860            })
861    }
862
863    /// Whether the subscription that owns a binding is still standing.
864    /// Subscribing counts as well as Active, which is what separates this draft
865    /// from the next one: draft-12 Section 8.8 reads "a different track with an
866    /// active subscription" and the alias arrives in the answer, so only an
867    /// answered request holds one. Here the alias is in the SUBSCRIBE itself,
868    /// and the sentence puts no qualifier on "already being used" - so it is in
869    /// use from the moment that message is sent or received, and stays in use
870    /// until the subscription ends.
871    fn binding_is_live(&self, id: u64, kind: BindingKind) -> bool {
872        let state = match kind {
873            BindingKind::Ours => self.subscriptions.get(&id).map(|sm| sm.state()),
874            BindingKind::Peers => self.inbound_subscribes.get(&id).map(|s| s.state.state()),
875        };
876        matches!(state, Some(SubscriptionState::Subscribing | SubscriptionState::Active))
877    }
878
879    /// The close Section 8.7 requires of an arriving SUBSCRIBE whose Track
880    /// Alias is spoken for, or `None` when it is free.
881    fn conflicting_track_alias(
882        &self,
883        id: u64,
884        alias: u64,
885        namespace: &TrackNamespace,
886        name: &[u8],
887    ) -> Option<EndpointError> {
888        let established = self.alias_holder(alias, namespace, name)?;
889        Some(EndpointError::DuplicateTrackAlias { alias, established, offered: id })
890    }
891
892    /// The close Section 8.9 requires of a SUBSCRIBE_ERROR offering a Track
893    /// Alias to retry with, or `None` when the offer can be taken up.
894    ///
895    /// The track is not in the SUBSCRIBE_ERROR: it is the one this endpoint's
896    /// own SUBSCRIBE asked for, so the request has to be looked up before the
897    /// alias offered for it can be judged. An offer of an alias this endpoint
898    /// already holds for that same track is the retry succeeding, not a
899    /// conflict.
900    fn conflicting_retry_alias(&self, id: u64, alias: u64) -> Option<EndpointError> {
901        let binding = self.track_bindings.get(&id)?;
902        let established = self.alias_holder(alias, &binding.namespace, &binding.name)?;
903        Some(EndpointError::DuplicateTrackAlias { alias, established, offered: id })
904    }
905
906    // ── Accessors ──────────────────────────────────────────────
907
908    /// Returns the role (client or server) of this endpoint.
909    pub fn role(&self) -> Role {
910        self.role
911    }
912
913    /// Returns the current session state.
914    pub fn session_state(&self) -> SessionState {
915        self.session.state()
916    }
917
918    /// Returns the negotiated MoQT version, if setup is complete.
919    pub fn negotiated_version(&self) -> Option<VarInt> {
920        self.negotiated_version
921    }
922
923    /// Returns the URI from a received GOAWAY message, if any.
924    pub fn goaway_uri(&self) -> Option<&[u8]> {
925        self.goaway_uri.as_deref()
926    }
927
928    /// Returns whether this endpoint is blocked on request ID allocation.
929    pub fn is_blocked(&self) -> bool {
930        self.request_ids.is_blocked()
931    }
932
933    /// Returns the number of active subscription state machines.
934    pub fn active_subscription_count(&self) -> usize {
935        self.subscriptions.len()
936    }
937
938    /// Returns the number of active fetch state machines.
939    pub fn active_fetch_count(&self) -> usize {
940        self.fetches.len()
941    }
942
943    /// Returns the number of active subscribe-announces state machines.
944    pub fn active_subscribe_announces_count(&self) -> usize {
945        self.subscribe_announces.len()
946    }
947
948    /// Returns the number of active announce state machines.
949    pub fn active_announce_count(&self) -> usize {
950        self.announces.len()
951    }
952
953    /// Returns the number of active track status state machines.
954    pub fn active_track_status_count(&self) -> usize {
955        self.track_statuses.len()
956    }
957
958    // ── Session lifecycle ──────────────────────────────────────
959
960    /// Transition from Connecting to SetupExchange.
961    pub fn connect(&mut self) -> Result<(), EndpointError> {
962        self.session.on_connect()?;
963        Ok(())
964    }
965
966    /// Close the session (SetupExchange, Active or Draining -> Closed).
967    pub fn close(&mut self) -> Result<(), EndpointError> {
968        self.session.on_close()?;
969        Ok(())
970    }
971
972    // ── Client setup ───────────────────────────────────────────
973
974    /// Generate a CLIENT_SETUP message (client-side).
975    pub fn send_client_setup(
976        &mut self,
977        versions: Vec<VarInt>,
978        parameters: Vec<KeyValuePair>,
979    ) -> Result<ControlMessage, EndpointError> {
980        self.offered_versions = versions.clone();
981        let msg = ClientSetup { supported_versions: versions, parameters };
982        setup::validate_client_setup(&msg)?;
983        // A MAX_REQUEST_ID parameter in our own CLIENT_SETUP is a grant to
984        // the peer, so it is the first value of the ceiling we advertise.
985        self.record_advertised_max(&msg.parameters);
986        Ok(ControlMessage::ClientSetup(msg))
987    }
988
989    /// Process a SERVER_SETUP message (client-side). Transitions to Active.
990    /// If the server includes a MAX_REQUEST_ID parameter (key 0x02), the
991    /// request ID allocator is initialized with that value.
992    pub fn receive_server_setup(&mut self, msg: &ServerSetup) -> Result<(), EndpointError> {
993        setup::validate_server_setup(msg)?;
994        let version = setup::negotiate_version(&self.offered_versions, msg.selected_version)?;
995        self.negotiated_version = Some(version);
996        self.session.on_setup_complete()?;
997        // Extract MAX_REQUEST_ID (key 0x02) from setup parameters if present
998        for param in &msg.parameters {
999            if param.key == VarInt::from_u64(0x02).unwrap() {
1000                if let KvpValue::Varint(v) = &param.value {
1001                    self.request_ids.update_max(v.into_inner())?;
1002                }
1003            }
1004        }
1005        Ok(())
1006    }
1007
1008    // ── Server setup ───────────────────────────────────────────
1009
1010    /// Process CLIENT_SETUP and generate SERVER_SETUP (server-side).
1011    pub fn receive_client_setup_and_respond(
1012        &mut self,
1013        client_setup: &ClientSetup,
1014        selected_version: VarInt,
1015    ) -> Result<ControlMessage, EndpointError> {
1016        setup::validate_client_setup(client_setup)?;
1017        // Section 8.3.2.2 puts no role restriction on MAX_REQUEST_ID, so a
1018        // CLIENT_SETUP may carry it and it grants this endpoint its budget.
1019        for param in &client_setup.parameters {
1020            if param.key == VarInt::from_u64(0x02).unwrap() {
1021                if let KvpValue::Varint(v) = &param.value {
1022                    self.request_ids.update_max(v.into_inner())?;
1023                }
1024            }
1025        }
1026        let version = setup::negotiate_version(&client_setup.supported_versions, selected_version)?;
1027        self.negotiated_version = Some(version);
1028        self.session.on_setup_complete()?;
1029        let msg = ServerSetup { selected_version: version, parameters: vec![] };
1030        Ok(ControlMessage::ServerSetup(msg))
1031    }
1032
1033    // ── MAX_REQUEST_ID ─────────────────────────────────────────
1034
1035    /// Take a Request ID from a new request the peer sent, holding it to all
1036    /// three halves of the rule.
1037    ///
1038    /// Section 8.1 closes the session with Invalid Request ID on a request ID
1039    /// "that is not valid for the peer", and Section 8.5 closes it with
1040    /// 'Too Many Requests' on one at or above the ceiling this endpoint
1041    /// advertised. The parity half belongs to the allocator; the ceiling half
1042    /// needs `advertised_max_id`, which is this endpoint's grant to the peer
1043    /// and not the peer's grant to it, so the two numbers are different and
1044    /// only one of them answers this question.
1045    ///
1046    /// The third is the sequence. The same sentence closes the session on "a
1047    /// new request with a Request ID that is not expected", and what is
1048    /// expected is fixed by the two lines above it: each endpoint starts at 0
1049    /// or 1 by role and steps by 2 per request. So a repeat and a skip are both
1050    /// refused, and neither is caught by parity or by the ceiling - a repeat has
1051    /// the right parity and sits below the ceiling by construction, having been
1052    /// accepted once already.
1053    ///
1054    /// Taking the ID is what advances the sequence, so this is for a **new**
1055    /// request only. A response, a cancellation or an update that names the
1056    /// request it modifies all carry an ID that has already been spent, and
1057    /// passing one here would refuse it.
1058    ///
1059    /// # The ceiling is a session rule, not a request rule
1060    ///
1061    /// Section 8.5: a Request ID "equal or larger than this" received by the
1062    /// endpoint that sent the MAX_REQUEST_ID in any request message - the list
1063    /// [`Self::receive_request`] carries - closes the session, and 'Too Many
1064    /// Requests' is the code. Which is also why the number measured against is
1065    /// the one **this** endpoint sent. An id that reaches the ceiling is not a
1066    /// request to refuse with an error message: the session is over, so this
1067    /// moves the endpoint's own state to Closed and leaves the code to
1068    /// [`EndpointError::session_error_code`].
1069    ///
1070    /// # Errors
1071    ///
1072    /// [`RequestIdError::WrongParity`] if the ID belongs to this endpoint's
1073    /// half of the space, [`RequestIdError::ExceedsMax`] if it is not
1074    /// below the advertised ceiling, or [`RequestIdError::OutOfSequence`] if it
1075    /// is not the one the peer's sequence called for. A ceiling that was never
1076    /// raised is 0, which refuses every ID, matching a default that reads "the
1077    /// peer MUST NOT send requests".
1078    ///
1079    /// All three end the session rather than the one request, so all three
1080    /// move this endpoint's own session to Closed on the way out and all three
1081    /// answer `Some` from [`EndpointError::session_error_code`]. Returning one
1082    /// of them without ending the session would leave a peer that broke the
1083    /// rule free to keep sending, and never told.
1084    pub fn validate_peer_request_id(&mut self, id: u64) -> Result<(), EndpointError> {
1085        if let Err(e) = self.request_ids.validate_peer_id(id) {
1086            return Err(self.fail_session(EndpointError::RequestId(e)));
1087        }
1088        if id >= self.advertised_max_id {
1089            return Err(self.fail_session(EndpointError::RequestId(RequestIdError::ExceedsMax(
1090                id,
1091                self.advertised_max_id,
1092            ))));
1093        }
1094        if let Err(e) = self.request_ids.record_peer_id(id) {
1095            return Err(self.fail_session(EndpointError::RequestId(e)));
1096        }
1097        Ok(())
1098    }
1099
1100    /// Record a MAX_REQUEST_ID parameter this endpoint is about to send as
1101    /// the ceiling it has advertised to the peer.
1102    fn record_advertised_max(&mut self, parameters: &[KeyValuePair]) {
1103        for param in parameters {
1104            if param.key == VarInt::from_u64(0x02).unwrap() {
1105                if let KvpValue::Varint(v) = &param.value {
1106                    self.advertised_max_id = v.into_inner();
1107                }
1108            }
1109        }
1110    }
1111
1112    /// Process an incoming MAX_REQUEST_ID message, ending the session if the
1113    /// ceiling it carries does not increase.
1114    /// Section 8.5: "The Maximum Request ID MUST only increase within a
1115    /// session, and receipt of a MAX_REQUEST_ID message with an equal or
1116    /// smaller Request ID value is a 'Protocol Violation'." Section 3.4 lists
1117    /// Protocol Violation among the codes for terminating the session - "The
1118    /// remote endpoint performed an action that was disallowed by the
1119    /// specification" - so naming it of a *receipt* is this draft saying the
1120    /// session ends, and with which code. Draft-16 Section 9.5 states the same
1121    /// rule with the verb in it: "it MUST close the session with a
1122    /// PROTOCOL_VIOLATION".
1123    ///
1124    /// # Errors
1125    ///
1126    /// [`RequestIdError::Decreased`] if the value does not increase, with the
1127    /// session already moved to Closed.
1128    pub fn receive_max_request_id(&mut self, msg: &MaxRequestId) -> Result<(), EndpointError> {
1129        if let Err(err) = self.request_ids.update_max(msg.request_id.into_inner()) {
1130            return Err(self.fail_session(err.into()));
1131        }
1132        Ok(())
1133    }
1134
1135    /// Generate a MAX_REQUEST_ID message (typically server-side).
1136    ///
1137    /// Section 8.5: "The Maximum Request ID MUST only increase within a
1138    /// session", and a peer that receives an equal or smaller value closes
1139    /// the session. The ceiling starts at 0 and 0 is not greater than 0, so
1140    /// the first value that may go on the wire is 1 and there is no opening
1141    /// case where a repeat is allowed.
1142    ///
1143    /// # Errors
1144    ///
1145    /// [`EndpointError::MaxRequestIdWouldNotIncrease`] if the value does not
1146    /// strictly increase. Its own variant rather than the one a *received*
1147    /// ceiling that did not increase raises, so that a refusal to write is
1148    /// never read back as a peer in violation.
1149    pub fn send_max_request_id(&mut self, max_id: VarInt) -> Result<ControlMessage, EndpointError> {
1150        let new_val = max_id.into_inner();
1151        if new_val <= self.advertised_max_id {
1152            return Err(EndpointError::MaxRequestIdWouldNotIncrease {
1153                advertised: self.advertised_max_id,
1154                offered: new_val,
1155            });
1156        }
1157        self.advertised_max_id = new_val;
1158        Ok(ControlMessage::MaxRequestId(MaxRequestId { request_id: max_id }))
1159    }
1160
1161    // ── GoAway ─────────────────────────────────────────────────
1162
1163    /// Process an incoming GOAWAY message. Transitions to Draining.
1164    ///
1165    /// # Errors
1166    ///
1167    /// [`EndpointError::GoAwayUriAtServer`] if this endpoint is the server and
1168    /// the GOAWAY carries a New Session URI. The session is over: this
1169    /// endpoint's own state has moved to Closed and the code the transport
1170    /// should close with is in [`EndpointError::session_error_code`].
1171    ///
1172    /// [`EndpointError::RepeatedGoAway`] if a GOAWAY has already been
1173    /// received. The session is over: this endpoint's own state has moved to
1174    /// Closed and the code the transport should close with is in
1175    /// [`EndpointError::session_error_code`].
1176    pub fn receive_goaway(&mut self, msg: &GoAway) -> Result<(), EndpointError> {
1177        // Section 8.4: "If a server receives a GOAWAY with a non-zero New
1178        // Session URI Length it MUST terminate the session with a Protocol
1179        // Violation." Refused before the URI is stored rather than
1180        // after, so an application reading `goaway_uri` back can never be
1181        // handed somewhere a client chose to send it. The session ends with
1182        // it: the sentence names a close and a code, and an endpoint that
1183        // raised the error and carried on would keep serving a peer it had
1184        // just found in violation.
1185        if self.role == Role::Server && !msg.new_session_uri.is_empty() {
1186            return Err(self.fail_session(EndpointError::GoAwayUriAtServer));
1187        }
1188        // Section 8.4: "The endpoint MUST terminate the session with a
1189        // Protocol Violation (Section 3.4) if it receives multiple GOAWAY messages."
1190        // Draining is reached from nowhere else - `on_goaway` is its only
1191        // entry and this method is that method's only caller - so the session
1192        // state is the record of the first GOAWAY having arrived.
1193        if self.session.state() == SessionState::Draining {
1194            return Err(self.fail_session(EndpointError::RepeatedGoAway));
1195        }
1196        self.session.on_goaway()?;
1197        self.goaway_uri = Some(msg.new_session_uri.clone());
1198        Ok(())
1199    }
1200
1201    // ── Subscribe flow ─────────────────────────────────────────
1202
1203    fn require_active_or_err(&self) -> Result<(), EndpointError> {
1204        match self.session.state() {
1205            SessionState::Active => Ok(()),
1206            SessionState::Draining => Err(EndpointError::Draining),
1207            _ => Err(EndpointError::NotActive),
1208        }
1209    }
1210
1211    /// Record that the session is over because the peer broke a rule this
1212    /// draft answers with a session close, and hand the error back unchanged.
1213    ///
1214    /// The state move is what makes the violation stick: every request entry
1215    /// point goes through
1216    /// [`require_active_or_err`](Self::require_active_or_err), so a caller
1217    /// that ignores the returned error still cannot start anything new.
1218    /// Closing on the wire is the connection layer's job - see
1219    /// [`EndpointError::session_error_code`] for the code it should use.
1220    fn fail_session(&mut self, err: EndpointError) -> EndpointError {
1221        // `on_close` accepts SetupExchange, Active and Draining. A violation
1222        // seen in Connecting or Closed leaves the state machine alone: there
1223        // is no session to close, and the error itself is still the answer.
1224        //
1225        // SetupExchange is in that set because the Termination section says
1226        // "The Transport Session can be terminated at any point", and the
1227        // Setup exchange is a point. So a violation caught while the setup is
1228        // still in flight does close the session, and the discarded result is
1229        // safe because that is one of the states `on_close` accepts.
1230        let _ = self.session.on_close();
1231        err
1232    }
1233
1234    /// Send a SUBSCRIBE message. Allocates an ID and creates a subscription
1235    /// state machine.
1236    ///
1237    /// `AbsoluteStart` and `AbsoluteRange` name a start location, which this
1238    /// call has no way to supply, and are answered with
1239    /// [`EndpointError::FilterNeedsRange`] - use [`Self::subscribe_range`] for
1240    /// those. Without the refusal this call would hand back a message whose
1241    /// filter announces fields the message does not carry, and the frame that
1242    /// goes on the wire is short by exactly those fields.
1243    pub fn subscribe(
1244        &mut self,
1245        track_alias: VarInt,
1246        track_namespace: TrackNamespace,
1247        track_name: Vec<u8>,
1248        subscriber_priority: u8,
1249        group_order: GroupOrder,
1250        filter_type: VarInt,
1251    ) -> Result<(VarInt, ControlMessage), EndpointError> {
1252        if matches!(filter_type.into_inner(), 3 | 4) {
1253            return Err(EndpointError::FilterNeedsRange);
1254        }
1255        self.subscribe_inner(
1256            track_alias,
1257            track_namespace,
1258            track_name,
1259            subscriber_priority,
1260            group_order,
1261            filter_type,
1262            None,
1263            None,
1264        )
1265    }
1266
1267    /// Send a SUBSCRIBE for a range of the track, starting at a given
1268    /// location.
1269    ///
1270    /// The Filter Type is derived from the arguments rather than taken beside
1271    /// them, so the message cannot name a filter whose fields it does not
1272    /// carry.
1273    #[allow(clippy::too_many_arguments)]
1274    pub fn subscribe_range(
1275        &mut self,
1276        track_alias: VarInt,
1277        track_namespace: TrackNamespace,
1278        track_name: Vec<u8>,
1279        subscriber_priority: u8,
1280        group_order: GroupOrder,
1281        start_location: Location,
1282        end_group: Option<VarInt>,
1283    ) -> Result<(VarInt, ControlMessage), EndpointError> {
1284        // 0x3 is AbsoluteStart and 0x4 is AbsoluteRange. The Filter Type is a
1285        // bare varint on this draft, so there is no enum to name them by.
1286        let filter_type = VarInt::from_u64(if end_group.is_some() { 4 } else { 3 }).unwrap();
1287        self.subscribe_inner(
1288            track_alias,
1289            track_namespace,
1290            track_name,
1291            subscriber_priority,
1292            group_order,
1293            filter_type,
1294            Some(start_location),
1295            end_group,
1296        )
1297    }
1298
1299    #[allow(clippy::too_many_arguments)]
1300    fn subscribe_inner(
1301        &mut self,
1302        track_alias: VarInt,
1303        track_namespace: TrackNamespace,
1304        track_name: Vec<u8>,
1305        subscriber_priority: u8,
1306        group_order: GroupOrder,
1307        filter_type: VarInt,
1308        start_location: Option<Location>,
1309        end_group: Option<VarInt>,
1310    ) -> Result<(VarInt, ControlMessage), EndpointError> {
1311        self.require_active_or_err()?;
1312        // The alias travels in the SUBSCRIBE, so this is the last point at
1313        // which giving it to a second track can still be taken back. Refused
1314        // before the Request ID is allocated, so a refusal spends nothing.
1315        let alias = track_alias.into_inner();
1316        if let Some(held) = self.alias_holder(alias, &track_namespace, &track_name) {
1317            return Err(EndpointError::TrackAliasInUse { alias, held });
1318        }
1319        let req_id = self.request_ids.allocate()?;
1320
1321        let mut sm = SubscriptionStateMachine::new();
1322        sm.on_subscribe_sent()?;
1323        self.subscriptions.insert(req_id.into_inner(), sm);
1324        self.track_bindings.insert(
1325            req_id.into_inner(),
1326            TrackBinding {
1327                namespace: track_namespace.clone(),
1328                name: track_name.clone(),
1329                alias,
1330                kind: BindingKind::Ours,
1331            },
1332        );
1333
1334        let (start_group, start_object) = match start_location {
1335            Some(start) => (Some(start.group), Some(start.object)),
1336            None => (None, None),
1337        };
1338        let msg = ControlMessage::Subscribe(Subscribe {
1339            request_id: req_id,
1340            track_alias,
1341            track_namespace,
1342            track_name,
1343            subscriber_priority,
1344            group_order,
1345            forward: Forward::Forward,
1346            filter_type,
1347            start_group,
1348            start_object,
1349            end_group,
1350            parameters: vec![],
1351        });
1352        Ok((req_id, msg))
1353    }
1354
1355    /// Process an incoming SUBSCRIBE_OK.
1356    pub fn receive_subscribe_ok(&mut self, msg: &SubscribeOk) -> Result<(), EndpointError> {
1357        let id = msg.request_id.into_inner();
1358        let sm = self.subscriptions.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1359        sm.on_subscribe_ok()?;
1360        Ok(())
1361    }
1362
1363    /// Process an incoming SUBSCRIBE_ERROR.
1364    pub fn receive_subscribe_error(&mut self, msg: &SubscribeError) -> Result<(), EndpointError> {
1365        let id = msg.request_id.into_inner();
1366        // Section 8.9 gives SUBSCRIBE_ERROR a Track Alias field with one
1367        // meaning: an alias to retry the SUBSCRIBE with. Judged before the
1368        // subscription is ended, because the request it names is what says
1369        // which track the offered alias would be for.
1370        if SubscribeErrorCode::from_u64(msg.error_code.into_inner())
1371            == Some(SubscribeErrorCode::RetryTrackAlias)
1372        {
1373            if let Some(conflict) = self.conflicting_retry_alias(id, msg.track_alias.into_inner()) {
1374                return Err(self.fail_session(conflict));
1375            }
1376        }
1377        let sm = self.subscriptions.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1378        sm.on_subscribe_error()?;
1379        Ok(())
1380    }
1381
1382    /// Send an UNSUBSCRIBE message for an active subscription.
1383    pub fn unsubscribe(&mut self, request_id: VarInt) -> Result<ControlMessage, EndpointError> {
1384        let id = request_id.into_inner();
1385        let sm = self.subscriptions.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1386        sm.on_unsubscribe()?;
1387        Ok(ControlMessage::Unsubscribe(Unsubscribe { request_id }))
1388    }
1389
1390    /// Send a SUBSCRIBE_UPDATE narrowing a subscription this endpoint opened.
1391    ///
1392    /// Section 8.10 gives the message to the subscriber, which is what this
1393    /// endpoint is for every subscription in `subscriptions`. No identifier is
1394    /// spent: the update's one identifier field names the subscription being
1395    /// modified rather than opening a request of its own.
1396    ///
1397    /// The narrowing rules the same section states are the caller's to keep.
1398    ///
1399    /// # Errors
1400    ///
1401    /// [`EndpointError::UnknownRequest`] when this endpoint opened no
1402    /// subscription under that identifier, and the subscription flow's own
1403    /// `InvalidTransition` when the one it names has already ended.
1404    #[allow(clippy::too_many_arguments)]
1405    pub fn subscribe_update(
1406        &mut self,
1407        request_id: VarInt,
1408        start_group: VarInt,
1409        start_object: VarInt,
1410        end_group: VarInt,
1411        subscriber_priority: u8,
1412        forward: Forward,
1413        parameters: Vec<KeyValuePair>,
1414    ) -> Result<ControlMessage, EndpointError> {
1415        self.require_active_or_err()?;
1416        let id = request_id.into_inner();
1417        let sm = self.subscriptions.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1418        sm.on_subscribe_update()?;
1419        Ok(ControlMessage::SubscribeUpdate(SubscribeUpdate {
1420            request_id,
1421            start_group,
1422            start_object,
1423            end_group,
1424            subscriber_priority,
1425            forward,
1426            parameters,
1427        }))
1428    }
1429
1430    /// Process an incoming SUBSCRIBE_UPDATE.
1431    ///
1432    /// Section 8.10: "A subscriber issues a SUBSCRIBE_UPDATE to a publisher to
1433    /// request a change to an existing subscription." One that arrives is
1434    /// therefore about a subscription the **peer** opened, which is why it is
1435    /// looked for among those and not among this endpoint's own.
1436    ///
1437    /// # Errors
1438    ///
1439    /// [`EndpointError::UpdateForUnknownRequest`] when the identifier names no
1440    /// subscription the peer has opened in this session, and the subscription
1441    /// flow's own `InvalidTransition` when it names one that has already
1442    /// ended. Neither ends the session: Section 8.10 says SHOULD.
1443    pub fn receive_subscribe_update(&mut self, msg: &SubscribeUpdate) -> Result<(), EndpointError> {
1444        let id = msg.request_id.into_inner();
1445        let sub = self
1446            .inbound_subscribes
1447            .get_mut(&id)
1448            .ok_or(EndpointError::UpdateForUnknownRequest(id))?;
1449        sub.state.on_subscribe_update_received()?;
1450        Ok(())
1451    }
1452
1453    /// Process an incoming SUBSCRIBE_DONE (subscriber side — publisher finished).
1454    pub fn receive_subscribe_done(&mut self, msg: &SubscribeDone) -> Result<(), EndpointError> {
1455        let id = msg.request_id.into_inner();
1456        let sm = self.subscriptions.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1457        sm.on_subscribe_done()?;
1458        Ok(())
1459    }
1460
1461    // ── Answering a SUBSCRIBE the peer sent ────────────────────
1462
1463    /// Process an incoming SUBSCRIBE, judging the Track Alias it carries and
1464    /// recording the subscription it opens.
1465    ///
1466    /// Section 8.7: "If the Track Alias is already being used for a different
1467    /// track, the publisher MUST close the session with a Duplicate Track
1468    /// Alias error". This endpoint is the publisher of a SUBSCRIBE that
1469    /// arrives, so this is where that close is raised. Judged before anything
1470    /// is written down, so a refused SUBSCRIBE leaves no binding behind.
1471    ///
1472    /// The Request ID has already been checked by [`Self::receive_request`],
1473    /// which every request message passes through before its own handler.
1474    ///
1475    /// # Errors
1476    ///
1477    /// [`EndpointError::DuplicateTrackAlias`] when the alias names a second
1478    /// track, with the session already moved to Closed.
1479    pub fn receive_subscribe(&mut self, msg: &Subscribe) -> Result<(), EndpointError> {
1480        let id = msg.request_id.into_inner();
1481        let alias = msg.track_alias.into_inner();
1482        if let Some(conflict) =
1483            self.conflicting_track_alias(id, alias, &msg.track_namespace, &msg.track_name)
1484        {
1485            return Err(self.fail_session(conflict));
1486        }
1487        let mut state = SubscriptionStateMachine::new();
1488        state.on_subscribe_received()?;
1489        self.inbound_subscribes.insert(id, InboundSubscribe { message: msg.clone(), state });
1490        self.track_bindings.insert(
1491            id,
1492            TrackBinding {
1493                namespace: msg.track_namespace.clone(),
1494                name: msg.track_name.clone(),
1495                alias,
1496                kind: BindingKind::Peers,
1497            },
1498        );
1499        Ok(())
1500    }
1501
1502    /// The SUBSCRIBE the peer sent under `request_id` and this endpoint has
1503    /// not answered yet.
1504    ///
1505    /// `None` once it has been answered, and for an identifier this session
1506    /// has no inbound subscription for.
1507    pub fn pending_subscribe(&self, request_id: VarInt) -> Option<&Subscribe> {
1508        self.inbound_subscribes
1509            .get(&request_id.into_inner())
1510            .filter(|s| s.state.state() == SubscriptionState::Subscribing)
1511            .map(|s| &s.message)
1512    }
1513
1514    /// How many SUBSCRIBEs the peer has sent that are still waiting for an
1515    /// answer.
1516    pub fn pending_subscribe_count(&self) -> usize {
1517        self.inbound_subscribes
1518            .values()
1519            .filter(|s| s.state.state() == SubscriptionState::Subscribing)
1520            .count()
1521    }
1522
1523    /// Build the SUBSCRIBE_OK accepting a subscription the peer opened.
1524    ///
1525    /// # Errors
1526    ///
1527    /// [`EndpointError::UnknownRequest`] if the peer opened no subscription
1528    /// under that identifier, and [`EndpointError::Subscription`] if it has
1529    /// already been answered.
1530    pub fn send_subscribe_ok(
1531        &mut self,
1532        request_id: VarInt,
1533        expires: VarInt,
1534        group_order: GroupOrder,
1535        parameters: Vec<KeyValuePair>,
1536    ) -> Result<ControlMessage, EndpointError> {
1537        let id = request_id.into_inner();
1538        let sub = self.inbound_subscribes.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1539        sub.state.on_subscribe_ok_sent()?;
1540        Ok(ControlMessage::SubscribeOk(SubscribeOk {
1541            request_id,
1542            expires,
1543            group_order,
1544            content_exists: ContentExists::NoLargestLocation,
1545            largest_location: None,
1546            parameters,
1547        }))
1548    }
1549
1550    /// Build the SUBSCRIBE_ERROR rejecting a subscription the peer opened.
1551    ///
1552    /// The Track Alias goes back out with the refusal because Section 8.9
1553    /// gives the field a use: an alias to retry with, when the code is 'Retry
1554    /// Track Alias'. Under any other code the peer reads nothing from it.
1555    ///
1556    /// # Errors
1557    ///
1558    /// [`EndpointError::UnknownRequest`] if the peer opened no subscription
1559    /// under that identifier, and [`EndpointError::Subscription`] if it has
1560    /// already been answered.
1561    pub fn send_subscribe_error(
1562        &mut self,
1563        request_id: VarInt,
1564        error_code: VarInt,
1565        reason_phrase: Vec<u8>,
1566        track_alias: VarInt,
1567    ) -> Result<ControlMessage, EndpointError> {
1568        let id = request_id.into_inner();
1569        let sub = self.inbound_subscribes.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1570        sub.state.on_subscribe_error_sent()?;
1571        Ok(ControlMessage::SubscribeError(SubscribeError {
1572            request_id,
1573            error_code,
1574            reason_phrase,
1575            track_alias,
1576        }))
1577    }
1578
1579    /// Build the SUBSCRIBE_DONE ending a subscription this endpoint accepted.
1580    ///
1581    /// # Errors
1582    ///
1583    /// [`EndpointError::UnknownRequest`] if the peer opened no subscription
1584    /// under that identifier, and [`EndpointError::Subscription`] if it is not
1585    /// one this endpoint accepted and has not already ended.
1586    pub fn send_subscribe_done(
1587        &mut self,
1588        request_id: VarInt,
1589        status_code: VarInt,
1590        reason_phrase: Vec<u8>,
1591    ) -> Result<ControlMessage, EndpointError> {
1592        let id = request_id.into_inner();
1593        let sub = self.inbound_subscribes.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1594        sub.state.on_subscribe_done_sent()?;
1595        Ok(ControlMessage::SubscribeDone(SubscribeDone {
1596            request_id,
1597            status_code,
1598            stream_count: VarInt::from_u64(0).unwrap(),
1599            reason_phrase,
1600        }))
1601    }
1602
1603    /// Process an incoming UNSUBSCRIBE, ending the subscription the peer
1604    /// opened and freeing the Track Alias it held.
1605    ///
1606    /// # Errors
1607    ///
1608    /// [`EndpointError::UnknownRequest`] if the peer opened no subscription
1609    /// under that identifier, and [`EndpointError::Subscription`] if it is not
1610    /// one this endpoint accepted and has not already ended.
1611    pub fn receive_unsubscribe(&mut self, msg: &Unsubscribe) -> Result<(), EndpointError> {
1612        let id = msg.request_id.into_inner();
1613        let sub = self.inbound_subscribes.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1614        sub.state.on_unsubscribe_received()?;
1615        Ok(())
1616    }
1617
1618    // ── Fetch flow ─────────────────────────────────────────────
1619
1620    /// Send a standalone FETCH message. Allocates a request ID and creates a
1621    /// fetch state machine.
1622    #[allow(clippy::too_many_arguments)]
1623    pub fn fetch(
1624        &mut self,
1625        track_namespace: TrackNamespace,
1626        track_name: Vec<u8>,
1627        subscriber_priority: u8,
1628        group_order: GroupOrder,
1629        start_group: VarInt,
1630        start_object: VarInt,
1631        end_group: VarInt,
1632        end_object: VarInt,
1633    ) -> Result<(VarInt, ControlMessage), EndpointError> {
1634        self.require_active_or_err()?;
1635        let req_id = self.request_ids.allocate()?;
1636
1637        let mut sm = FetchStateMachine::new();
1638        sm.on_fetch_sent()?;
1639        self.fetches.insert(req_id.into_inner(), sm);
1640
1641        let msg = ControlMessage::Fetch(Fetch {
1642            request_id: req_id,
1643            subscriber_priority,
1644            group_order,
1645            fetch_type: FetchType::Standalone,
1646            fetch_payload: FetchPayload::Standalone {
1647                track_namespace,
1648                track_name,
1649                start_group,
1650                start_object,
1651                end_group,
1652                end_object,
1653            },
1654            parameters: vec![],
1655        });
1656        Ok((req_id, msg))
1657    }
1658
1659    /// Send a Relative Joining Fetch (Fetch Type 0x2), attaching a fetch to a
1660    /// subscription this session already holds. Allocates a Request ID.
1661    ///
1662    /// Section 8.13 calls it "A Fetch joined together with a Subscribe by
1663    /// specifying the Request ID of an active subscription and a relative
1664    /// starting offset", and has "A publisher receiving a Joining Fetch uses
1665    /// properties of the associated Subscribe to determine the Track
1666    /// Namespace, Track, Start Group, Start Object, End Group, and End Object
1667    /// such that it is contiguous with the associated Subscribe." So a joining
1668    /// fetch names neither the namespace nor the name, and `joining_start` is
1669    /// read against the subscription rather than against the track: Section
1670    /// 8.13.1 has the publisher compute "Fetch Start Group: Subscribe Largest
1671    /// Group - Joining start", which makes it a count of groups back from the
1672    /// live edge.
1673    ///
1674    /// The subscription is named by `joining_subscribe_id`, which is what this
1675    /// draft's field list calls the field although the sentence under it
1676    /// already reads "The Request ID of the existing subscription to be
1677    /// joined".
1678    ///
1679    /// # Errors
1680    ///
1681    /// The session error when the session is not established, and the
1682    /// request-id error when this endpoint has no identifier left to spend. A
1683    /// Request ID naming no subscription is not refused here — Section 8.13
1684    /// answers that at the publisher, "it MUST respond with a Fetch Error with
1685    /// code Invalid Joining Subscribe ID", and this endpoint is the
1686    /// subscriber.
1687    pub fn joining_fetch(
1688        &mut self,
1689        subscriber_priority: u8,
1690        group_order: GroupOrder,
1691        joining_subscribe_id: VarInt,
1692        joining_start: VarInt,
1693    ) -> Result<(VarInt, ControlMessage), EndpointError> {
1694        self.joining_fetch_of_type(
1695            FetchType::RelativeJoining,
1696            subscriber_priority,
1697            group_order,
1698            joining_subscribe_id,
1699            joining_start,
1700        )
1701    }
1702
1703    /// Send an Absolute Joining Fetch (Fetch Type 0x3).
1704    ///
1705    /// Section 8.13.2 is the whole of the difference: "Identical to the
1706    /// Relative Joining fetch except that Fetch Start Group is the Joining
1707    /// Start value." So `joining_start` is the group to begin at rather than a
1708    /// count of groups back, which is what an application that knows the group
1709    /// it wants actually has. Asking for the same range relatively would need
1710    /// the Largest Group, and a subscriber that has not yet been told one
1711    /// cannot compute the offset.
1712    ///
1713    /// # Errors
1714    ///
1715    /// As [`Endpoint::joining_fetch`].
1716    pub fn absolute_joining_fetch(
1717        &mut self,
1718        subscriber_priority: u8,
1719        group_order: GroupOrder,
1720        joining_subscribe_id: VarInt,
1721        joining_start: VarInt,
1722    ) -> Result<(VarInt, ControlMessage), EndpointError> {
1723        self.joining_fetch_of_type(
1724            FetchType::AbsoluteJoining,
1725            subscriber_priority,
1726            group_order,
1727            joining_subscribe_id,
1728            joining_start,
1729        )
1730    }
1731
1732    /// What both calls above are, and why neither of them takes the type.
1733    ///
1734    /// Section 8.13 admits three Fetch Types and this payload carries two of
1735    /// them. A type taken as an argument here would leave a third value a
1736    /// caller could pass and this call would have to answer for; naming the
1737    /// two rules it out instead, so there is no answer left to get wrong.
1738    fn joining_fetch_of_type(
1739        &mut self,
1740        fetch_type: FetchType,
1741        subscriber_priority: u8,
1742        group_order: GroupOrder,
1743        joining_subscribe_id: VarInt,
1744        joining_start: VarInt,
1745    ) -> Result<(VarInt, ControlMessage), EndpointError> {
1746        self.require_active_or_err()?;
1747        let req_id = self.request_ids.allocate()?;
1748
1749        let mut sm = FetchStateMachine::new();
1750        sm.on_fetch_sent()?;
1751        self.fetches.insert(req_id.into_inner(), sm);
1752
1753        let msg = ControlMessage::Fetch(Fetch {
1754            request_id: req_id,
1755            subscriber_priority,
1756            group_order,
1757            fetch_type,
1758            fetch_payload: FetchPayload::Joining { joining_subscribe_id, joining_start },
1759            parameters: vec![],
1760        });
1761        Ok((req_id, msg))
1762    }
1763
1764    /// Process an incoming FETCH_OK.
1765    pub fn receive_fetch_ok(&mut self, msg: &message::FetchOk) -> Result<(), EndpointError> {
1766        let id = msg.request_id.into_inner();
1767        let sm = self.fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1768        sm.on_fetch_ok()?;
1769        Ok(())
1770    }
1771
1772    /// Process an incoming FETCH_ERROR.
1773    pub fn receive_fetch_error(&mut self, msg: &message::FetchError) -> Result<(), EndpointError> {
1774        let id = msg.request_id.into_inner();
1775        let sm = self.fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1776        sm.on_fetch_error()?;
1777        Ok(())
1778    }
1779
1780    /// Send a FETCH_CANCEL message.
1781    pub fn fetch_cancel(&mut self, request_id: VarInt) -> Result<ControlMessage, EndpointError> {
1782        let id = request_id.into_inner();
1783        let sm = self.fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1784        sm.on_fetch_cancel()?;
1785        Ok(ControlMessage::FetchCancel(FetchCancel { request_id }))
1786    }
1787
1788    /// Notify that a fetch data stream received FIN.
1789    ///
1790    /// It may arrive before the FETCH_OK or FETCH_ERROR answering the
1791    /// request, which leaves the fetch in `FetchState::Unanswered` until the
1792    /// answer lands.
1793    pub fn on_fetch_stream_fin(&mut self, request_id: VarInt) -> Result<(), EndpointError> {
1794        let id = request_id.into_inner();
1795        let sm = self.fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1796        sm.on_stream_fin()?;
1797        Ok(())
1798    }
1799
1800    /// Notify that a fetch data stream was reset.
1801    ///
1802    /// As with a FIN, it may arrive before the answer to the request.
1803    pub fn on_fetch_stream_reset(&mut self, request_id: VarInt) -> Result<(), EndpointError> {
1804        let id = request_id.into_inner();
1805        let sm = self.fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1806        sm.on_stream_reset()?;
1807        Ok(())
1808    }
1809
1810    // ── Answering a FETCH the peer sent ────────────────────────
1811
1812    /// Process an incoming FETCH, recording the fetch it opens.
1813    ///
1814    /// The Request ID has already been checked by [`Self::receive_request`],
1815    /// which every request message passes through before its own handler.
1816    ///
1817    /// A Joining Fetch is recorded like any other. Section 8.13 answers one
1818    /// naming a subscription this session cannot join with a refusal and not
1819    /// a session close, and a refusal is a message this endpoint has to
1820    /// build, so the request it refuses has to be on record first.
1821    ///
1822    /// # Errors
1823    ///
1824    /// The session error when the session is not established, and the fetch
1825    /// flow's own `InvalidTransition` for a second FETCH under an identifier
1826    /// already carrying one.
1827    pub fn receive_fetch(&mut self, msg: &Fetch) -> Result<(), EndpointError> {
1828        self.require_active_or_err()?;
1829        let id = msg.request_id.into_inner();
1830        let unjoinable = self.joining_subscription_missing(msg);
1831        let mut state = FetchStateMachine::new();
1832        state.on_fetch_received()?;
1833        self.inbound_fetches.insert(id, InboundFetch { message: msg.clone(), state, unjoinable });
1834        Ok(())
1835    }
1836
1837    /// The FETCH the peer sent under `request_id` and this endpoint has not
1838    /// answered yet.
1839    ///
1840    /// `None` once it has been answered, and for an identifier this session
1841    /// has no inbound fetch for. The record itself lives on past the answer,
1842    /// because the fetch is not over until its data stream is.
1843    pub fn pending_fetch(&self, request_id: VarInt) -> Option<&Fetch> {
1844        self.inbound_fetches
1845            .get(&request_id.into_inner())
1846            .filter(|f| matches!(f.state.state(), FetchState::Pending | FetchState::Unanswered))
1847            .map(|f| &f.message)
1848    }
1849
1850    /// How many FETCHes the peer has sent that are still waiting for an
1851    /// answer.
1852    pub fn pending_fetch_count(&self) -> usize {
1853        self.inbound_fetches
1854            .values()
1855            .filter(|f| matches!(f.state.state(), FetchState::Pending | FetchState::Unanswered))
1856            .count()
1857    }
1858
1859    /// The identifier an arriving Joining Fetch names, when this session has
1860    /// no subscription it may join.
1861    ///
1862    /// Section 8.13:
1863    /// "If a publisher receives a Joining Fetch with a Request ID that
1864    /// does not correspond to an existing Subscribe in the same session, it
1865    /// MUST respond with a Fetch Error with code Invalid Joining Subscribe ID."
1866    ///
1867    /// The verdict is taken as the FETCH arrives, because that is the moment
1868    /// the sentence names, and it is kept. A subscription that ends between
1869    /// the FETCH and its answer does not turn a fetch that could be joined
1870    /// into one that could not.
1871    ///
1872    /// A standalone fetch names none and answers `None`, and so does a joining
1873    /// one whose subscription is live. The subscription is one the peer opened,
1874    /// because the peer is the end that fetches and this endpoint is the one
1875    /// answering.
1876    /// "Existing" is read as "has not ended". Draft-16 Section 9.16.2 states
1877    /// the same rule with the states named - "in the Established or Pending
1878    /// (subscriber) states" - which is the same set read the same way.
1879    fn joining_subscription_missing(&self, msg: &Fetch) -> Option<u64> {
1880        let message::FetchPayload::Joining { joining_subscribe_id: joined, .. } =
1881            &msg.fetch_payload
1882        else {
1883            return None;
1884        };
1885        let joined = joined.into_inner();
1886        let live = self
1887            .inbound_subscribes
1888            .get(&joined)
1889            .is_some_and(|s| s.state.state() != SubscriptionState::Done);
1890        if live {
1891            None
1892        } else {
1893            Some(joined)
1894        }
1895    }
1896
1897    /// Build the FETCH_OK accepting a fetch the peer opened.
1898    ///
1899    /// # Errors
1900    ///
1901    /// [`EndpointError::UnknownRequest`] if the peer opened no fetch under
1902    /// that identifier, [`EndpointError::UnjoinableSubscription`] for a
1903    /// Joining Fetch naming a subscription this session cannot join, and the
1904    /// fetch flow's own `InvalidTransition` for a second answer: Section 4
1905    /// says the publisher "MUST send exactly one FETCH_OK or FETCH_ERROR in
1906    /// response to a FETCH".
1907    pub fn send_fetch_ok(
1908        &mut self,
1909        request_id: VarInt,
1910        group_order: GroupOrder,
1911        end_of_track: u8,
1912        end_location: Location,
1913        parameters: Vec<KeyValuePair>,
1914    ) -> Result<ControlMessage, EndpointError> {
1915        let id = request_id.into_inner();
1916        let unjoinable =
1917            self.inbound_fetches.get(&id).ok_or(EndpointError::UnknownRequest(id))?.unjoinable;
1918        if let Some(joining) = unjoinable {
1919            return Err(EndpointError::UnjoinableSubscription { fetch: id, joining });
1920        }
1921        let fetch = self.inbound_fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1922        fetch.state.on_fetch_ok_sent()?;
1923        Ok(ControlMessage::FetchOk(message::FetchOk {
1924            request_id,
1925            group_order,
1926            end_of_track,
1927            end_location,
1928            parameters,
1929        }))
1930    }
1931
1932    /// Build the FETCH_ERROR refusing a fetch the peer opened.
1933    ///
1934    /// Section 8.13 names the code a Joining Fetch naming an unjoinable
1935    /// subscription is refused with, so that refusal cannot go out under any
1936    /// other: a subscriber told the wrong reason retries the wrong thing.
1937    ///
1938    /// # Errors
1939    ///
1940    /// [`EndpointError::UnknownRequest`] if the peer opened no fetch under
1941    /// that identifier, and the fetch flow's own `InvalidTransition` if it has
1942    /// already been answered.
1943    pub fn send_fetch_error(
1944        &mut self,
1945        request_id: VarInt,
1946        error_code: VarInt,
1947        reason_phrase: Vec<u8>,
1948    ) -> Result<ControlMessage, EndpointError> {
1949        let id = request_id.into_inner();
1950        let fetch = self.inbound_fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1951        if fetch.unjoinable.is_some() {
1952            let required = FetchErrorCode::InvalidJoiningSubscribeId as u64;
1953            if error_code.into_inner() != required {
1954                return Err(EndpointError::WrongJoiningRefusal { fetch: id, required });
1955            }
1956        }
1957        fetch.state.on_fetch_error_sent()?;
1958        Ok(ControlMessage::FetchError(message::FetchError {
1959            request_id,
1960            error_code,
1961            reason_phrase,
1962        }))
1963    }
1964
1965    /// Process an incoming FETCH_CANCEL, ending the fetch the peer opened.
1966    ///
1967    /// Section 8.16: the subscriber sends it to stop a fetch it no longer
1968    /// wants, so the record this endpoint serves the fetch from is the one it
1969    /// ends.
1970    ///
1971    /// # Errors
1972    ///
1973    /// [`EndpointError::UnknownRequest`] if the peer opened no fetch under
1974    /// that identifier, and the fetch flow's own `InvalidTransition` for a fetch
1975    /// that has already ended.
1976    pub fn receive_fetch_cancel(&mut self, msg: &FetchCancel) -> Result<(), EndpointError> {
1977        let id = msg.request_id.into_inner();
1978        let fetch = self.inbound_fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1979        fetch.state.on_fetch_cancel_received()?;
1980        Ok(())
1981    }
1982
1983    /// Note that this endpoint finished the data stream serving a fetch the
1984    /// peer opened.
1985    ///
1986    /// A fetch is over when its answer and its data stream have both settled,
1987    /// and this is the second of those for the end that serves it.
1988    ///
1989    /// # Errors
1990    ///
1991    /// [`EndpointError::UnknownRequest`] if the peer opened no fetch under
1992    /// that identifier, and the fetch flow's own `InvalidTransition` from a state
1993    /// the stream cannot close from.
1994    pub fn on_peer_fetch_stream_fin(&mut self, request_id: VarInt) -> Result<(), EndpointError> {
1995        let id = request_id.into_inner();
1996        let fetch = self.inbound_fetches.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
1997        fetch.state.on_stream_fin_sent()?;
1998        Ok(())
1999    }
2000
2001    // ── Subscribe Announces flow ───────────────────────────────
2002
2003    /// Send a SUBSCRIBE_ANNOUNCES message. Returns the allocated request ID
2004    /// alongside the control message so the caller can correlate replies.
2005    ///
2006    /// Section 8.24 addresses the first half of the overlap rule to this end
2007    /// of the session: "A subscriber cannot make overlapping namespace
2008    /// subscriptions on a single session." So a prefix overlapping one this
2009    /// endpoint has already asked about is refused here rather than built and
2010    /// sent for the peer to refuse.
2011    ///
2012    /// # Errors
2013    ///
2014    /// The session error when the session is not established,
2015    /// [`EndpointError::OwnPrefixOverlap`] when the prefix overlaps one this
2016    /// endpoint is already subscribed to, and the Request ID allocator's own
2017    /// error when the peer has granted no room for another request.
2018    pub fn subscribe_announces(
2019        &mut self,
2020        track_namespace_prefix: TrackNamespace,
2021    ) -> Result<(VarInt, ControlMessage), EndpointError> {
2022        self.require_active_or_err()?;
2023        // The subscriber's half of the rule, refused before the message
2024        // exists. A publisher that follows this draft answers it with a
2025        // refusal, so building it spends a Request ID on a namespace
2026        // subscription that is not going to open.
2027        if let Some(established) = self.own_prefix_overlap(&track_namespace_prefix) {
2028            return Err(EndpointError::OwnPrefixOverlap { established });
2029        }
2030        let req_id = self.request_ids.allocate()?;
2031        let key = track_namespace_prefix.0.clone();
2032        let mut sm = SubscribeAnnouncesStateMachine::new();
2033        sm.on_subscribe_announces_sent()?;
2034        self.subscribe_announces.insert(req_id.into_inner(), sm);
2035        self.subscribe_announces_ids.insert(key, req_id.into_inner());
2036        Ok((
2037            req_id,
2038            ControlMessage::SubscribeAnnounces(SubscribeAnnounces {
2039                request_id: req_id,
2040                track_namespace_prefix,
2041                parameters: vec![],
2042            }),
2043        ))
2044    }
2045
2046    /// Process an incoming SUBSCRIBE_ANNOUNCES_OK.
2047    pub fn receive_subscribe_announces_ok(
2048        &mut self,
2049        msg: &SubscribeAnnouncesOk,
2050    ) -> Result<(), EndpointError> {
2051        let id = msg.request_id.into_inner();
2052        let sm = self.subscribe_announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2053        sm.on_subscribe_announces_ok()?;
2054        Ok(())
2055    }
2056
2057    /// Process an incoming SUBSCRIBE_ANNOUNCES_ERROR.
2058    pub fn receive_subscribe_announces_error(
2059        &mut self,
2060        msg: &SubscribeAnnouncesError,
2061    ) -> Result<(), EndpointError> {
2062        let id = msg.request_id.into_inner();
2063        let sm = self.subscribe_announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2064        sm.on_subscribe_announces_error()?;
2065        Ok(())
2066    }
2067
2068    /// Send an UNSUBSCRIBE_ANNOUNCES message.
2069    pub fn unsubscribe_announces(
2070        &mut self,
2071        track_namespace_prefix: TrackNamespace,
2072    ) -> Result<ControlMessage, EndpointError> {
2073        let id = *self
2074            .subscribe_announces_ids
2075            .get(&track_namespace_prefix.0)
2076            .ok_or(EndpointError::UnknownNamespace)?;
2077        let sm = self.subscribe_announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2078        sm.on_unsubscribe_announces()?;
2079        Ok(ControlMessage::UnsubscribeAnnounces(UnsubscribeAnnounces { track_namespace_prefix }))
2080    }
2081
2082    // ── Answering a SUBSCRIBE_ANNOUNCES the peer sent ──────────
2083
2084    /// Process an incoming SUBSCRIBE_ANNOUNCES, recording the namespace
2085    /// subscription it opens.
2086    ///
2087    /// Section 8.24: "The subscriber sends the SUBSCRIBE_ANNOUNCES control
2088    /// message to a publisher to request the current set of matching
2089    /// announcements, as well as future updates to the set."
2090    ///
2091    /// The set it asks for is this endpoint's to decide, and deciding needs
2092    /// both the request and somewhere to answer from. The record holds the
2093    /// message and not only its state, because every message that answers
2094    /// this request or ends it names the prefix the request carried, and
2095    /// nothing else here has it.
2096    ///
2097    /// # Errors
2098    ///
2099    /// The session error when the session is not established.
2100    pub fn receive_subscribe_announces(
2101        &mut self,
2102        msg: &SubscribeAnnounces,
2103    ) -> Result<(), EndpointError> {
2104        self.require_active_or_err()?;
2105        let overlaps = self.peer_prefix_overlap(&msg.track_namespace_prefix);
2106        let mut state = SubscribeAnnouncesStateMachine::new();
2107        state.on_subscribe_announces_received()?;
2108        self.inbound_subscribe_announces.insert(
2109            msg.request_id.into_inner(),
2110            InboundSubscribeAnnounces { message: msg.clone(), state, overlaps },
2111        );
2112        Ok(())
2113    }
2114
2115    /// The earliest namespace subscription the peer has made whose prefix
2116    /// overlaps `prefix`, and `None` when there is none.
2117    ///
2118    /// Every one the peer has made counts, including one it has since
2119    /// withdrawn and one this endpoint refused: the sentence weighs the
2120    /// arriving prefix against "an earlier SUBSCRIBE_ANNOUNCES", and one that
2121    /// has ended was still earlier. Draft-12 changes that word to "active",
2122    /// and there a subscription that has ended stops counting.
2123    ///
2124    /// Namespace subscriptions this endpoint made are a separate set and are
2125    /// not consulted. This endpoint is the subscriber for those, so a prefix
2126    /// it asked about says nothing about what the peer may ask about.
2127    ///
2128    /// The lowest Request ID wins when more than one overlaps, so the answer
2129    /// does not depend on the order a map happens to iterate in. Identifiers
2130    /// are handed out in increasing order, so the lowest of them is the
2131    /// earliest request.
2132    fn peer_prefix_overlap(&self, prefix: &TrackNamespace) -> Option<u64> {
2133        self.inbound_subscribe_announces
2134            .iter()
2135            .filter_map(|(&id, s)| {
2136                prefixes_overlap(&s.message.track_namespace_prefix.0, &prefix.0).then_some(id)
2137            })
2138            .min()
2139    }
2140
2141    /// The earliest namespace subscription **this endpoint** has made whose
2142    /// prefix overlaps `prefix`, and `None` when there is none.
2143    ///
2144    /// The subscriber's half of the same sentence reads over this endpoint's
2145    /// own requests, and takes the same view of which of them are past as
2146    /// [`Self::peer_prefix_overlap`] takes of the peer's.
2147    fn own_prefix_overlap(&self, prefix: &TrackNamespace) -> Option<u64> {
2148        self.subscribe_announces_ids
2149            .iter()
2150            .filter_map(|(key, &id)| prefixes_overlap(key, &prefix.0).then_some(id))
2151            .min()
2152    }
2153
2154    /// The SUBSCRIBE_ANNOUNCES the peer sent under `request_id` and this
2155    /// endpoint has not answered yet.
2156    ///
2157    /// `None` once it has been answered, and for an identifier the peer has
2158    /// subscribed to nothing under. The record itself lives on past the
2159    /// answer, because a namespace subscription that was accepted is not over
2160    /// until it is withdrawn.
2161    pub fn pending_subscribe_announces(&self, request_id: VarInt) -> Option<&SubscribeAnnounces> {
2162        self.inbound_subscribe_announces
2163            .get(&request_id.into_inner())
2164            .filter(|s| s.state.state() == SubscribeAnnouncesState::Pending)
2165            .map(|s| &s.message)
2166    }
2167
2168    /// How many namespace subscriptions the peer has made that are still
2169    /// waiting for an answer.
2170    pub fn pending_subscribe_announces_count(&self) -> usize {
2171        self.inbound_subscribe_announces
2172            .values()
2173            .filter(|s| s.state.state() == SubscribeAnnouncesState::Pending)
2174            .count()
2175    }
2176
2177    /// The identifier of a live namespace subscription the peer made for
2178    /// `prefix`.
2179    ///
2180    /// Section 8.27 names a Track Namespace Prefix where the
2181    /// SUBSCRIBE_ANNOUNCES it ends named a Request ID, so one record has to
2182    /// be reachable both ways. It is stored under the identifier, which is
2183    /// unique, and found by prefix with a scan of the same map. A second map
2184    /// from prefix to identifier would be quicker and could fall out of step
2185    /// with the first; there is nothing here for it to disagree with.
2186    ///
2187    /// One that has ended is skipped, so a prefix subscribed again after
2188    /// being withdrawn finds the live one.
2189    fn inbound_subscribe_announces_id(&self, prefix: &TrackNamespace) -> Option<u64> {
2190        self.inbound_subscribe_announces
2191            .iter()
2192            .find(|(_, s)| {
2193                s.message.track_namespace_prefix == *prefix
2194                    && s.state.state() != SubscribeAnnouncesState::Done
2195            })
2196            .map(|(id, _)| *id)
2197    }
2198
2199    /// Build the SUBSCRIBE_ANNOUNCES_OK accepting a namespace subscription
2200    /// the peer made.
2201    ///
2202    /// Section 5.1: "A publisher MUST send exactly one SUBSCRIBE_ANNOUNCES_OK
2203    /// or SUBSCRIBE_ANNOUNCES_ERROR in response to a SUBSCRIBE_ANNOUNCES."
2204    ///
2205    /// One answer and no second one: the flow moves on the first, and a
2206    /// second call finds a record that has left Pending.
2207    ///
2208    /// # Errors
2209    ///
2210    /// [`EndpointError::UnknownRequest`] if the peer has subscribed to
2211    /// nothing under that identifier, [`EndpointError::PeerPrefixOverlap`] if
2212    /// the prefix it asked about overlaps one the peer is already subscribed
2213    /// to, and the namespace flow's own `InvalidTransition` for a request
2214    /// already answered.
2215    pub fn send_subscribe_announces_ok(
2216        &mut self,
2217        request_id: VarInt,
2218    ) -> Result<ControlMessage, EndpointError> {
2219        let id = request_id.into_inner();
2220        let sub = self
2221            .inbound_subscribe_announces
2222            .get_mut(&id)
2223            .ok_or(EndpointError::UnknownRequest(id))?;
2224        // The MUST names one answer for this request, and it is not this one.
2225        if let Some(established) = sub.overlaps {
2226            return Err(EndpointError::PeerPrefixOverlap { request: id, established });
2227        }
2228        sub.state.on_subscribe_announces_ok_sent()?;
2229        Ok(ControlMessage::SubscribeAnnouncesOk(SubscribeAnnouncesOk { request_id }))
2230    }
2231
2232    /// Build the SUBSCRIBE_ANNOUNCES_ERROR refusing a namespace subscription
2233    /// the peer made.
2234    ///
2235    /// The other half of the same sentence: one message back, whichever of
2236    /// the two it is.
2237    ///
2238    /// # Errors
2239    ///
2240    /// [`EndpointError::UnknownRequest`] if the peer has subscribed to
2241    /// nothing under that identifier, [`EndpointError::WrongOverlapRefusal`]
2242    /// if the request overlaps another and the code named is not the one the
2243    /// draft assigns to that refusal, and the namespace flow's own
2244    /// `InvalidTransition` for a request already answered.
2245    pub fn send_subscribe_announces_error(
2246        &mut self,
2247        request_id: VarInt,
2248        error_code: VarInt,
2249        reason_phrase: Vec<u8>,
2250    ) -> Result<ControlMessage, EndpointError> {
2251        let id = request_id.into_inner();
2252        let sub = self
2253            .inbound_subscribe_announces
2254            .get_mut(&id)
2255            .ok_or(EndpointError::UnknownRequest(id))?;
2256        if sub.overlaps.is_some() {
2257            let required = SubscribeAnnouncesErrorCode::NamespacePrefixOverlap as u64;
2258            if error_code.into_inner() != required {
2259                return Err(EndpointError::WrongOverlapRefusal { request: id, required });
2260            }
2261        }
2262        sub.state.on_subscribe_announces_error_sent()?;
2263        Ok(ControlMessage::SubscribeAnnouncesError(SubscribeAnnouncesError {
2264            request_id,
2265            error_code,
2266            reason_phrase,
2267        }))
2268    }
2269
2270    /// Process an incoming UNSUBSCRIBE_ANNOUNCES, ending the namespace
2271    /// subscription the peer made.
2272    ///
2273    /// Section 5.1: "An UNSUBSCRIBE_ANNOUNCES withdraws a previous
2274    /// SUBSCRIBE_ANNOUNCES."
2275    ///
2276    /// The subscription it ends is the peer's, so the record it reads is the
2277    /// one this endpoint keeps of what the peer subscribed to. One this
2278    /// endpoint made is withdrawn by [`Endpoint::unsubscribe_announces`],
2279    /// which is the same message travelling the other way.
2280    ///
2281    /// # Errors
2282    ///
2283    /// [`EndpointError::UnknownPeerNamespaceSubscription`] if the peer has no
2284    /// live namespace subscription for that prefix, and the namespace flow's
2285    /// own `InvalidTransition` for one this endpoint never accepted.
2286    pub fn receive_unsubscribe_announces(
2287        &mut self,
2288        msg: &UnsubscribeAnnounces,
2289    ) -> Result<(), EndpointError> {
2290        let id = self
2291            .inbound_subscribe_announces_id(&msg.track_namespace_prefix)
2292            .ok_or(EndpointError::UnknownPeerNamespaceSubscription)?;
2293        let sub = self
2294            .inbound_subscribe_announces
2295            .get_mut(&id)
2296            .ok_or(EndpointError::UnknownRequest(id))?;
2297        sub.state.on_unsubscribe_announces_received()?;
2298        Ok(())
2299    }
2300
2301    // ── Announce flow ──────────────────────────────────────────
2302
2303    /// Send an ANNOUNCE message. Returns the allocated request ID alongside
2304    /// the control message.
2305    pub fn announce(
2306        &mut self,
2307        track_namespace: TrackNamespace,
2308    ) -> Result<(VarInt, ControlMessage), EndpointError> {
2309        self.require_active_or_err()?;
2310        let req_id = self.request_ids.allocate()?;
2311        let key = track_namespace.0.clone();
2312        let mut sm = AnnounceStateMachine::new();
2313        sm.on_announce_sent()?;
2314        self.announces.insert(req_id.into_inner(), sm);
2315        self.announce_ids.insert(key, req_id.into_inner());
2316        Ok((
2317            req_id,
2318            ControlMessage::Announce(Announce {
2319                request_id: req_id,
2320                track_namespace,
2321                parameters: vec![],
2322            }),
2323        ))
2324    }
2325
2326    /// Process an incoming ANNOUNCE_OK.
2327    pub fn receive_announce_ok(&mut self, msg: &AnnounceOk) -> Result<(), EndpointError> {
2328        let id = msg.request_id.into_inner();
2329        let sm = self.announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2330        sm.on_announce_ok()?;
2331        Ok(())
2332    }
2333
2334    /// Process an incoming ANNOUNCE_ERROR.
2335    pub fn receive_announce_error(&mut self, msg: &AnnounceError) -> Result<(), EndpointError> {
2336        let id = msg.request_id.into_inner();
2337        let sm = self.announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2338        sm.on_announce_error()?;
2339        Ok(())
2340    }
2341
2342    /// Process an incoming ANNOUNCE_CANCEL.
2343    pub fn receive_announce_cancel(&mut self, msg: &AnnounceCancel) -> Result<(), EndpointError> {
2344        let id = *self
2345            .announce_ids
2346            .get(&msg.track_namespace.0)
2347            .ok_or(EndpointError::UnknownNamespace)?;
2348        let sm = self.announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2349        sm.on_announce_cancel()?;
2350        Ok(())
2351    }
2352
2353    /// Send an UNANNOUNCE message (publisher withdrawing).
2354    pub fn unannounce(
2355        &mut self,
2356        track_namespace: TrackNamespace,
2357    ) -> Result<ControlMessage, EndpointError> {
2358        let id =
2359            *self.announce_ids.get(&track_namespace.0).ok_or(EndpointError::UnknownNamespace)?;
2360        let sm = self.announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2361        sm.on_unannounce()?;
2362        Ok(ControlMessage::Unannounce(Unannounce { track_namespace }))
2363    }
2364
2365    // ── Answering an ANNOUNCE the peer sent ────────────────────
2366
2367    /// Process an incoming ANNOUNCE, recording the announcement it makes.
2368    ///
2369    /// Section 8.19: "The publisher sends the ANNOUNCE control message to
2370    /// advertise that it has tracks available within the announced Track
2371    /// Namespace. The receiver verifies the publisher is authorized to publish
2372    /// tracks under this namespace."
2373    ///
2374    /// Verifying is the application's to do, and it needs both the message to
2375    /// verify and somewhere to answer from. The Request ID has already been
2376    /// checked by [`Self::receive_request`], which every request message passes
2377    /// through before its own handler, so a repeat of one the peer has already
2378    /// spent never reaches here.
2379    ///
2380    /// # Errors
2381    ///
2382    /// The session error when the session is not established.
2383    pub fn receive_announce(&mut self, msg: &Announce) -> Result<(), EndpointError> {
2384        self.require_active_or_err()?;
2385        let id = msg.request_id.into_inner();
2386        let mut state = AnnounceStateMachine::new();
2387        state.on_announce_received()?;
2388        self.inbound_announces.insert(id, InboundAnnounce { message: msg.clone(), state });
2389        Ok(())
2390    }
2391
2392    /// The ANNOUNCE the peer sent under `request_id` and this endpoint has not
2393    /// answered yet.
2394    ///
2395    /// `None` once it has been answered, and for an identifier the peer has
2396    /// announced nothing under. The record itself lives on past the answer,
2397    /// because an announcement that was accepted is not over until it is
2398    /// withdrawn or cancelled.
2399    pub fn pending_announce(&self, request_id: VarInt) -> Option<&Announce> {
2400        self.inbound_announces
2401            .get(&request_id.into_inner())
2402            .filter(|a| a.state.state() == AnnounceState::Pending)
2403            .map(|a| &a.message)
2404    }
2405
2406    /// How many announcements the peer has made that are still waiting for an
2407    /// answer.
2408    pub fn pending_announce_count(&self) -> usize {
2409        self.inbound_announces
2410            .values()
2411            .filter(|a| a.state.state() == AnnounceState::Pending)
2412            .count()
2413    }
2414
2415    /// The identifier of a live announcement the peer made for `namespace`.
2416    ///
2417    /// Section 8.22: "The publisher sends the UNANNOUNCE control message to
2418    /// indicate its intent to stop serving new subscriptions for tracks within
2419    /// the provided Track Namespace." and Section 8.23 says what a cancellation
2420    /// is for: the subscriber "will stop sending new subscriptions for tracks
2421    /// within the provided Track Namespace". Both name a namespace where the
2422    /// ANNOUNCE they are about named a Request ID, so one record has to be
2423    /// reachable both ways.
2424    ///
2425    /// It is stored under the identifier, which is unique, and found by
2426    /// namespace with a scan of the same map. A second map from namespace to
2427    /// identifier would be quicker and could fall out of step with the first;
2428    /// there is nothing here for it to disagree with.
2429    ///
2430    /// An announcement that has ended is skipped, so a namespace announced
2431    /// again after being withdrawn finds the live one. Which of two live
2432    /// announcements for the same namespace is found is not decided here,
2433    /// because no sentence in the draft makes a second one for a namespace
2434    /// already announced an error.
2435    fn inbound_announce_id(&self, namespace: &TrackNamespace) -> Option<u64> {
2436        self.inbound_announces
2437            .iter()
2438            .find(|(_, a)| {
2439                a.message.track_namespace == *namespace && a.state.state() != AnnounceState::Done
2440            })
2441            .map(|(id, _)| *id)
2442    }
2443
2444    /// Build the ANNOUNCE_OK accepting an announcement the peer made.
2445    ///
2446    /// Section 5.2: "A subscriber MUST send exactly one ANNOUNCE_OK or
2447    /// ANNOUNCE_ERROR in response to an ANNOUNCE. The publisher SHOULD close
2448    /// the session with a protocol error if it receives more than one."
2449    ///
2450    /// One answer and no second one: the flow moves on the first, and a second
2451    /// call finds a record that has left Pending.
2452    ///
2453    /// # Errors
2454    ///
2455    /// [`EndpointError::UnknownRequest`] if the peer has announced nothing
2456    /// under that identifier, and the namespace flow's own `InvalidTransition`
2457    /// for an announcement already answered.
2458    pub fn send_announce_ok(
2459        &mut self,
2460        request_id: VarInt,
2461    ) -> Result<ControlMessage, EndpointError> {
2462        let id = request_id.into_inner();
2463        let ann = self.inbound_announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2464        ann.state.on_announce_ok_sent()?;
2465        Ok(ControlMessage::AnnounceOk(AnnounceOk { request_id }))
2466    }
2467
2468    /// Build the ANNOUNCE_ERROR refusing an announcement the peer made.
2469    ///
2470    /// The same sentence in Section 5.2 answers both ways: one message back and
2471    /// no second one, whichever of the two it is.
2472    ///
2473    /// # Errors
2474    ///
2475    /// [`EndpointError::UnknownRequest`] if the peer has announced nothing
2476    /// under that identifier, and the namespace flow's own `InvalidTransition`
2477    /// for an announcement already answered.
2478    pub fn send_announce_error(
2479        &mut self,
2480        request_id: VarInt,
2481        error_code: VarInt,
2482        reason_phrase: Vec<u8>,
2483    ) -> Result<ControlMessage, EndpointError> {
2484        let id = request_id.into_inner();
2485        let ann = self.inbound_announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2486        ann.state.on_announce_error_sent()?;
2487        Ok(ControlMessage::AnnounceError(AnnounceError { request_id, error_code, reason_phrase }))
2488    }
2489
2490    /// Process an incoming UNANNOUNCE, ending the announcement the peer made.
2491    ///
2492    /// Section 8.22: "The publisher sends the UNANNOUNCE control message to
2493    /// indicate its intent to stop serving new subscriptions for tracks within
2494    /// the provided Track Namespace."
2495    ///
2496    /// The announcement it ends is the peer's, so the record it reads is the
2497    /// one this endpoint keeps of what the peer announced. An announcement this
2498    /// endpoint made is withdrawn by [`Self::unannounce`], which is the same
2499    /// message travelling the other way.
2500    ///
2501    /// # Errors
2502    ///
2503    /// [`EndpointError::UnknownPeerNamespace`] if the peer has no live
2504    /// announcement for that namespace, and the namespace flow's own
2505    /// `InvalidTransition` for one this endpoint never accepted.
2506    pub fn receive_unannounce(&mut self, msg: &Unannounce) -> Result<(), EndpointError> {
2507        let id = self
2508            .inbound_announce_id(&msg.track_namespace)
2509            .ok_or(EndpointError::UnknownPeerNamespace)?;
2510        let ann = self.inbound_announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2511        ann.state.on_unannounce_received()?;
2512        Ok(())
2513    }
2514
2515    /// Build the ANNOUNCE_CANCEL revoking an acceptance.
2516    ///
2517    /// Section 7.3 names what a cancellation revokes: a namespace "it
2518    /// previously responded ANNOUNCE_OK to". Section 8.23 says what it does:
2519    /// the subscriber "will stop sending new subscriptions for tracks within
2520    /// the provided Track Namespace".
2521    ///
2522    /// Previously responded ANNOUNCE_OK to is a state, and it is Active: an
2523    /// announcement reaches it by being accepted and no other way. One still
2524    /// waiting for an answer, one refused and one already ended are all refused
2525    /// here rather than sent.
2526    ///
2527    /// The announcement is the peer's. An announcement this endpoint made is
2528    /// not cancelled by its own publisher; the peer cancels it, and that
2529    /// arrives at [`Self::receive_announce_cancel`].
2530    ///
2531    /// # Errors
2532    ///
2533    /// [`EndpointError::UnknownPeerNamespace`] if the peer has no live
2534    /// announcement for that namespace, and the namespace flow's own
2535    /// `InvalidTransition` for one this endpoint never accepted.
2536    pub fn announce_cancel(
2537        &mut self,
2538        track_namespace: TrackNamespace,
2539        error_code: VarInt,
2540        reason_phrase: Vec<u8>,
2541    ) -> Result<ControlMessage, EndpointError> {
2542        let id = self
2543            .inbound_announce_id(&track_namespace)
2544            .ok_or(EndpointError::UnknownPeerNamespace)?;
2545        let ann = self.inbound_announces.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2546        ann.state.on_announce_cancel_sent()?;
2547        Ok(ControlMessage::AnnounceCancel(AnnounceCancel {
2548            track_namespace,
2549            error_code,
2550            reason_phrase,
2551        }))
2552    }
2553
2554    // ── Track Status flow ──────────────────────────────────────
2555
2556    /// Send a TRACK_STATUS_REQUEST message. Returns the allocated request ID
2557    /// alongside the control message so the caller can correlate replies.
2558    pub fn track_status_request(
2559        &mut self,
2560        track_namespace: TrackNamespace,
2561        track_name: Vec<u8>,
2562    ) -> Result<(VarInt, ControlMessage), EndpointError> {
2563        self.require_active_or_err()?;
2564        let req_id = self.request_ids.allocate()?;
2565        let mut sm = TrackStatusStateMachine::new();
2566        sm.on_track_status_request_sent()?;
2567        self.track_statuses.insert(req_id.into_inner(), sm);
2568        Ok((
2569            req_id,
2570            ControlMessage::TrackStatusRequest(TrackStatusRequest {
2571                request_id: req_id,
2572                track_namespace,
2573                track_name,
2574                parameters: vec![],
2575            }),
2576        ))
2577    }
2578
2579    /// Process an incoming TRACK_STATUS reply.
2580    pub fn receive_track_status(&mut self, msg: &TrackStatus) -> Result<(), EndpointError> {
2581        let id = msg.request_id.into_inner();
2582        let sm = self.track_statuses.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2583        sm.on_track_status()?;
2584        Ok(())
2585    }
2586
2587    // ── Answering a TRACK_STATUS_REQUEST the peer sent ─────────
2588
2589    /// Process an incoming TRACK_STATUS_REQUEST, recording what the peer asked
2590    /// about.
2591    ///
2592    /// Section 8.17: "A potential subscriber sends a 'TRACK_STATUS_REQUEST'
2593    /// message on the control stream to obtain information about the current
2594    /// status of a given track."
2595    ///
2596    /// Answering is the application's to do, and it needs both the request and
2597    /// somewhere to answer from. This draft gave the request a Request ID, so
2598    /// that is what the record is filed under and what the answer names.
2599    ///
2600    /// # Errors
2601    ///
2602    /// The session error when the session is not established.
2603    pub fn receive_track_status_request(
2604        &mut self,
2605        msg: &TrackStatusRequest,
2606    ) -> Result<(), EndpointError> {
2607        self.require_active_or_err()?;
2608        let id = msg.request_id.into_inner();
2609        let mut state = TrackStatusStateMachine::new();
2610        state.on_track_status_request_received()?;
2611        self.inbound_track_statuses.insert(id, InboundTrackStatus { message: msg.clone(), state });
2612        Ok(())
2613    }
2614
2615    /// The TRACK_STATUS_REQUEST the peer sent under `request_id` and this
2616    /// endpoint has not answered yet.
2617    ///
2618    /// `None` once it has been answered, and for an identifier this session has
2619    /// carried no track status request under.
2620    pub fn pending_track_status_request(&self, request_id: VarInt) -> Option<&TrackStatusRequest> {
2621        self.inbound_track_statuses
2622            .get(&request_id.into_inner())
2623            .filter(|t| t.state.state() == TrackStatusState::Pending)
2624            .map(|t| &t.message)
2625    }
2626
2627    /// How many track statuses the peer has asked about that are still waiting
2628    /// for an answer.
2629    pub fn pending_track_status_request_count(&self) -> usize {
2630        self.inbound_track_statuses
2631            .values()
2632            .filter(|t| t.state.state() == TrackStatusState::Pending)
2633            .count()
2634    }
2635
2636    /// Build the TRACK_STATUS answering a request the peer sent.
2637    ///
2638    /// Section 8.17 leaves the answering end no discretion about whether to
2639    /// answer: "A TRACK_STATUS message MUST be sent in response to each
2640    /// TRACK_STATUS_REQUEST." What it bounds is how many, and that half is what
2641    /// the record carries: the request leaves `Pending` on the first answer, so
2642    /// a second call finds nothing left to answer.
2643    ///
2644    /// Section 8.18 says the identifier this message carries is "The Request ID
2645    /// of the TRACK_STATUS_REQUEST this message is replying to", which is why
2646    /// the answer is asked for by the identifier the request arrived under and
2647    /// not by the track it named.
2648    ///
2649    /// # Errors
2650    ///
2651    /// [`EndpointError::UnknownRequest`] if the peer has asked nothing under
2652    /// that identifier, and the flow's own `InvalidTransition` for a request
2653    /// already answered.
2654    pub fn send_track_status(
2655        &mut self,
2656        request_id: VarInt,
2657        status_code: VarInt,
2658        largest_location: Location,
2659        parameters: Vec<KeyValuePair>,
2660    ) -> Result<ControlMessage, EndpointError> {
2661        let id = request_id.into_inner();
2662        let req =
2663            self.inbound_track_statuses.get_mut(&id).ok_or(EndpointError::UnknownRequest(id))?;
2664        req.state.on_track_status_sent()?;
2665        Ok(ControlMessage::TrackStatus(TrackStatus {
2666            request_id,
2667            status_code,
2668            largest_location,
2669            parameters,
2670        }))
2671    }
2672
2673    // ── Requests blocked ───────────────────────────────────────
2674
2675    /// Process an incoming REQUESTS_BLOCKED message.
2676    ///
2677    /// Draft-11 renames draft-10's SUBSCRIBES_BLOCKED to REQUESTS_BLOCKED so
2678    /// the peer can explicitly report that a new request id would exceed our
2679    /// advertised maximum. The endpoint records the peer's reported maximum;
2680    /// acting on it (issuing a new `MAX_REQUEST_ID`) is up to the caller.
2681    pub fn receive_requests_blocked(&mut self, msg: &RequestsBlocked) -> Result<(), EndpointError> {
2682        self.peer_reported_max_request_id = Some(msg.maximum_request_id);
2683        Ok(())
2684    }
2685
2686    /// The maximum request id that the peer most recently reported in a
2687    /// `REQUESTS_BLOCKED` message, if any.
2688    pub fn peer_reported_max_request_id(&self) -> Option<VarInt> {
2689        self.peer_reported_max_request_id
2690    }
2691
2692    /// Hold a Request ID a peer allocated to the rules Section 8.1 states
2693    /// about it.
2694    ///
2695    /// "The client's Request ID starts at 0 and are even and the server's
2696    /// Request ID starts at 1 and are odd. The Request ID increments by 2 ...
2697    /// If an endpoint receives a Request ID that is not valid for the peer, it
2698    /// MUST close the session with Invalid Request ID." Parity says which end
2699    /// may have chosen it; the ceiling this endpoint advertised says how far
2700    /// the peer may go.
2701    ///
2702    /// This is where [`Self::validate_peer_request_id`] is applied to arriving
2703    /// traffic, and the only place inside the endpoint that applies it. A
2704    /// request message the list below omits is one whose Request ID is checked
2705    /// for nothing: not its parity, not the ceiling this endpoint advertised,
2706    /// not its place in the peer's sequence.
2707    ///
2708    /// A message that is a response rather than a request carries the id of a
2709    /// request this endpoint made, so it is not checked here - it is checked by
2710    /// finding the state machine it names. SUBSCRIBE_UPDATE is the same case on
2711    /// this draft: its one Request ID field names the subscription it modifies
2712    /// rather than opening a request of its own, which is why Section 8.1 does
2713    /// not list it among the messages that step the sequence.
2714    ///
2715    /// # The list below is the rule, not a convenience
2716    ///
2717    /// Every message named here spends one of the peer's Request IDs, and
2718    /// nothing else does. That is what makes the list load-bearing, because
2719    /// the sequence is tracked: a request left out of it spends an ID
2720    /// this endpoint never counts, so the peer's **next** request looks like a
2721    /// skip and a conforming session is closed over it.
2722    ///
2723    /// # Errors
2724    ///
2725    /// The request-id errors, for a wrong parity, an id at or above the
2726    /// advertised ceiling, or one that is not the next in the peer's sequence.
2727    pub fn receive_request(&mut self, msg: &ControlMessage) -> Result<(), EndpointError> {
2728        let request_id = match msg {
2729            ControlMessage::Subscribe(m) => m.request_id,
2730            ControlMessage::Fetch(m) => m.request_id,
2731            ControlMessage::Announce(m) => m.request_id,
2732            ControlMessage::SubscribeAnnounces(m) => m.request_id,
2733            ControlMessage::TrackStatusRequest(m) => m.request_id,
2734            _ => return Ok(()),
2735        };
2736        self.validate_peer_request_id(request_id.into_inner())
2737    }
2738
2739    // ── Unified message dispatch ───────────────────────────────
2740
2741    /// Dispatch an incoming control message to the appropriate handler.
2742    pub fn receive_message(&mut self, msg: ControlMessage) -> Result<(), EndpointError> {
2743        self.receive_request(&msg)?;
2744        match msg {
2745            ControlMessage::GoAway(ref m) => self.receive_goaway(m),
2746            ControlMessage::MaxRequestId(ref m) => self.receive_max_request_id(m),
2747            ControlMessage::RequestsBlocked(ref m) => self.receive_requests_blocked(m),
2748            ControlMessage::SubscribeOk(ref m) => self.receive_subscribe_ok(m),
2749            ControlMessage::SubscribeError(ref m) => self.receive_subscribe_error(m),
2750            ControlMessage::SubscribeUpdate(ref m) => self.receive_subscribe_update(m),
2751            ControlMessage::SubscribeDone(ref m) => self.receive_subscribe_done(m),
2752            ControlMessage::FetchOk(ref m) => self.receive_fetch_ok(m),
2753            ControlMessage::FetchError(ref m) => self.receive_fetch_error(m),
2754            ControlMessage::SubscribeAnnouncesOk(ref m) => self.receive_subscribe_announces_ok(m),
2755            ControlMessage::SubscribeAnnouncesError(ref m) => {
2756                self.receive_subscribe_announces_error(m)
2757            }
2758            ControlMessage::AnnounceOk(ref m) => self.receive_announce_ok(m),
2759            ControlMessage::AnnounceError(ref m) => self.receive_announce_error(m),
2760            ControlMessage::AnnounceCancel(ref m) => self.receive_announce_cancel(m),
2761            ControlMessage::TrackStatus(ref m) => self.receive_track_status(m),
2762            ControlMessage::TrackStatusRequest(ref m) => self.receive_track_status_request(m),
2763            ControlMessage::Subscribe(ref m) => self.receive_subscribe(m),
2764            ControlMessage::Unsubscribe(ref m) => self.receive_unsubscribe(m),
2765            ControlMessage::Fetch(ref m) => self.receive_fetch(m),
2766            ControlMessage::FetchCancel(ref m) => self.receive_fetch_cancel(m),
2767            ControlMessage::Announce(ref m) => self.receive_announce(m),
2768            ControlMessage::Unannounce(ref m) => self.receive_unannounce(m),
2769            ControlMessage::SubscribeAnnounces(ref m) => self.receive_subscribe_announces(m),
2770            ControlMessage::UnsubscribeAnnounces(ref m) => self.receive_unsubscribe_announces(m),
2771            _ => Ok(()),
2772        }
2773    }
2774}