From 1833df58d46b1d7f6e8cdc15d2f05ed373cd3ab2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 14:46:20 +0200 Subject: [PATCH] cultnet-rs: a direct send measures the window after reclaiming expired sends `send` checked `window_admits` before `send_many` purged deadline-passed packets, so the first direct send after the lowest unacked packet expired was refused for acknowledged bytes that `send_many` would already have released. `send` now purges first, as `send_many` does. Co-Authored-By: Claude Opus 5.5 --- packages/cultnet-rs/src/rudp.rs | 5 +++ packages/cultnet-rs/tests/rudp_flow_window.rs | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/cultnet-rs/src/rudp.rs b/packages/cultnet-rs/src/rudp.rs index bf611a15..9e8c4a1c 100644 --- a/packages/cultnet-rs/src/rudp.rs +++ b/packages/cultnet-rs/src/rudp.rs @@ -574,6 +574,11 @@ impl CultNetRudpSession { payload: Vec, options: CultNetRudpSendOptions, ) -> Result { + // Measure the window as `send_many` will: without the deadline-passed + // sends it is about to reclaim. + if options.reliable { + self.purge_expired_reliable(options.now_ms); + } if options.reliable && (!self.queued_reliable.is_empty() || !self.window_admits(self.next_sequence, payload.len())) diff --git a/packages/cultnet-rs/tests/rudp_flow_window.rs b/packages/cultnet-rs/tests/rudp_flow_window.rs index 85c7a864..58065970 100644 --- a/packages/cultnet-rs/tests/rudp_flow_window.rs +++ b/packages/cultnet-rs/tests/rudp_flow_window.rs @@ -249,3 +249,34 @@ fn acknowledged_bytes_stop_counting_when_the_lowest_unacked_packet_expires() -> } Ok(()) } + +#[test] +fn a_direct_send_is_not_refused_for_bytes_whose_lowest_packet_has_expired() -> Result<()> { + let mut sender = connected(1)?; + let expiring = CultNetRudpSendOptions { + reliable: true, + now_ms: 0, + reliable_expire_after_ms: Some(100), + ..Default::default() + }; + let g = sender + .send_many("state", vec![7], expiring, None)? + .remove(0); + for offset in 1..=4u32 { + assert_eq!(send(&mut sender, MIB)?.len(), 1); + sender.receive(&ack_for(g.sequence + offset), 1)?; + } + assert!(sender.send("state", vec![7; MIB], reliable()).is_err()); + + // The first call after g's deadline is a direct send: it measures the + // window without g, as `send_many` would, and goes on the wire. + let at_200 = CultNetRudpSendOptions { + reliable: true, + now_ms: 200, + ..Default::default() + }; + let sent = sender.send("state", vec![7; MIB], at_200)?; + assert_eq!(sent.sequence, g.sequence + 5); + assert_eq!(sender.outstanding_reliable_packet_count(), 1); + Ok(()) +}