Files
amethyst/quic
Claude 29282634e5 feat(quic): step 4 of RFC 9002 retransmit — pending* fields + writer drain
Adds the writer's drain side of the retransmit path. The QuicConnection
gains four `pending*` fields:

  - pendingMaxStreamsUni: Long?
  - pendingMaxStreamsBidi: Long?
  - pendingMaxData: Long?
  - pendingMaxStreamData: MutableMap<Long, Long>  (keyed by stream id)

Each non-null entry signals "the last extension we sent at this value
was lost; re-emit it". appendFlowControlUpdates now drains all four
ahead of the rolling-extension threshold check, emitting a fresh
frame + RecoveryToken for each pending entry and clearing it.

Step 4 only wires the consumer side; the setter side (loss
dispatcher) is step 6. Tests populate `pending*` directly to exercise
the drain in isolation.

Tests added (8, all pass):
  - pendingMaxStreamsUni / Bidi / MaxData / MaxStreamData each
    individually drain to a SentPacket carrying the matching token
  - multiplePending: all four pending types drain into one packet
    (writer drains them sequentially, no fan-out)
  - noPending: drain produces no extension tokens
  - pendingDrainBeforeThresholdCheck_supersedeOrderObservable:
    writer drains the pending value as-is — supersede check is the
    setter's responsibility (step 6), not the drain's
  - pendingClearedAcrossDrains: a cleared pending stays cleared on
    subsequent drains

Mirrors neqo's `fc.rs` retransmit tests in the plan
(`need_max_allowed_frame_after_loss`, `lost_after_increase`,
`multiple_retries_after_frame_pending_is_set`,
`new_retired_before_loss`). The supersede-check tests
(`no_max_allowed_frame_after_old_loss`,
`set_max_active_equal_does_not_set_frame_pending`) belong to step 6
and land there.

https://claude.ai/code/session_01PYYez8a6sjiakyjAxsfCEQ
2026-05-04 22:48:40 +00:00
..