retransmit method

int? retransmit(
  1. UDXPacket packet
)

Re-sends a still-unacked packet under a FRESH sequence number, returning the new sequence (or null when nothing should be sent).

Reusing the original number is what made loss fatal: the retry budget rode on the packet, so exhausting it left a hole the stream could never fill and the packet was silently dropped, stalling the stream. QUIC never reuses a packet number (RFC 9000 section 12.3); it re-frames lost data into a new packet, bounded by the connection's idle timeout rather than a per-packet count. Both v3 stacks reassemble streams on byte offsets, so a fresh number is safe here — a duplicate is discarded by its offset, and the retransmission is acknowledged unambiguously (no Karn ambiguity, because sentTime is reset to the resend).

The re-key moves the tracking entry and the retransmit timer from the old number to the new. It leaves congestion accounting alone: the bytes stay in flight across the re-key (onRetransmit never calls onPacketSent), so they are counted once, from the original send until the packet is acknowledged.

Returns null when the packet was already acknowledged, or was retransmitted within the last RTO — collapsing a near-simultaneous RTO timer and fast-retransmit into one send.

Implementation

int? retransmit(UDXPacket packet) {
  final oldSeq = packet.sequence;
  if (!_sentPackets.containsKey(oldSeq)) {
    _retransmitTimers[oldSeq]?.cancel();
    _retransmitTimers.remove(oldSeq);
    return null;
  }

  final now = DateTime.now();
  if (packet.retransmitCount > 0 &&
      packet.lastRetransmit != null &&
      now.difference(packet.lastRetransmit!).inMilliseconds < retransmitTimeout) {
    return null;
  }

  final newSeq = nextSequence;

  _retransmitTimers[oldSeq]?.cancel();
  _retransmitTimers.remove(oldSeq);
  _sentPackets.remove(oldSeq);

  packet.sequence = newSeq;
  packet.retransmitCount++;
  packet.lastRetransmit = now;
  // Deliberately NOT touching sentTime. It is the ORIGINAL send time, and the
  // only reader is persistent-congestion detection, which measures how long a
  // packet has gone undelivered — that clock must not restart on a resend. RTT
  // is sampled from a separate per-sequence map in the socket, and a
  // retransmit's fresh sequence is simply absent from it, so a retransmitted
  // packet yields no RTT sample (Karn's algorithm) without any special-casing.
  lastSentPacketNumber = newSeq;
  _sentPackets[newSeq] = packet;

  final backoff = _retransmitBackoffMs(packet.retransmitCount);
  _retransmitTimers[newSeq] = Timer(
    Duration(milliseconds: backoff),
    () => _onRetransmitTimer(packet),
  );

  return newSeq;
}