Skip to main content

phantom_protocol/transport/
stream.rs

1//! Phantom Protocol - Stream Management
2//!
3//! Multiplexed streams within a session.
4//! Each stream has independent sequence numbers (no Head-of-Line blocking).
5
6use crate::errors::CoreError;
7use crate::transport::sack::Sack;
8use crate::transport::types::{SequenceNumber, StreamId};
9
10use bytes::Bytes;
11use std::collections::VecDeque;
12use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
13use std::sync::Arc;
14use std::time::Duration;
15use tokio::sync::{Mutex, Notify, Semaphore};
16
17const MAX_PENDING_PACKETS: usize = 1024;
18
19/// Upper bound on out-of-order segments held for reassembly per stream. In
20/// practice the flow-control window bounds in-flight (hence reorderable) data far
21/// below this; a peer that floods past its window with huge gaps is refused here
22/// (the refused segment is NOT recorded as received, so it is not SACKed and the
23/// sender retransmits it — no SACK-without-data hazard, bounded memory).
24const MAX_RECV_REORDER: usize = 2048;
25
26/// Per-stream byte budget for the out-of-order reorder buffer (H-3), tied to the flow-control
27/// window. A compliant peer keeps in-flight (hence reorderable) data within ~one
28/// [`INITIAL_STREAM_WINDOW`]; the 2× headroom absorbs a boundary segment. A future hole that
29/// would push the buffered total past this is refused (dropped → retransmitted via the
30/// "refused segment is not SACKed" contract), so per-stream reorder memory is bounded
31/// regardless of the per-entry frame size (~253 KiB UDP / 4 MiB TCP) — the entry cap alone is
32/// not, since one entry can dwarf the window.
33pub const MAX_RECV_REORDER_BYTES: usize = 2 * INITIAL_STREAM_WINDOW as usize;
34
35/// RFC 9002 §6.1.1 packet-threshold: a still-unacked segment is declared lost
36/// once a segment at least this many offsets *newer* has been SACK-acked.
37const PACKET_THRESHOLD: u32 = 3;
38
39/// Initial per-stream send window — caps how many bytes the local
40/// side will put on the wire before receiving a `WINDOW_UPDATE` from
41/// the peer. 64 KiB matches QUIC's stream initial-window default.
42pub const INITIAL_STREAM_WINDOW: u32 = 64 * 1024;
43
44/// Hard ceiling on the credit-based send window. `WINDOW_UPDATE` frames add
45/// *relative* credit; this caps the accumulated window so a peer that floods
46/// inflated credits cannot overflow the counter. A compliant peer never grants
47/// more than ~one [`INITIAL_STREAM_WINDOW`] of outstanding credit, so the cap is
48/// only a misbehaving-peer guard (the receiver's own delivery HARD_CAP is the
49/// real bound on buffering).
50pub const MAX_SEND_WINDOW: u32 = 8 * INITIAL_STREAM_WINDOW;
51
52/// Stream state
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum StreamState {
55    /// Stream is open for both directions
56    Open,
57    /// Local side has finished sending
58    HalfClosedLocal,
59    /// Remote side has finished sending
60    HalfClosedRemote,
61    /// Stream is fully closed
62    Closed,
63}
64
65/// Pending data waiting to be sent
66#[derive(Debug)]
67struct PendingData {
68    /// Gap-free per-stream reliable-data offset — the reassembly / SACK / loss-
69    /// detection key (A.5). Carried in the AEAD plaintext so the receiver can
70    /// deliver reliable data strictly in send order even when `sequence` has
71    /// control-frame holes. Stable across retransmits.
72    stream_offset: SequenceNumber,
73    data: Bytes,
74    sent_at: Option<tokio::time::Instant>,
75    #[allow(dead_code)]
76    retries: u32,
77    /// Flagged lost by the SACK-driven loss detector (RFC 9002 packet- or
78    /// time-threshold, L1-B). `poll_send`'s Pass-0 fast-retransmits it ahead of
79    /// cwnd/window, then clears the flag. Distinct from the RTO pass (Pass-1).
80    lost: bool,
81}
82
83/// One reliable segment retired by [`Stream::on_sack`] — a segment whose
84/// sequence a received SACK covered and which has now been removed from the
85/// send buffer.
86#[derive(Debug, Clone, Copy)]
87pub struct RetiredSegment {
88    /// When the segment was last (re)transmitted, if it had been sent at all.
89    /// `None` means the segment was acknowledged before `poll_send` ever stamped
90    /// it (e.g. a duplicate cumulative SACK) — no RTT sample is taken.
91    pub sent_at: Option<tokio::time::Instant>,
92    /// On-wire payload size of the segment.
93    pub size: u64,
94    /// True if the segment had been retransmitted at least once (`retries > 0`).
95    /// Per Karn's algorithm, the caller must NOT sample RTT from such a segment.
96    pub was_retransmit: bool,
97}
98
99/// One segment newly declared lost by [`Stream::on_sack`]'s RFC-9002 loss
100/// detector (L1-B) — still buffered, now flagged for fast-retransmit.
101#[derive(Debug, Clone, Copy)]
102pub struct LostSegment {
103    /// Gap-free reliable offset of the lost segment.
104    pub stream_offset: SequenceNumber,
105    /// On-wire payload size — the caller reports it to congestion control via
106    /// `Session::on_packet_lost`.
107    pub size: u64,
108}
109
110/// Outcome of processing a received SACK against the send buffer.
111#[derive(Debug, Default)]
112pub struct SackResult {
113    /// The segments newly retired by this SACK (were in the send buffer, now
114    /// removed). The caller feeds each into congestion control / the RTT
115    /// estimator. Empty if the SACK acknowledged nothing still buffered (e.g. a
116    /// duplicate or stale ACK).
117    pub retired: Vec<RetiredSegment>,
118    /// Segments newly declared lost (packet- or time-threshold, RFC 9002) by this
119    /// SACK — still buffered, now flagged for Pass-0 fast-retransmit. The caller
120    /// feeds each into `Session::on_packet_lost` (the real BBR loss signal).
121    pub lost: Vec<LostSegment>,
122}
123
124impl SackResult {
125    /// The gap-free offsets of the segments newly declared lost, ascending.
126    pub fn lost_offsets(&self) -> Vec<SequenceNumber> {
127        self.lost.iter().map(|l| l.stream_offset).collect()
128    }
129}
130
131/// One segment handed back by [`Stream::poll_send`] for transmission.
132#[derive(Debug, Clone)]
133pub struct OutboundSegment {
134    /// Gap-free per-stream reliable-data offset (A.5). The send path prepends it
135    /// (big-endian u32) to the AEAD plaintext of a reliable segment so the
136    /// receiver reassembles in send order regardless of control-frame holes.
137    /// Meaningless for unreliable segments (the send path does not prefix those).
138    pub stream_offset: SequenceNumber,
139    /// Payload bytes.
140    pub data: Bytes,
141    /// Whether the segment is on the reliable (ACK-tracked) path.
142    pub reliable: bool,
143    /// True when this is a retransmission (the RTO expired) rather than a first
144    /// transmission — the caller reports it to congestion control as a loss.
145    pub retransmit: bool,
146}
147
148/// RFC 6298 retransmission-timeout estimator (per stream). Replaces a fixed
149/// retransmit timer with one that tracks measured RTT (SRTT / RTTVAR) and backs
150/// off exponentially on consecutive timeouts.
151#[derive(Debug)]
152struct RtoEstimator {
153    /// Smoothed RTT; `None` until the first measurement.
154    srtt: Option<Duration>,
155    /// RTT variation estimate.
156    rttvar: Duration,
157    /// Number of consecutive timeouts (RTO is doubled `backoff_shift` times).
158    backoff_shift: u32,
159}
160
161impl RtoEstimator {
162    /// RFC 6298 (2.1): RTO before the first measurement.
163    const INITIAL_RTO: Duration = Duration::from_secs(1);
164    /// Floor — RFC's 1s minimum is too conservative for a low-latency transport.
165    const MIN_RTO: Duration = Duration::from_millis(200);
166    /// Ceiling, so a stalled path can't push the timer arbitrarily high.
167    const MAX_RTO: Duration = Duration::from_secs(60);
168    /// Clock-granularity term `G` in RFC 6298 (2.3).
169    const GRANULARITY: Duration = Duration::from_millis(1);
170    /// Cap on the backoff doubling (2^6 = 64×).
171    const MAX_BACKOFF_SHIFT: u32 = 6;
172
173    fn new() -> Self {
174        Self {
175            srtt: None,
176            rttvar: Duration::ZERO,
177            backoff_shift: 0,
178        }
179    }
180
181    /// Feed a fresh (non-retransmitted, per Karn) RTT measurement.
182    fn on_rtt_sample(&mut self, r: Duration) {
183        match self.srtt {
184            None => {
185                // RFC 6298 (2.2): first measurement.
186                self.srtt = Some(r);
187                self.rttvar = r / 2;
188            }
189            Some(srtt) => {
190                // RFC 6298 (2.3): RTTVAR = (1-1/4)·RTTVAR + 1/4·|SRTT-R|;
191                //                 SRTT  = (1-1/8)·SRTT  + 1/8·R.
192                let diff = srtt.abs_diff(r);
193                self.rttvar = (self.rttvar * 3 + diff) / 4;
194                self.srtt = Some((srtt * 7 + r) / 8);
195            }
196        }
197        // A fresh measurement clears any accumulated backoff.
198        self.backoff_shift = 0;
199    }
200
201    /// Current RTO, honoring backoff and the floor / ceiling.
202    fn rto(&self) -> Duration {
203        // RFC 6298 (2.2)/(2.3): RTO = SRTT + max(G, K·RTTVAR), K = 4.
204        let base = match self.srtt {
205            None => Self::INITIAL_RTO,
206            Some(srtt) => srtt + std::cmp::max(Self::GRANULARITY, self.rttvar * 4),
207        };
208        // Exponential backoff (RFC 6298 (5.5)); saturate to MAX_RTO on overflow.
209        let scaled = base
210            .checked_mul(1u32 << self.backoff_shift)
211            .unwrap_or(Self::MAX_RTO);
212        scaled.clamp(Self::MIN_RTO, Self::MAX_RTO)
213    }
214
215    /// On a retransmission timeout: double the RTO (RFC 6298 (5.5)).
216    fn on_timeout(&mut self) {
217        self.backoff_shift = (self.backoff_shift + 1).min(Self::MAX_BACKOFF_SHIFT);
218    }
219
220    /// Reset to the initial state (Phase 4 / QUIC §9.4): a migration path switch
221    /// lands on a different network, so the old RTT estimate must not carry over.
222    /// Wired by the P4.2 migration switch (`Stream::reset_rto`).
223    fn reset(&mut self) {
224        self.srtt = None;
225        self.rttvar = Duration::ZERO;
226        self.backoff_shift = 0;
227    }
228}
229
230#[cfg(test)]
231mod rto_tests {
232    use super::RtoEstimator;
233    use std::time::Duration;
234
235    #[test]
236    fn follows_rfc6298_srtt_rttvar() {
237        let mut est = RtoEstimator::new();
238        // No samples yet → initial 1s.
239        assert_eq!(est.rto(), Duration::from_secs(1));
240        // First sample R=100ms: SRTT=100, RTTVAR=50, RTO = 100 + 4*50 = 300ms.
241        est.on_rtt_sample(Duration::from_millis(100));
242        assert_eq!(est.rto(), Duration::from_millis(300));
243        // A steady stream of identical samples drives RTTVAR→0, so RTO→SRTT,
244        // floored at MIN_RTO (200ms).
245        for _ in 0..50 {
246            est.on_rtt_sample(Duration::from_millis(100));
247        }
248        assert_eq!(est.rto(), Duration::from_millis(200));
249    }
250
251    #[test]
252    fn backoff_doubles_and_fresh_sample_resets() {
253        let mut est = RtoEstimator::new();
254        est.on_rtt_sample(Duration::from_millis(100)); // RTO = 300ms
255        assert_eq!(est.rto(), Duration::from_millis(300));
256        est.on_timeout();
257        assert_eq!(est.rto(), Duration::from_millis(600));
258        est.on_timeout();
259        assert_eq!(est.rto(), Duration::from_millis(1200));
260        // A fresh measurement clears the backoff. This is a *second* sample, so
261        // RTTVAR shrinks 50ms → 37.5ms and RTO = 100 + 4*37.5 = 250ms. The key
262        // check is that backoff is gone: with shift still at 2 it would be 1000ms.
263        est.on_rtt_sample(Duration::from_millis(100));
264        assert_eq!(est.rto(), Duration::from_millis(250));
265    }
266
267    #[test]
268    fn reset_clears_estimate_and_backoff() {
269        let mut est = RtoEstimator::new();
270        // Build up an SRTT and a backed-off RTO.
271        est.on_rtt_sample(Duration::from_millis(100)); // RTO = 300ms
272        est.on_timeout(); // RTO = 600ms (backed off)
273        assert_eq!(est.rto(), Duration::from_millis(600));
274        // Phase 4 / QUIC §9.4: a migration path switch must reset the estimate so
275        // the new network's RTT is measured fresh (no stale tiny RTO => no
276        // spurious-retransmit storm on the first packets of the new path).
277        est.reset();
278        assert_eq!(est.rto(), Duration::from_secs(1)); // INITIAL_RTO, no backoff
279    }
280}
281
282/// Stream - multiplexed data channel within a session
283pub struct Stream {
284    /// Stream identifier
285    id: StreamId,
286    /// Current state
287    state: Mutex<StreamState>,
288    /// Gap-free per-stream reliable-data offset counter (A.5). Only reliable data
289    /// consumes it, so it has no control-frame holes; it is the reassembly / SACK
290    /// key carried in the reliable-data AEAD plaintext.
291    reliable_offset: AtomicU32,
292    /// Next expected receive **stream offset** (gap-free reassembly cursor, A.5).
293    recv_sequence: AtomicU32,
294    /// Send buffer (data waiting to be sent)
295    send_buffer: Mutex<VecDeque<PendingData>>,
296    /// Unreliable send buffer (fire and forget)
297    unreliable_buffer: Mutex<VecDeque<Bytes>>,
298    /// Receive buffer (out-of-order data). Each entry is one cursor position
299    /// `(sequence, payloads)`; `payloads` is normally a single reliable frame but
300    /// carries a COALESCED bundle's sub-payloads when several share one sequence.
301    recv_buffer: Mutex<VecDeque<(SequenceNumber, Vec<Bytes>)>>,
302    /// Total payload bytes currently held in `recv_buffer` (H-3). Mutated only under the
303    /// `recv_buffer` lock (in `accept_in_order`), so it stays exactly in step with the
304    /// buffer; an `AtomicUsize` only so it can be read lock-free for stats/tests. Bounds the
305    /// out-of-order reorder buffer by *bytes*, not entries, since one entry can be ~253 KiB
306    /// (UDP) / 4 MiB (TCP).
307    recv_buffer_bytes: AtomicUsize,
308    /// Ordered receive queue (ready for application)
309    recv_ready: Mutex<VecDeque<Bytes>>,
310    /// Notify when data is ready to read
311    recv_notify: Notify,
312    /// Whether stream is finished locally
313    local_finished: AtomicBool,
314    /// Whether stream is finished remotely
315    remote_finished: AtomicBool,
316    /// Priority (higher = more important)
317    priority: AtomicU32,
318    /// Backpressure semaphore
319    send_semaphore: Arc<Semaphore>,
320    /// Bytes the **peer** has granted us to send — decremented as we
321    /// emit payload bytes, replenished by inbound `WINDOW_UPDATE`
322    /// frames (Phase 4.3). When it hits zero, `poll_send` stalls
323    /// until the next `WINDOW_UPDATE`.
324    peer_send_window: AtomicU32,
325    /// Bytes the local side has granted the peer — replenished as
326    /// the application drains `recv_ready`. We periodically emit a
327    /// `WINDOW_UPDATE` carrying the new absolute window.
328    local_recv_window: AtomicU32,
329    /// Total bytes the local side has consumed since the last
330    /// emitted `WINDOW_UPDATE`. Used to decide when to send the
331    /// next update (avoid flooding the wire with tiny updates).
332    bytes_since_last_update: AtomicU32,
333    /// Pending **relative** flow-control credit to advertise in a
334    /// `WINDOW_UPDATE`, staged by the receive **delivery** task (which credits
335    /// the window on *real* app consumption) and flushed by the **send loop** —
336    /// the sole *outbound* writer, so the encrypted control frame is sealed by the
337    /// same task that stamps every data packet, under the epoch live at flush
338    /// time. (The epoch itself has TWO writers — the send loop's own `rekey()` and
339    /// the receive task's authenticated forward catch-up in
340    /// `decrypt_packet_accepting_rekey` — but both serialise through the session's
341    /// `rekey_lock`, so the send loop always seals under a consistent key.)
342    /// Credits accumulate additively, so several grants between two flushes are
343    /// never lost. `0` = nothing pending.
344    pending_window_update: AtomicU32,
345    /// RFC 6298 retransmission-timeout estimator. A plain (sync) mutex: it is
346    /// updated only from the serial ACK path and read by `poll_send`, and the
347    /// guard is never held across an `.await`.
348    rto: std::sync::Mutex<RtoEstimator>,
349    /// Receive instant of the most recent reliable data packet, used to populate
350    /// the SACK's `ack_delay_us` (`now − recv_at`). A plain sync mutex; the guard
351    /// is never held across an `.await`.
352    last_data_recv_at: std::sync::Mutex<Option<tokio::time::Instant>>,
353}
354
355impl Stream {
356    /// Create a new stream
357    pub fn new(id: StreamId) -> Self {
358        Self {
359            id,
360            state: Mutex::new(StreamState::Open),
361            reliable_offset: AtomicU32::new(0),
362            recv_sequence: AtomicU32::new(0),
363            send_buffer: Mutex::new(VecDeque::new()),
364            unreliable_buffer: Mutex::new(VecDeque::new()),
365            recv_buffer: Mutex::new(VecDeque::new()),
366            recv_buffer_bytes: AtomicUsize::new(0),
367            recv_ready: Mutex::new(VecDeque::new()),
368            recv_notify: Notify::new(),
369            local_finished: AtomicBool::new(false),
370            remote_finished: AtomicBool::new(false),
371            priority: AtomicU32::new(0),
372            send_semaphore: Arc::new(Semaphore::new(MAX_PENDING_PACKETS)),
373            peer_send_window: AtomicU32::new(INITIAL_STREAM_WINDOW),
374            local_recv_window: AtomicU32::new(INITIAL_STREAM_WINDOW),
375            bytes_since_last_update: AtomicU32::new(0),
376            pending_window_update: AtomicU32::new(0),
377            rto: std::sync::Mutex::new(RtoEstimator::new()),
378            last_data_recv_at: std::sync::Mutex::new(None),
379        }
380    }
381
382    // ── RFC 6298 retransmission timeout ──
383
384    /// Current retransmission timeout. A poisoned lock is recovered by taking
385    /// the inner value — the RTO is a heuristic, not a correctness invariant.
386    fn current_rto(&self) -> Duration {
387        match self.rto.lock() {
388            Ok(g) => g.rto(),
389            Err(poisoned) => poisoned.into_inner().rto(),
390        }
391    }
392
393    /// Reset the RTT estimator (Phase 4 / QUIC §9.4): a migration path switch lands
394    /// on a different network, so the old RTT must not carry over. A poisoned lock
395    /// is recovered by taking the inner value — the RTO is a heuristic.
396    pub fn reset_rto(&self) {
397        match self.rto.lock() {
398            Ok(mut g) => g.reset(),
399            Err(poisoned) => poisoned.into_inner().reset(),
400        }
401    }
402
403    /// Smoothed RTT estimate, or `None` before the first measurement. Feeds the
404    /// RFC-9002 time-threshold loss detector (L1-B).
405    fn smoothed_rtt(&self) -> Option<Duration> {
406        match self.rto.lock() {
407            Ok(g) => g.srtt,
408            Err(poisoned) => poisoned.into_inner().srtt,
409        }
410    }
411
412    /// Feed a fresh RTT measurement into the RTO estimator.
413    fn record_rtt_sample(&self, rtt: Duration) {
414        let mut g = match self.rto.lock() {
415            Ok(g) => g,
416            Err(poisoned) => poisoned.into_inner(),
417        };
418        g.on_rtt_sample(rtt);
419    }
420
421    /// Tell the RTO estimator a segment timed out (exponential backoff).
422    fn note_rto_timeout(&self) {
423        let mut g = match self.rto.lock() {
424            Ok(g) => g,
425            Err(poisoned) => poisoned.into_inner(),
426        };
427        g.on_timeout();
428    }
429
430    /// Get stream ID
431    pub fn id(&self) -> StreamId {
432        self.id
433    }
434
435    /// Get current state
436    pub async fn state(&self) -> StreamState {
437        *self.state.lock().await
438    }
439
440    /// Get priority
441    pub fn priority(&self) -> u32 {
442        self.priority.load(Ordering::Relaxed)
443    }
444
445    /// Set priority
446    pub fn set_priority(&self, priority: u32) {
447        self.priority.store(priority, Ordering::Relaxed);
448    }
449
450    // ── Flow control (Phase 4.3) ──
451
452    /// Bytes the peer currently allows us to send.
453    pub fn peer_send_window(&self) -> u32 {
454        self.peer_send_window.load(Ordering::Acquire)
455    }
456
457    /// Atomically reserve `n` bytes from the peer's send window.
458    /// Returns `true` if the reservation succeeded (and the window
459    /// was decremented); `false` if the window doesn't have enough
460    /// capacity — caller must wait for a `WINDOW_UPDATE`.
461    pub fn try_consume_send_window(&self, n: u32) -> bool {
462        let mut cur = self.peer_send_window.load(Ordering::Acquire);
463        loop {
464            if cur < n {
465                return false;
466            }
467            match self.peer_send_window.compare_exchange_weak(
468                cur,
469                cur - n,
470                Ordering::AcqRel,
471                Ordering::Acquire,
472            ) {
473                Ok(_) => return true,
474                Err(actual) => cur = actual,
475            }
476        }
477    }
478
479    /// Process an inbound `WINDOW_UPDATE` from the peer. The payload is a
480    /// **relative credit** — the number of bytes the peer's application just
481    /// consumed and is therefore newly willing to receive. We *add* it to the
482    /// send window (saturating at [`MAX_SEND_WINDOW`] so a misbehaving peer's
483    /// inflated credit cannot overflow the counter).
484    ///
485    /// Relative credit (vs. an absolute window) is what makes flow control
486    /// correct for a session of any length: the sender's window is
487    /// `initial + Σ credit_granted − Σ bytes_sent` = `initial + consumed −
488    /// sent`, so the receiver's outstanding (unconsumed) bytes `sent − consumed`
489    /// are bounded by `initial`. An absolute u32 window could not express this
490    /// for sessions exceeding 4 GiB and over-committed the receiver's buffer.
491    pub fn apply_peer_window_update(&self, credit: u32) {
492        let mut cur = self.peer_send_window.load(Ordering::Acquire);
493        loop {
494            let next = cur.saturating_add(credit).min(MAX_SEND_WINDOW);
495            if next == cur {
496                return; // already at the cap; nothing to add
497            }
498            match self.peer_send_window.compare_exchange_weak(
499                cur,
500                next,
501                Ordering::AcqRel,
502                Ordering::Acquire,
503            ) {
504                Ok(_) => return,
505                Err(actual) => cur = actual,
506            }
507        }
508    }
509
510    /// Bytes the local side has granted the peer.
511    pub fn local_recv_window(&self) -> u32 {
512        self.local_recv_window.load(Ordering::Acquire)
513    }
514
515    /// Record that the application has actually consumed `n` bytes from this
516    /// stream (called by the receive *delivery* task on real drainage, not
517    /// on routing). Accumulates the consumed bytes and, once the unreported
518    /// total crosses half the initial window, returns `Some(credit)` — the
519    /// **relative credit** to advertise in a `WINDOW_UPDATE` (the peer *adds*
520    /// it to its send window). The half-window threshold trades update frequency
521    /// against peer stalls.
522    pub fn record_app_consumed(&self, n: u32) -> Option<u32> {
523        let pending = self.bytes_since_last_update.fetch_add(n, Ordering::AcqRel) + n;
524        let threshold = INITIAL_STREAM_WINDOW / 2;
525        if pending >= threshold {
526            // Grant exactly the bytes we accumulated since the last update and
527            // reset the accumulator. Use a CAS-free `fetch_sub` of the granted
528            // amount rather than `store(0)` so a concurrent consume isn't lost.
529            self.bytes_since_last_update
530                .fetch_sub(pending, Ordering::AcqRel);
531            // Keep the (now informational) local_recv_window in step for stats.
532            self.local_recv_window.fetch_add(pending, Ordering::AcqRel);
533            Some(pending)
534        } else {
535            None
536        }
537    }
538
539    /// Stage relative flow-control credit to be flushed by the send loop.
540    /// Called by the receive delivery task after it credits real app
541    /// consumption. Credits **accumulate additively** (saturating at
542    /// `u32::MAX`) rather than overwriting, so several grants landing between
543    /// two send-loop flushes are summed instead of lost — the send loop is the
544    /// single emitter (epoch-safe), and it may run arbitrarily after a grant.
545    pub fn stage_window_update_credit(&self, credit: u32) {
546        let mut cur = self.pending_window_update.load(Ordering::Acquire);
547        loop {
548            let next = cur.saturating_add(credit);
549            if next == cur {
550                return; // nothing to add (zero credit, or already saturated)
551            }
552            match self.pending_window_update.compare_exchange_weak(
553                cur,
554                next,
555                Ordering::AcqRel,
556                Ordering::Acquire,
557            ) {
558                Ok(_) => return,
559                Err(actual) => cur = actual,
560            }
561        }
562    }
563
564    /// Take all staged credit (swaps the slot back to `0`). The send loop calls
565    /// this each drain pass and emits one `WINDOW_UPDATE` carrying the summed
566    /// credit if `Some`.
567    pub fn take_pending_window_update(&self) -> Option<u32> {
568        match self.pending_window_update.swap(0, Ordering::AcqRel) {
569            0 => None,
570            w => Some(w),
571        }
572    }
573
574    /// Assign the next gap-free reliable `stream_offset`, failing closed at `u32`
575    /// exhaustion (T4.5, reviewer §1). The cursor (`reliable_offset`) holds the
576    /// next-to-assign value; the last assignable offset is `u32::MAX - 1` (assigning
577    /// it advances the cursor to the `u32::MAX` exhaustion sentinel). A plain
578    /// `fetch_add(1)` would wrap `u32::MAX` back to `0`, re-issuing offset `0` and
579    /// corrupting reassembly / SACK dedup (a duplicate offset — NOT an AEAD nonce
580    /// reuse, since the nonce is the `u64` packet number). Instead we fail closed,
581    /// mirroring the epoch-saturation guard in [`Session::rekey`]. The CAS loop keeps
582    /// the "never wrap" invariant correct even under a (rare) concurrent caller.
583    fn next_reliable_offset(&self) -> Result<SequenceNumber, CoreError> {
584        loop {
585            let cur = self.reliable_offset.load(Ordering::SeqCst);
586            let next = cur.checked_add(1).ok_or_else(|| {
587                CoreError::StreamError(
588                    "reliable stream offset space exhausted (u32); reconnect required".into(),
589                )
590            })?;
591            if self
592                .reliable_offset
593                .compare_exchange(cur, next, Ordering::SeqCst, Ordering::SeqCst)
594                .is_ok()
595            {
596                return Ok(cur);
597            }
598        }
599    }
600
601    /// Queue data for sending with reliability.
602    ///
603    /// Returns the gap-free `stream_offset` assigned to this chunk (the reassembly
604    /// / SACK key). The wire packet number is assigned later, at send time, by the
605    /// data pump (① — Phase 4). Fails closed with [`CoreError::StreamError`] once the
606    /// `u32` offset space is exhausted (T4.5) — the acquired backpressure permit is
607    /// released on that path so the semaphore accounting stays correct.
608    pub async fn send_reliable(&self, data: Bytes) -> Result<SequenceNumber, CoreError> {
609        // Backpressure: wait until there is space in the buffer.
610        // PANIC-SAFETY: `Semaphore::acquire` only errors after `close()`. The
611        // `send_semaphore` is a private field of this struct, constructed in
612        // `Stream::new` and never closed anywhere in the crate — the variant
613        // is structurally unreachable.
614        #[allow(clippy::expect_used)]
615        let permit = self
616            .send_semaphore
617            .acquire()
618            .await
619            .expect("Semaphore closed");
620
621        // Gap-free reliable-data offset (A.5) — the reassembly / SACK key. Assigned
622        // BEFORE forgetting the permit so a fail-closed exhaustion (`?`) drops the
623        // permit and releases the slot instead of leaking backpressure capacity.
624        let stream_offset = self.next_reliable_offset()?;
625        permit.forget();
626
627        let pending = PendingData {
628            stream_offset,
629            data,
630            sent_at: None,
631            retries: 0,
632            lost: false,
633        };
634
635        self.send_buffer.lock().await.push_back(pending);
636
637        Ok(stream_offset)
638    }
639
640    /// Queue data for unreliable sending. Fire-and-forget; the wire packet number
641    /// is assigned at send time by the data pump (① — Phase 4).
642    pub async fn send_unreliable(&self, data: Bytes) {
643        // Unreliable data does not consume buffer permits.
644        self.unreliable_buffer.lock().await.push_back(data);
645    }
646
647    /// Get the next segment to (re)transmit, or `None` if nothing is due.
648    ///
649    /// `cwnd_budget` is how many bytes of *new* data the congestion window
650    /// currently permits. Retransmissions ignore it — loss recovery must always
651    /// proceed — but a first transmission is withheld (`None`) when it would
652    /// exceed the budget, so the next drain resumes once ACKs free the window.
653    /// Pass `u64::MAX` to disable the limit.
654    pub async fn poll_send(&self, cwnd_budget: u64) -> Option<OutboundSegment> {
655        // Unreliable data is fire-and-forget and not congestion-controlled.
656        if let Some(data) = self.unreliable_buffer.lock().await.pop_front() {
657            return Some(OutboundSegment {
658                // Unreliable segments are not reassembled; offset is unused (the
659                // send path does not prefix it).
660                stream_offset: 0,
661                data,
662                reliable: false,
663                retransmit: false,
664            });
665        }
666
667        let mut buffer = self.send_buffer.lock().await;
668        let now = tokio::time::Instant::now();
669        // Adaptive RFC 6298 timeout (was a fixed 500ms).
670        let timeout = self.current_rto();
671
672        // Pass 0: fast-retransmit a segment the SACK loss detector flagged (RFC
673        // 9002, L1-B). Recovers a loss in ~1 RTT instead of waiting out an RTO.
674        // Like Pass 1 it BYPASSES cwnd/window (loss recovery must always proceed —
675        // the flow-control invariant), but it does NOT back the RTO off (this was a
676        // SACK-detected loss, not a timeout). Clears the flag and marks the segment
677        // retransmitted (ambiguous for RTT — Karn).
678        for pending in buffer.iter_mut() {
679            if pending.lost && pending.sent_at.is_some() {
680                pending.lost = false;
681                pending.sent_at = Some(now);
682                pending.retries += 1;
683                return Some(OutboundSegment {
684                    stream_offset: pending.stream_offset,
685                    data: pending.data.clone(),
686                    reliable: true,
687                    retransmit: true,
688                });
689            }
690        }
691
692        // Pass 1: a timed-out segment (retransmission) — always allowed.
693        for pending in buffer.iter_mut() {
694            if let Some(sent_at) = pending.sent_at {
695                if now.duration_since(sent_at) >= timeout {
696                    pending.sent_at = Some(now);
697                    pending.retries += 1;
698                    // Back the RTO off exponentially for the next attempt.
699                    self.note_rto_timeout();
700                    return Some(OutboundSegment {
701                        stream_offset: pending.stream_offset,
702                        data: pending.data.clone(),
703                        reliable: true,
704                        retransmit: true,
705                    });
706                }
707            }
708        }
709
710        // Pass 2: the next unsent segment, if it fits BOTH the congestion window
711        // AND the peer's advertised flow-control window. In-order: if the head
712        // unsent segment doesn't fit, stop (don't skip). Retransmissions (Pass 1)
713        // bypass both budgets — those bytes were already accounted on first send
714        // (Karn), and loss recovery must always proceed.
715        for pending in buffer.iter_mut() {
716            if pending.sent_at.is_none() {
717                let len = pending.data.len() as u64;
718                if len > cwnd_budget {
719                    return None; // congestion window full — wait for ACKs to free it
720                }
721                // Flow-control enforcement: consume the peer's advertised
722                // receive window. If it is exhausted, withhold the segment and
723                // wait for a `WINDOW_UPDATE` — this is what propagates a slow
724                // peer-side consumer back to us as real backpressure (the
725                // receive delivery task only credits the window on actual app
726                // consumption). `try_consume_send_window` is an atomic CAS; on
727                // success the window is debited and we WILL send (no later check
728                // can fail), so the debit never leaks.
729                if !self.try_consume_send_window(len as u32) {
730                    return None; // peer flow-control window closed — wait for WINDOW_UPDATE
731                }
732                pending.sent_at = Some(now);
733                return Some(OutboundSegment {
734                    stream_offset: pending.stream_offset,
735                    data: pending.data.clone(),
736                    reliable: true,
737                    retransmit: false,
738                });
739            }
740        }
741
742        None
743    }
744
745    /// Mark a sequence number as acknowledged.
746    /// Returns the timestamp when the packet was originally sent and its size, if found.
747    pub async fn ack(&self, stream_offset: SequenceNumber) -> Option<(tokio::time::Instant, u64)> {
748        let mut buffer = self.send_buffer.lock().await;
749        let mut result = None;
750
751        // Find the segment (by gap-free `stream_offset`, A.5) and get its sent_at.
752        if let Some(pos) = buffer.iter().position(|p| p.stream_offset == stream_offset) {
753            let sent_at = buffer[pos].sent_at;
754            let retries = buffer[pos].retries;
755            let size = buffer[pos].data.len() as u64;
756            buffer.remove(pos);
757
758            // Released space, add permit back
759            self.send_semaphore.add_permits(1);
760
761            if let Some(sent_at) = sent_at {
762                result = Some((sent_at, size));
763                // Karn's algorithm: only sample RTT from segments that were not
764                // retransmitted — an ACK for a resent sequence is ambiguous.
765                if retries == 0 {
766                    let rtt = tokio::time::Instant::now().duration_since(sent_at);
767                    self.record_rtt_sample(rtt);
768                }
769            }
770        }
771
772        result
773    }
774
775    /// Reset a still-buffered reliable segment's send timestamp so the next
776    /// [`poll_send`](Self::poll_send) re-offers it immediately (as an unsent
777    /// segment) rather than waiting a full RTO for the retransmit pass. Used
778    /// when a send attempt failed *after* `poll_send` had already stamped
779    /// `sent_at` — the bytes never reached the wire, so the segment must not be
780    /// treated as in-flight. No-op if the segment was already acknowledged and
781    /// removed.
782    pub async fn mark_unsent(&self, stream_offset: SequenceNumber) {
783        let mut buffer = self.send_buffer.lock().await;
784        if let Some(pending) = buffer.iter_mut().find(|p| p.stream_offset == stream_offset) {
785            pending.sent_at = None;
786        }
787    }
788
789    // ── SACK (selective acknowledgement) — L1-A / A.5 ──
790
791    /// Build a [`Sack`] describing exactly the reliable-data sequences this stream
792    /// currently holds, derived from the **reorder state** (single source of truth):
793    /// the contiguous delivered run `[0, recv_sequence-1]` as one range, plus one
794    /// range per out-of-order island still buffered in `recv_buffer`. Returns
795    /// `None` if nothing has been received yet.
796    ///
797    /// Because the SACK is derived from what the reorder buffer actually holds, the
798    /// receiver never SACKs a sequence it has dropped (the SACK-without-data hazard
799    /// of a separate received-set). `ack_delay_us`: the caller's measured value, or
800    /// — when `0` — a coarse `now − last_data_recv_at` so the on-wire field is
801    /// populated. The range set is capped to [`crate::transport::sack::MAX_SACK_RANGES`]
802    /// by [`Sack::from_inclusive_ranges`] so it always decodes at the peer.
803    pub async fn received_sack(&self, ack_delay_us: u32) -> Option<Sack> {
804        let next = self.recv_sequence.load(Ordering::SeqCst);
805        let buf = self.recv_buffer.lock().await;
806        if next == 0 && buf.is_empty() {
807            return None;
808        }
809        // Contiguous delivered run first (lowest), then the buffered islands
810        // (all strictly above `next`, since `next` itself is the missing hole).
811        let mut ranges: Vec<(u32, u32)> = Vec::new();
812        if next > 0 {
813            ranges.push((0, next - 1));
814        }
815        let mut islands: Vec<SequenceNumber> = buf.iter().map(|(s, _)| *s).collect();
816        drop(buf);
817        islands.sort_unstable();
818        for s in islands {
819            match ranges.last_mut() {
820                // Coalesce adjacent / duplicate into the previous ascending range.
821                Some(last) if s <= last.1.saturating_add(1) => {
822                    if s > last.1 {
823                        last.1 = s;
824                    }
825                }
826                _ => ranges.push((s, s)),
827            }
828        }
829
830        let delay = if ack_delay_us != 0 {
831            ack_delay_us
832        } else {
833            // Coarse fallback: time since the most recent data arrival.
834            let recv_at = match self.last_data_recv_at.lock() {
835                Ok(g) => *g,
836                Err(poisoned) => *poisoned.into_inner(),
837            };
838            recv_at
839                .map(|t| {
840                    let micros = tokio::time::Instant::now().duration_since(t).as_micros();
841                    u32::try_from(micros).unwrap_or(u32::MAX)
842                })
843                .unwrap_or(0)
844        };
845        Sack::from_inclusive_ranges(ranges, delay)
846    }
847
848    /// Process a received SACK, retiring **every** buffered reliable segment whose
849    /// gap-free `stream_offset` the SACK covers (A.5; the SACK ranges are over
850    /// `stream_offset`, not the control-frame-holed wire `sequence`). Returns a
851    /// [`SackResult`] listing the newly-retired segments so the caller can feed
852    /// congestion control / the RTT estimator per segment.
853    ///
854    /// RTT is sampled here (Karn's algorithm) only for segments that were never
855    /// retransmitted (`retries == 0`); `RetiredSegment::was_retransmit` marks the
856    /// rest so the caller does not double-count or use an ambiguous sample.
857    ///
858    /// This is a cumulative retire: a SACK re-acks every still-buffered offset it
859    /// covers, so a lost ACK no longer strands a segment — the next SACK retires
860    /// it. **No loss detection / fast-retransmit here** — that is L1-B.
861    pub async fn on_sack(&self, sack: &Sack) -> SackResult {
862        let mut buffer = self.send_buffer.lock().await;
863        let mut retired = Vec::new();
864        let mut freed = 0u32;
865        let now = tokio::time::Instant::now();
866
867        // Retain only the segments the SACK does NOT cover; collect the rest.
868        let mut i = 0;
869        while i < buffer.len() {
870            // SACK ranges are over the gap-free reliable `stream_offset` (A.5),
871            // NOT the wire `sequence` (which has control-frame holes).
872            // PANIC-SAFETY: `i < buffer.len()` is the loop guard, so the index is
873            // in range; `get` cannot return `None`.
874            #[allow(clippy::unwrap_used, clippy::disallowed_methods)]
875            let covered = sack.acks(buffer.get(i).unwrap().stream_offset);
876            if covered {
877                // PANIC-SAFETY: `i` is a valid index (loop guard); `remove`
878                // returns `Some` for an in-range index in a VecDeque.
879                #[allow(clippy::unwrap_used, clippy::disallowed_methods)]
880                let pending = buffer.remove(i).unwrap();
881                freed += 1;
882                let was_retransmit = pending.retries > 0;
883                let size = pending.data.len() as u64;
884                if let Some(sent_at) = pending.sent_at {
885                    // Karn: only sample RTT from segments never retransmitted.
886                    if !was_retransmit {
887                        let rtt = now.duration_since(sent_at);
888                        self.record_rtt_sample(rtt);
889                    }
890                }
891                retired.push(RetiredSegment {
892                    sent_at: pending.sent_at,
893                    size,
894                    was_retransmit,
895                });
896                // Do NOT advance `i`: `remove` shifted the next element into `i`.
897            } else {
898                i += 1;
899            }
900        }
901
902        // Loss detection (RFC 9002 §6.1.1) over the still-buffered, in-flight
903        // segments, keyed on the gap-free `stream_offset`: declare lost any offset
904        // at least `PACKET_THRESHOLD` behind `largest_acked` (packet-threshold), or
905        // — if an srtt is known — any offset below `largest_acked` aged past
906        // srtt·9/8 (RACK time-threshold). Flagged segments are fast-retransmitted
907        // by `poll_send`'s Pass-0; already-flagged ones are skipped (no double-count
908        // into congestion control).
909        // T5.4: clamp `largest_acked` to the highest `stream_offset` we have actually assigned
910        // (`reliable_offset` is the next-to-assign, so it bounds every offset on the wire). A
911        // peer cannot legitimately ack an offset we never sent; without this an authenticated
912        // peer inflating `largest_acked` (e.g. `high + 1e6`) would declare freshly-sent,
913        // in-flight segments "lost" and force a cwnd-bypassing Pass-0 retransmit storm.
914        let largest_acked = sack
915            .largest_acked
916            .min(self.reliable_offset.load(Ordering::SeqCst));
917        // RFC 9002: loss_delay = max(kGranularity, kTimeThreshold · smoothed_rtt).
918        // The kGranularity (1 ms) floor is load-bearing: without it a near-zero
919        // srtt makes the threshold ~0 and flags freshly-sent segments as "aged",
920        // which would over-report loss.
921        let time_threshold = self
922            .smoothed_rtt()
923            .map(|r| std::cmp::max(Duration::from_millis(1), r * 9 / 8));
924        let mut lost = Vec::new();
925        for pending in buffer.iter_mut() {
926            if pending.lost {
927                continue;
928            }
929            let Some(sent_at) = pending.sent_at else {
930                continue; // not yet on the wire — nothing to lose
931            };
932            if pending.stream_offset >= largest_acked {
933                continue; // not behind the largest ack — still legitimately in flight
934            }
935            let packet_lost =
936                largest_acked >= pending.stream_offset.saturating_add(PACKET_THRESHOLD);
937            let time_lost = time_threshold.is_some_and(|t| now.duration_since(sent_at) >= t);
938            if packet_lost || time_lost {
939                pending.lost = true;
940                lost.push(LostSegment {
941                    stream_offset: pending.stream_offset,
942                    size: pending.data.len() as u64,
943                });
944            }
945        }
946        drop(buffer);
947
948        // Return the buffer permits for every retired segment in one shot.
949        if freed > 0 {
950            self.send_semaphore.add_permits(freed as usize);
951        }
952
953        SackResult { retired, lost }
954    }
955
956    // ── Receive-side in-order reassembly (A.5) ──
957
958    /// Accept reliable data payloads carried at `sequence` and return the
959    /// contiguous in-order run now deliverable to the application, in ascending
960    /// order. The returned `Vec` is empty when this is a future hole (buffered for
961    /// later), a duplicate, or refused for capacity.
962    ///
963    /// `payloads` is normally one element (a single RELIABLE frame); a COALESCED
964    /// bundle passes its sub-payloads so the whole bundle occupies one cursor
965    /// position. This is the **single source of truth** for receive ordering: the
966    /// live data pump routes every reliable app payload through here so the app
967    /// sees the reliable stream strictly in `sequence` order even over a
968    /// reordering (UDP) path. Out-of-order segments are held in `recv_buffer`
969    /// (bounded by `MAX_RECV_REORDER`); the data-arrival instant is stamped for
970    /// the SACK `ack_delay_us`.
971    pub async fn accept_in_order(
972        &self,
973        sequence: SequenceNumber,
974        payloads: Vec<Bytes>,
975    ) -> Vec<Bytes> {
976        {
977            let mut at = match self.last_data_recv_at.lock() {
978                Ok(g) => g,
979                Err(poisoned) => poisoned.into_inner(),
980            };
981            *at = Some(tokio::time::Instant::now());
982        }
983
984        let expected = self.recv_sequence.load(Ordering::SeqCst);
985        if sequence < expected {
986            return Vec::new(); // duplicate of already-delivered data
987        }
988
989        let mut buf = self.recv_buffer.lock().await;
990        if sequence != expected {
991            // Future segment: buffer if not already held, within the entry cap, AND within
992            // the per-stream byte budget (H-3). A refused segment is NOT recorded, so it is
993            // not SACKed → the sender retransmits it (no SACK-without-data hazard, bounded
994            // memory regardless of per-entry frame size).
995            let already = buf.iter().any(|(s, _)| *s == sequence);
996            let seg_bytes: usize = payloads.iter().map(Bytes::len).sum();
997            let within_byte_budget = self
998                .recv_buffer_bytes
999                .load(Ordering::Relaxed)
1000                .saturating_add(seg_bytes)
1001                <= MAX_RECV_REORDER_BYTES;
1002            if !already && buf.len() < MAX_RECV_REORDER && within_byte_budget {
1003                buf.push_back((sequence, payloads));
1004                self.recv_buffer_bytes
1005                    .fetch_add(seg_bytes, Ordering::Relaxed);
1006            }
1007            return Vec::new();
1008        }
1009
1010        // In-order: deliver this segment's payloads, then drain any now-contiguous
1011        // buffered segments.
1012        let mut out = payloads;
1013        self.recv_sequence.fetch_add(1, Ordering::SeqCst);
1014        loop {
1015            let next = self.recv_sequence.load(Ordering::SeqCst);
1016            if let Some(pos) = buf.iter().position(|(s, _)| *s == next) {
1017                // PANIC-SAFETY: `pos` was just returned by `position`, so the
1018                // index is valid; `recv_buf` is locked, so no concurrent drain.
1019                #[allow(clippy::unwrap_used, clippy::disallowed_methods)]
1020                let (_, payloads) = buf.remove(pos).unwrap();
1021                let seg_bytes: usize = payloads.iter().map(Bytes::len).sum();
1022                self.recv_buffer_bytes
1023                    .fetch_sub(seg_bytes, Ordering::Relaxed);
1024                out.extend(payloads);
1025                self.recv_sequence.fetch_add(1, Ordering::SeqCst);
1026            } else {
1027                break;
1028            }
1029        }
1030        out
1031    }
1032
1033    /// Total payload bytes currently held in the out-of-order reorder buffer (H-3). Bounded
1034    /// by `MAX_RECV_REORDER_BYTES`; exposed so the byte bound is observable/testable.
1035    pub fn recv_reorder_bytes(&self) -> usize {
1036        self.recv_buffer_bytes.load(Ordering::Relaxed)
1037    }
1038
1039    /// Pull-API adapter over [`accept_in_order`](Self::accept_in_order): buffer a
1040    /// single reliable payload for in-order reassembly and push the released run
1041    /// into `recv_ready` for [`recv`](Self::recv) / [`try_recv`](Self::try_recv).
1042    /// (Not used by the live session pump, which consumes the returned run
1043    /// directly; retained for the pull-style read API.)
1044    pub async fn on_receive(&self, sequence: SequenceNumber, data: Bytes) {
1045        let delivered = self.accept_in_order(sequence, vec![data]).await;
1046        if !delivered.is_empty() {
1047            let mut ready = self.recv_ready.lock().await;
1048            for d in delivered {
1049                ready.push_back(d);
1050            }
1051            drop(ready);
1052            self.recv_notify.notify_waiters();
1053        }
1054    }
1055
1056    /// Read data from the stream (async, waits if no data available)
1057    pub async fn recv(&self) -> Option<Bytes> {
1058        loop {
1059            {
1060                let mut ready = self.recv_ready.lock().await;
1061                if let Some(data) = ready.pop_front() {
1062                    return Some(data);
1063                }
1064
1065                // Check if stream is closed
1066                if self.remote_finished.load(Ordering::SeqCst) {
1067                    return None;
1068                }
1069            }
1070
1071            // Wait for new data
1072            self.recv_notify.notified().await;
1073        }
1074    }
1075
1076    /// Try to read data without waiting
1077    pub async fn try_recv(&self) -> Option<Bytes> {
1078        self.recv_ready.lock().await.pop_front()
1079    }
1080
1081    /// Mark local side as finished (no more data to send)
1082    pub async fn finish(&self) {
1083        self.local_finished.store(true, Ordering::SeqCst);
1084        self.update_state().await;
1085    }
1086
1087    /// Mark remote side as finished
1088    pub async fn on_remote_finish(&self) {
1089        self.remote_finished.store(true, Ordering::SeqCst);
1090        self.recv_notify.notify_waiters();
1091        self.update_state().await;
1092    }
1093
1094    /// Update stream state based on finish flags
1095    async fn update_state(&self) {
1096        let local = self.local_finished.load(Ordering::SeqCst);
1097        let remote = self.remote_finished.load(Ordering::SeqCst);
1098
1099        let new_state = match (local, remote) {
1100            (true, true) => StreamState::Closed,
1101            (true, false) => StreamState::HalfClosedLocal,
1102            (false, true) => StreamState::HalfClosedRemote,
1103            (false, false) => StreamState::Open,
1104        };
1105
1106        *self.state.lock().await = new_state;
1107    }
1108
1109    /// Get number of pending send chunks
1110    pub async fn pending_send_count(&self) -> usize {
1111        self.send_buffer.lock().await.len()
1112    }
1113
1114    /// Get number of pending receive chunks
1115    pub async fn pending_recv_count(&self) -> usize {
1116        self.recv_ready.lock().await.len()
1117    }
1118
1119    /// Check if stream is closed
1120    pub fn is_closed(&self) -> bool {
1121        self.local_finished.load(Ordering::SeqCst) && self.remote_finished.load(Ordering::SeqCst)
1122    }
1123}
1124
1125impl std::fmt::Debug for Stream {
1126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1127        f.debug_struct("Stream")
1128            .field("id", &self.id)
1129            .field("recv_offset", &self.recv_sequence.load(Ordering::Relaxed))
1130            .field("priority", &self.priority.load(Ordering::Relaxed))
1131            .finish()
1132    }
1133}
1134
1135#[cfg(test)]
1136mod tests {
1137    use super::*;
1138
1139    #[tokio::test]
1140    async fn test_stream_send_recv() {
1141        let stream = Stream::new(1);
1142
1143        // Send data
1144        stream.send_reliable(Bytes::from("hello")).await.unwrap();
1145        stream.send_reliable(Bytes::from("world")).await.unwrap();
1146
1147        // Check pending
1148        assert_eq!(stream.pending_send_count().await, 2);
1149
1150        // Poll send twice, the second should be None because it's already sent and hasn't timed out
1151        let seg = stream.poll_send(u64::MAX).await.unwrap();
1152        assert_eq!(seg.stream_offset, 0);
1153        assert_eq!(seg.data, Bytes::from("hello"));
1154        assert!(seg.reliable);
1155        assert!(!seg.retransmit);
1156
1157        let seg2 = stream.poll_send(u64::MAX).await.unwrap();
1158        assert_eq!(seg2.stream_offset, 1);
1159        assert_eq!(seg2.data, Bytes::from("world"));
1160        assert!(seg2.reliable);
1161        assert!(!seg2.retransmit);
1162
1163        assert!(stream.poll_send(u64::MAX).await.is_none());
1164    }
1165
1166    /// T4.5 (reviewer §1, `stream_offset`): the gap-free reliable offset is a `u32`
1167    /// assigned per reliable segment. A naive `fetch_add(1)` silently wraps `u32::MAX`
1168    /// back to `0`, colliding with the first segment's offset and corrupting
1169    /// reassembly / SACK dedup (a duplicate offset, NOT a nonce reuse — the AEAD nonce
1170    /// is the `u64` packet number). It must fail-closed instead — mirroring the epoch
1171    /// saturation guard in `Session::rekey` — so an exhausted stream refuses new
1172    /// reliable data rather than corrupting the stream.
1173    #[tokio::test]
1174    async fn reliable_offset_fails_closed_at_u32_exhaustion() {
1175        let stream = Stream::new(1);
1176
1177        // The last assignable offset is `u32::MAX - 1`; assigning it leaves the cursor
1178        // at `u32::MAX`, the exhaustion sentinel.
1179        stream.reliable_offset.store(u32::MAX - 1, Ordering::SeqCst);
1180        let last = stream
1181            .send_reliable(Bytes::from_static(b"a"))
1182            .await
1183            .expect("offset u32::MAX-1 must still be assignable");
1184        assert_eq!(last, u32::MAX - 1, "last assignable reliable offset");
1185
1186        // The next send must fail-closed — never wrap to 0.
1187        let exhausted = stream.send_reliable(Bytes::from_static(b"b")).await;
1188        assert!(
1189            matches!(exhausted, Err(crate::errors::CoreError::StreamError(_))),
1190            "send_reliable must fail-closed (StreamError) at u32 offset exhaustion, got {exhausted:?}"
1191        );
1192
1193        // And directly at the sentinel.
1194        stream.reliable_offset.store(u32::MAX, Ordering::SeqCst);
1195        let at_sentinel = stream.send_reliable(Bytes::from_static(b"c")).await;
1196        assert!(
1197            at_sentinel.is_err(),
1198            "send_reliable at the u32::MAX sentinel must fail-closed, got {at_sentinel:?}"
1199        );
1200    }
1201
1202    #[tokio::test]
1203    async fn test_stream_retransmission() {
1204        // We use tokio::time::pause to mock time and test timeout
1205        tokio::time::pause();
1206        let stream = Stream::new(1);
1207
1208        stream.send_reliable(Bytes::from("hello")).await.unwrap();
1209
1210        // First send — not a retransmission.
1211        let seg = stream.poll_send(u64::MAX).await.unwrap();
1212        assert_eq!(seg.stream_offset, 0);
1213        assert!(seg.reliable);
1214        assert!(!seg.retransmit);
1215
1216        // Immediate poll should be None
1217        assert!(stream.poll_send(u64::MAX).await.is_none());
1218
1219        // Advance 400ms — still under the initial 1s RTO (RFC 6298 (2.1):
1220        // no RTT samples yet, so the timer sits at the 1-second default).
1221        tokio::time::advance(std::time::Duration::from_millis(400)).await;
1222        assert!(stream.poll_send(u64::MAX).await.is_none());
1223
1224        // Advance past the 1s initial RTO (total ~1.1s).
1225        tokio::time::advance(std::time::Duration::from_millis(700)).await;
1226
1227        // Now it should retransmit — flagged as a retransmission.
1228        let seg2 = stream.poll_send(u64::MAX).await.unwrap();
1229        assert_eq!(seg2.stream_offset, 0);
1230        assert_eq!(seg2.data, Bytes::from("hello"));
1231        assert!(seg2.reliable);
1232        assert!(seg2.retransmit);
1233
1234        // Ack it
1235        let acked = stream.ack(0).await;
1236        assert!(acked.is_some());
1237
1238        // Poll again - queue is empty
1239        assert!(stream.poll_send(u64::MAX).await.is_none());
1240    }
1241
1242    #[tokio::test]
1243    async fn mark_unsent_re_offers_without_waiting_rto() {
1244        // Time is paused, so nothing ever crosses the RTO — any re-offer here is
1245        // due to `mark_unsent`, not the retransmit timer.
1246        tokio::time::pause();
1247        let stream = Stream::new(1);
1248        stream.send_reliable(Bytes::from("hello")).await.unwrap();
1249
1250        // First poll stamps `sent_at`; an immediate re-poll yields nothing
1251        // (treated as in-flight, not yet timed out).
1252        let seg = stream.poll_send(u64::MAX).await.unwrap();
1253        assert_eq!(seg.stream_offset, 0);
1254        assert!(!seg.retransmit);
1255        assert!(stream.poll_send(u64::MAX).await.is_none());
1256
1257        // Simulate a send that failed *after* `poll_send` stamped the segment:
1258        // clear `sent_at` so it is no longer considered in-flight.
1259        stream.mark_unsent(0).await;
1260
1261        // It is re-offered immediately — without advancing past the RTO — and as
1262        // a fresh send (Pass 2), not a retransmission.
1263        let seg2 = stream.poll_send(u64::MAX).await.unwrap();
1264        assert_eq!(seg2.stream_offset, 0);
1265        assert_eq!(seg2.data, Bytes::from("hello"));
1266        assert!(seg2.reliable);
1267        assert!(!seg2.retransmit);
1268
1269        // `mark_unsent` on an already-acked (removed) segment is a no-op.
1270        assert!(stream.ack(0).await.is_some());
1271        stream.mark_unsent(0).await; // no panic, no effect
1272        assert!(stream.poll_send(u64::MAX).await.is_none());
1273    }
1274
1275    #[tokio::test]
1276    async fn poll_send_respects_the_cwnd_budget() {
1277        let stream = Stream::new(1);
1278        stream
1279            .send_reliable(Bytes::from("0123456789"))
1280            .await
1281            .unwrap(); // 10 bytes
1282        stream.send_reliable(Bytes::from("abcde")).await.unwrap(); // 5 bytes
1283
1284        // Budget of 10 admits the 10-byte head segment.
1285        let seg = stream.poll_send(10).await.unwrap();
1286        assert_eq!(seg.data.len(), 10);
1287        assert!(!seg.retransmit);
1288
1289        // Budget of 4 is too small for the next (5-byte) segment → withheld.
1290        assert!(stream.poll_send(4).await.is_none());
1291
1292        // A budget of 5 now admits it.
1293        let seg2 = stream.poll_send(5).await.unwrap();
1294        assert_eq!(seg2.data, Bytes::from("abcde"));
1295    }
1296
1297    #[tokio::test]
1298    async fn test_stream_in_order_receive() {
1299        let stream = Stream::new(1);
1300
1301        // Receive in order
1302        stream.on_receive(0, Bytes::from("first")).await;
1303        stream.on_receive(1, Bytes::from("second")).await;
1304
1305        assert_eq!(stream.try_recv().await, Some(Bytes::from("first")));
1306        assert_eq!(stream.try_recv().await, Some(Bytes::from("second")));
1307        assert_eq!(stream.try_recv().await, None);
1308    }
1309
1310    #[tokio::test]
1311    async fn test_stream_out_of_order_receive() {
1312        let stream = Stream::new(1);
1313
1314        // Receive out of order
1315        stream.on_receive(1, Bytes::from("second")).await;
1316        stream.on_receive(0, Bytes::from("first")).await;
1317
1318        // Should be reordered
1319        assert_eq!(stream.try_recv().await, Some(Bytes::from("first")));
1320        assert_eq!(stream.try_recv().await, Some(Bytes::from("second")));
1321    }
1322
1323    #[tokio::test]
1324    async fn test_stream_state() {
1325        let stream = Stream::new(1);
1326
1327        assert_eq!(stream.state().await, StreamState::Open);
1328
1329        stream.finish().await;
1330        assert_eq!(stream.state().await, StreamState::HalfClosedLocal);
1331
1332        stream.on_remote_finish().await;
1333        assert_eq!(stream.state().await, StreamState::Closed);
1334        assert!(stream.is_closed());
1335    }
1336
1337    #[tokio::test]
1338    async fn test_stream_backpressure() {
1339        let stream = Stream::new(1);
1340
1341        // Fill the buffer
1342        for _ in 0..MAX_PENDING_PACKETS {
1343            stream.send_reliable(Bytes::from("data")).await.unwrap();
1344        }
1345
1346        assert_eq!(stream.pending_send_count().await, MAX_PENDING_PACKETS);
1347
1348        // Try to send one more with timeout
1349        let send_future = stream.send_reliable(Bytes::from("blocked"));
1350        let result = tokio::time::timeout(std::time::Duration::from_millis(100), send_future).await;
1351        assert!(result.is_err(), "Send should have blocked");
1352
1353        // Ack one
1354        stream.ack(0).await;
1355
1356        // Now it should succeed
1357        let send_future = stream.send_reliable(Bytes::from("resumed"));
1358        let result = tokio::time::timeout(std::time::Duration::from_millis(100), send_future).await;
1359        assert!(result.is_ok(), "Send should have succeeded after ack");
1360        assert_eq!(stream.pending_send_count().await, MAX_PENDING_PACKETS);
1361    }
1362
1363    // ── SACK (selective acknowledgement) — L1-A ──
1364
1365    /// Stage segments 0..=5 on the send buffer, feed a SACK that covers
1366    /// {0,1,2,4,5} (gap at 3), and assert it retires exactly those five segments,
1367    /// leaving only segment 3 buffered. This is the headline L1-A behaviour: a
1368    /// single SACK retires multiple segments at once, skipping the gap.
1369    #[tokio::test]
1370    async fn on_sack_retires_all_covered_segments_skipping_the_gap() {
1371        let stream = Stream::new(1);
1372        for i in 0..6u32 {
1373            let seq = stream
1374                .send_reliable(Bytes::from(format!("seg-{i}")))
1375                .await
1376                .unwrap();
1377            assert_eq!(seq, i);
1378            // Stamp it as in-flight so RTT sampling has a `sent_at`.
1379            let seg = stream.poll_send(u64::MAX).await.expect("poll");
1380            assert_eq!(seg.stream_offset, i);
1381        }
1382        assert_eq!(stream.pending_send_count().await, 6);
1383
1384        // SACK covers {0,1,2,4,5} — segment 3 is the gap.
1385        let sack = Sack::from_received(&[0, 1, 2, 4, 5], 1234).expect("sack");
1386        assert_eq!(sack.ranges(), &[(4, 5), (0, 2)]);
1387        let result = stream.on_sack(&sack).await;
1388
1389        // Five segments retired, none of them retransmissions.
1390        assert_eq!(result.retired.len(), 5);
1391        assert!(result.retired.iter().all(|r| !r.was_retransmit));
1392        assert!(result.retired.iter().all(|r| r.sent_at.is_some()));
1393
1394        // Only segment 3 remains buffered.
1395        assert_eq!(stream.pending_send_count().await, 1);
1396        // Re-acking the retired sequences finds nothing (already removed); seq 3
1397        // is still ackable.
1398        for retired_seq in [0u32, 1, 2, 4, 5] {
1399            assert!(
1400                stream.ack(retired_seq).await.is_none(),
1401                "seq {retired_seq} should already be retired by the SACK"
1402            );
1403        }
1404        assert!(
1405            stream.ack(3).await.is_some(),
1406            "the gap segment 3 must remain buffered"
1407        );
1408    }
1409
1410    /// T5.4 (audit SACK-storm LOW): a SACK's `largest_acked` is clamped to the highest
1411    /// stream_offset actually sent, so an authenticated peer can't inflate it (e.g.
1412    /// `high + 1e6`) to declare freshly-sent, legitimately-in-flight segments "lost" and force
1413    /// a cwnd-bypassing Pass-0 retransmit storm.
1414    #[tokio::test]
1415    async fn on_sack_clamps_inflated_largest_acked() {
1416        let stream = Stream::new(1);
1417        for i in 0..5u32 {
1418            let seq = stream
1419                .send_reliable(Bytes::from(format!("seg-{i}")))
1420                .await
1421                .unwrap();
1422            assert_eq!(seq, i);
1423            let seg = stream.poll_send(u64::MAX).await.expect("poll"); // stamps sent_at
1424            assert_eq!(seg.stream_offset, i);
1425        }
1426        // A SACK that acks NONE of our segments (0..5) but claims a `largest_acked` far beyond
1427        // anything we ever sent.
1428        let sack = Sack::from_received(&[1_000_000], 0).expect("sack");
1429        assert_eq!(sack.largest_acked, 1_000_000);
1430        let result = stream.on_sack(&sack).await;
1431        // The freshest in-flight segment (within PACKET_THRESHOLD of the highest sent) must NOT
1432        // be flagged lost — the clamp limits loss detection to the real sent range.
1433        assert!(
1434            !result.lost.iter().any(|l| l.stream_offset == 4),
1435            "an inflated largest_acked must not flag the freshest in-flight segment as lost"
1436        );
1437    }
1438
1439    /// A SACK that covers nothing still buffered (stale / duplicate) retires
1440    /// nothing and leaves the send buffer intact.
1441    #[tokio::test]
1442    async fn on_sack_for_unbuffered_sequences_retires_nothing() {
1443        let stream = Stream::new(1);
1444        stream.send_reliable(Bytes::from("zero")).await.unwrap(); // seq 0
1445        let _ = stream.poll_send(u64::MAX).await.expect("poll");
1446
1447        // SACK only covers high sequences we never sent.
1448        let sack = Sack::from_received(&[100, 101, 102], 0).expect("sack");
1449        let result = stream.on_sack(&sack).await;
1450        assert!(result.retired.is_empty());
1451        assert_eq!(stream.pending_send_count().await, 1);
1452    }
1453
1454    /// A retransmitted segment retired by a SACK is flagged `was_retransmit`, so
1455    /// the caller does not sample RTT from it (Karn's algorithm).
1456    #[tokio::test]
1457    async fn on_sack_flags_retransmits_for_karn() {
1458        tokio::time::pause();
1459        let stream = Stream::new(1);
1460        stream.send_reliable(Bytes::from("payload")).await.unwrap(); // seq 0
1461        let _ = stream.poll_send(u64::MAX).await.expect("first send");
1462
1463        // Force a retransmit by crossing the RTO, so retries > 0.
1464        tokio::time::advance(Duration::from_millis(1100)).await;
1465        let retx = stream.poll_send(u64::MAX).await.expect("retransmit");
1466        assert!(retx.retransmit);
1467
1468        let sack = Sack::from_received(&[0], 0).expect("sack");
1469        let result = stream.on_sack(&sack).await;
1470        assert_eq!(result.retired.len(), 1);
1471        assert!(
1472            result.retired[0].was_retransmit,
1473            "a retransmitted segment must be flagged so the caller skips RTT sampling"
1474        );
1475    }
1476
1477    // ── L1-B: loss detection (RFC 9002) + fast-retransmit ──
1478
1479    /// **L1-B packet-threshold loss + Pass-0 fast-retransmit.** Stage offsets
1480    /// 0..=5 in flight; a SACK acking only {4,5} declares every still-buffered
1481    /// offset ≤ largest_acked − PACKET_THRESHOLD(3) = 2 lost (0,1,2), leaving 3
1482    /// unflagged. `poll_send`'s Pass-0 then fast-retransmits a flagged-lost segment
1483    /// even with a CLOSED congestion window (cwnd_budget = 0), ahead of new data.
1484    #[tokio::test]
1485    async fn on_sack_packet_threshold_marks_lost_and_pass0_fast_retransmits() {
1486        // Pause time so no segment ages past the 1 ms time-threshold floor — this
1487        // isolates the PACKET-threshold (the time-threshold has its own test).
1488        tokio::time::pause();
1489        let stream = Stream::new(1);
1490        for _ in 0..6u32 {
1491            stream
1492                .send_reliable(Bytes::from_static(b"x"))
1493                .await
1494                .unwrap();
1495            let _ = stream.poll_send(u64::MAX).await.expect("in-flight");
1496        }
1497        // SACK acks offsets {4,5}: 0,1,2 are ≤ 5−3 → lost; 3 is within threshold.
1498        let sack = Sack::from_received(&[4, 5], 0).expect("sack");
1499        let result = stream.on_sack(&sack).await;
1500        assert_eq!(
1501            result.lost_offsets(),
1502            vec![0, 1, 2],
1503            "packet-threshold must flag every offset ≤ largest_acked − 3"
1504        );
1505        // Pass-0 re-sends a flagged segment even with a closed congestion window.
1506        let seg = stream
1507            .poll_send(0)
1508            .await
1509            .expect("Pass-0 fast-retransmit must ignore the congestion window");
1510        assert!(seg.retransmit, "Pass-0 segment is a retransmit");
1511        assert!(
1512            [0u32, 1, 2].contains(&seg.stream_offset),
1513            "a flagged-lost offset is fast-retransmitted (got {})",
1514            seg.stream_offset
1515        );
1516    }
1517
1518    /// **L1-B time-threshold (RACK) loss.** With an established srtt, a
1519    /// still-buffered segment older than srtt·9/8 is declared lost once a LATER
1520    /// segment is acked, even when the packet threshold cannot fire (fewer than 3
1521    /// newer offsets acked). Offsets 0 and 1 are in flight; a SACK acks only {1}
1522    /// (largest_acked = 1, so 0 is within the packet threshold) but 0 has aged past
1523    /// srtt·9/8 → lost by time-threshold.
1524    #[tokio::test]
1525    async fn on_sack_time_threshold_marks_aged_segment_lost() {
1526        tokio::time::pause();
1527        let stream = Stream::new(1);
1528        // Establish a small srtt: send offset 0, ack it after ~10 ms.
1529        stream
1530            .send_reliable(Bytes::from_static(b"a"))
1531            .await
1532            .unwrap(); // offset 0
1533        let _ = stream.poll_send(u64::MAX).await.expect("send 0");
1534        tokio::time::advance(Duration::from_millis(10)).await;
1535        let _ = stream
1536            .on_sack(&Sack::from_received(&[0], 0).expect("sack"))
1537            .await; // srtt ≈ 10 ms
1538
1539        // Send offsets 1 and 2; age them well past srtt·9/8 (≈ 11 ms).
1540        stream
1541            .send_reliable(Bytes::from_static(b"b"))
1542            .await
1543            .unwrap(); // offset 1
1544        stream
1545            .send_reliable(Bytes::from_static(b"c"))
1546            .await
1547            .unwrap(); // offset 2
1548        let _ = stream.poll_send(u64::MAX).await.expect("send 1");
1549        let _ = stream.poll_send(u64::MAX).await.expect("send 2");
1550        tokio::time::advance(Duration::from_millis(50)).await;
1551
1552        // SACK acks only {2} (largest_acked = 2). Offset 1 is within the packet
1553        // threshold (2 − 1 < 3) but aged past srtt·9/8 → lost by time-threshold.
1554        let result = stream
1555            .on_sack(&Sack::from_received(&[2], 0).expect("sack"))
1556            .await;
1557        assert_eq!(
1558            result.lost_offsets(),
1559            vec![1],
1560            "an aged unacked segment must be flagged by the time-threshold"
1561        );
1562    }
1563
1564    /// `received_sack` derives ranges from the reorder state with a gap, and
1565    /// `ack_delay_us` is populated (non-zero) when the receiver holds before
1566    /// emitting (here, the coarse `now − recv_at` fallback under paused time).
1567    #[tokio::test]
1568    async fn received_sack_builds_ranges_with_gap_and_populates_ack_delay() {
1569        tokio::time::pause();
1570        let stream = Stream::new(1);
1571        // Receiver got 0,1,2,4,5 (gap at 3): 0,1,2 deliver in order (recv_sequence
1572        // → 3), 4 and 5 stay buffered as an island.
1573        for seq in [0u32, 1, 2, 4, 5] {
1574            let _ = stream
1575                .accept_in_order(seq, vec![Bytes::from_static(b"x")])
1576                .await;
1577        }
1578        // Hold briefly so `now − recv_at` is non-zero.
1579        tokio::time::advance(Duration::from_micros(500)).await;
1580
1581        let sack = stream
1582            .received_sack(0)
1583            .await
1584            .expect("non-empty received set");
1585        assert_eq!(sack.largest_acked, 5);
1586        // Contiguous run (0,2) plus the buffered island (4,5), descending.
1587        assert_eq!(sack.ranges(), &[(4, 5), (0, 2)]);
1588        assert!(
1589            sack.ack_delay_us >= 500,
1590            "ack_delay_us must be populated from the recv-to-emit hold (got {})",
1591            sack.ack_delay_us
1592        );
1593
1594        // An explicit (non-zero) ack_delay passes through verbatim.
1595        let sack2 = stream.received_sack(42).await.expect("non-empty");
1596        assert_eq!(sack2.ack_delay_us, 42);
1597    }
1598
1599    /// Nothing received yet yields no SACK.
1600    #[tokio::test]
1601    async fn received_sack_empty_returns_none() {
1602        let stream = Stream::new(1);
1603        assert!(stream.received_sack(0).await.is_none());
1604    }
1605
1606    /// `accept_in_order` delivers the contiguous run and buffers holes: feeding
1607    /// 0, then 2, then 1 yields `[0]`, `[]` (2 buffered), `[1, 2]` (1 fills the
1608    /// gap and drains the buffered 2) — strict in-order delivery.
1609    #[tokio::test]
1610    async fn accept_in_order_delivers_contiguous_run_and_buffers_holes() {
1611        let stream = Stream::new(1);
1612        let d0 = stream
1613            .accept_in_order(0, vec![Bytes::from_static(b"0")])
1614            .await;
1615        assert_eq!(d0, vec![Bytes::from_static(b"0")]);
1616        let d2 = stream
1617            .accept_in_order(2, vec![Bytes::from_static(b"2")])
1618            .await;
1619        assert!(
1620            d2.is_empty(),
1621            "seq 2 is a future hole — buffered, not delivered"
1622        );
1623        let d1 = stream
1624            .accept_in_order(1, vec![Bytes::from_static(b"1")])
1625            .await;
1626        assert_eq!(
1627            d1,
1628            vec![Bytes::from_static(b"1"), Bytes::from_static(b"2")],
1629            "filling the gap at 1 must release 1 then the buffered 2, in order"
1630        );
1631    }
1632
1633    /// `accept_in_order` drops duplicates of already-delivered sequences.
1634    #[tokio::test]
1635    async fn accept_in_order_drops_duplicates() {
1636        let stream = Stream::new(1);
1637        let _ = stream
1638            .accept_in_order(0, vec![Bytes::from_static(b"0")])
1639            .await;
1640        let _ = stream
1641            .accept_in_order(1, vec![Bytes::from_static(b"1")])
1642            .await;
1643        let dup = stream
1644            .accept_in_order(0, vec![Bytes::from_static(b"0")])
1645            .await;
1646        assert!(
1647            dup.is_empty(),
1648            "a duplicate of delivered data must release nothing"
1649        );
1650    }
1651
1652    /// A COALESCED bundle's multiple sub-payloads occupy ONE cursor position and
1653    /// are delivered together, in order, ahead of the next sequence.
1654    #[tokio::test]
1655    async fn accept_in_order_delivers_coalesced_bundle_as_one_cursor_position() {
1656        let stream = Stream::new(1);
1657        let bundle = vec![
1658            Bytes::from_static(b"A"),
1659            Bytes::from_static(b"B"),
1660            Bytes::from_static(b"C"),
1661        ];
1662        let d0 = stream.accept_in_order(0, bundle).await;
1663        assert_eq!(
1664            d0,
1665            vec![
1666                Bytes::from_static(b"A"),
1667                Bytes::from_static(b"B"),
1668                Bytes::from_static(b"C")
1669            ]
1670        );
1671        // The bundle consumed exactly one sequence; the next reliable frame is 1.
1672        let d1 = stream
1673            .accept_in_order(1, vec![Bytes::from_static(b"D")])
1674            .await;
1675        assert_eq!(d1, vec![Bytes::from_static(b"D")]);
1676    }
1677
1678    // ── Flow control (Phase 4.3) ──
1679
1680    #[test]
1681    fn peer_send_window_starts_at_initial() {
1682        let s = Stream::new(1);
1683        assert_eq!(s.peer_send_window(), INITIAL_STREAM_WINDOW);
1684    }
1685
1686    #[test]
1687    fn try_consume_send_window_decrements_atomically() {
1688        let s = Stream::new(1);
1689        assert!(s.try_consume_send_window(1000));
1690        assert_eq!(s.peer_send_window(), INITIAL_STREAM_WINDOW - 1000);
1691        assert!(s.try_consume_send_window(INITIAL_STREAM_WINDOW - 1000));
1692        assert_eq!(s.peer_send_window(), 0);
1693        // Further consumption fails until refilled.
1694        assert!(!s.try_consume_send_window(1));
1695    }
1696
1697    #[test]
1698    fn apply_peer_window_update_adds_relative_credit() {
1699        let s = Stream::new(1);
1700        // Drain to 100 bytes.
1701        assert!(s.try_consume_send_window(INITIAL_STREAM_WINDOW - 100));
1702        assert_eq!(s.peer_send_window(), 100);
1703
1704        // A WINDOW_UPDATE is a relative credit: it ADDS to the window.
1705        s.apply_peer_window_update(1000);
1706        assert_eq!(s.peer_send_window(), 1100);
1707        s.apply_peer_window_update(50);
1708        assert_eq!(s.peer_send_window(), 1150);
1709
1710        // Saturates at the hard cap (misbehaving-peer guard).
1711        s.apply_peer_window_update(u32::MAX);
1712        assert_eq!(s.peer_send_window(), MAX_SEND_WINDOW);
1713    }
1714
1715    #[test]
1716    fn record_app_consumed_grants_relative_credit_after_threshold() {
1717        let s = Stream::new(1);
1718        let threshold = INITIAL_STREAM_WINDOW / 2;
1719
1720        // Small drains return None.
1721        assert!(s.record_app_consumed(100).is_none());
1722        assert!(s.record_app_consumed(200).is_none());
1723
1724        // Drain across the half-window threshold → emit a credit equal to the
1725        // accumulated consumption (300 + threshold), NOT an absolute window.
1726        let credit = s.record_app_consumed(threshold);
1727        assert_eq!(
1728            credit,
1729            Some(300 + threshold),
1730            "WINDOW_UPDATE carries the relative credit (bytes consumed since last update)"
1731        );
1732
1733        // Counter resets after emitting — small further drains do not re-emit.
1734        assert!(s.record_app_consumed(10).is_none());
1735    }
1736
1737    #[test]
1738    fn relative_credit_round_trip_bounds_outstanding_to_one_window() {
1739        // Model: receiver grants credit == consumed; sender's window =
1740        // initial + Σcredit − Σsent, so outstanding (sent − consumed) ≤ initial.
1741        let sender = Stream::new(1);
1742        let receiver = Stream::new(1);
1743        let threshold = INITIAL_STREAM_WINDOW / 2;
1744
1745        // Sender fills the initial window exactly.
1746        assert!(sender.try_consume_send_window(INITIAL_STREAM_WINDOW));
1747        assert_eq!(sender.peer_send_window(), 0, "initial window exhausted");
1748
1749        // Receiver consumes one threshold's worth → grants that much credit.
1750        let credit = receiver
1751            .record_app_consumed(threshold)
1752            .expect("threshold crossed");
1753        sender.apply_peer_window_update(credit);
1754        assert_eq!(
1755            sender.peer_send_window(),
1756            threshold,
1757            "sender may now send exactly the bytes the receiver consumed"
1758        );
1759    }
1760
1761    #[test]
1762    fn staged_window_update_credit_accumulates_until_taken() {
1763        let s = Stream::new(1);
1764        assert_eq!(s.take_pending_window_update(), None);
1765
1766        // Two grants staged before a single flush must SUM, not overwrite: the
1767        // send loop (sole emitter) may run arbitrarily late after a credit is
1768        // staged, so back-to-back grants would otherwise lose all but the last
1769        // — a permanent credit leak that shrinks the peer's window over time.
1770        s.stage_window_update_credit(1000);
1771        s.stage_window_update_credit(2500);
1772        assert_eq!(s.take_pending_window_update(), Some(3500));
1773
1774        // The slot resets to empty once taken.
1775        assert_eq!(s.take_pending_window_update(), None);
1776
1777        // Accumulation saturates instead of wrapping past u32::MAX.
1778        s.stage_window_update_credit(u32::MAX);
1779        s.stage_window_update_credit(10);
1780        assert_eq!(s.take_pending_window_update(), Some(u32::MAX));
1781
1782        // Zero credit is a no-op (no spurious WINDOW_UPDATE).
1783        s.stage_window_update_credit(0);
1784        assert_eq!(s.take_pending_window_update(), None);
1785    }
1786}