feat(quic,nestsclient): plumb RESET_STREAM + STOP_SENDING through WebTransport

Extends the WebTransport interface and its three implementations
(QUIC-backed, in-memory fake, peer-stripped demux) with typed
error-code cancel paths so moq-lite Lite-03's M2/M3 audit items can
land.

QUIC layer (`:quic`):
  - `StrippedWtStream` gains optional `reset` and `stopSending`
    closures, wired by `WtPeerStreamDemux.emitStripped` through
    `QuicStream.resetStream(errorCode)` /
    `QuicStream.stopSending(errorCode)` + `driver.wakeup()` so the
    frames actually leave the connection. `reset` is null on
    peer-initiated uni streams (no send half on our side);
    `stopSending` is unconditional (every surfaced stream has a
    read side).

WebTransport interface (`:nestsClient`):
  - `WebTransportReadStream.stopSending(errorCode: Long)` —
    suspending; first-call-wins.
  - `WebTransportWriteStream.reset(errorCode: Long)` —
    suspending; first-call-wins. Distinct from the existing
    graceful `finish()` / FIN.

Adapters:
  - `QuicBidiStreamAdapter`, `QuicReadStreamAdapter`,
    `QuicUniWriteStreamAdapter` route directly to the underlying
    `QuicStream` API.
  - `StrippedWtReadStreamAdapter` /
    `StrippedWtBidiStreamAdapter` route through the demux's
    closures; defensive `?:` fallbacks defend against a future
    demux bug that forgets to wire a closure.
  - `FakeBidiStream`, `FakeReadStream`, `ChannelWriteStream` (the
    in-memory test transport) record reset / stopSending codes
    in shared `AtomicLong` cells so tests can assert "the peer
    reset with code X" / "the listener stopSending with code Y"
    via new `lastResetCode` / `lastStopSendingCode` /
    `lastPeerResetCode` / `lastPeerStopSendingCode` properties.
    Sentinel `NO_CODE = Long.MIN_VALUE` distinguishes "not called"
    from a legitimate code 0.

Pure plumbing — no moq-lite call sites use the new methods yet.
Subsequent commits wire M3 (publisher Drop reply uses
reset(errorCode)) and M2 (listener group-cancel uses
stopSending(errorCode)).

Audit doc: nestsClient/plans/2026-05-09-moq-lite-rfc-compliance.md (M2 + M3).

https://claude.ai/code/session_012TGfo99Ugz7fcCPv85a8Tq
This commit is contained in:
Claude
2026-05-09 14:31:48 +00:00
parent 20a3e9006d
commit 1c7b158e11
4 changed files with 303 additions and 11 deletions
@@ -44,6 +44,14 @@ import kotlinx.coroutines.launch
* is false), [send] and [finish] are wired to the stream's outbound
* half so the application can write its response. They are null on
* unidirectional streams.
*
* For both directions, [reset] (RFC 9000 §3.5 RESET_STREAM) and
* [stopSending] (RFC 9000 §3.5 STOP_SENDING) are exposed so application
* code (e.g. moq-lite Lite-03's typed cancel paths) can convey error
* codes instead of relying on graceful FIN. [reset] is null on uni
* streams whose write side belongs to the peer; [stopSending] is null
* when there's no read side to ask the peer to stop on (i.e. never
* — every stream we surface here has a read side).
*/
class StrippedWtStream(
val streamId: Long,
@@ -58,6 +66,22 @@ class StrippedWtStream(
* Half-close the send side (FIN). Null for unidirectional streams.
*/
val finish: (suspend () -> Unit)? = null,
/**
* Send `RESET_STREAM(applicationErrorCode)` on the send half (RFC
* 9000 §3.5). Null when the application doesn't own the send side
* (peer-initiated uni stream). The first call wins per
* `QuicStream.resetStream`'s lock-free first-call-wins gate;
* duplicate calls with different codes are silently ignored to
* keep the wire frame stable.
*/
val reset: (suspend (errorCode: Long) -> Unit)? = null,
/**
* Send `STOP_SENDING(applicationErrorCode)` on the receive half
* (RFC 9000 §3.5) — asks the peer to RESET its corresponding
* send side. Always non-null on streams surfaced here: every
* stripped stream has a read side. First call wins.
*/
val stopSending: (suspend (errorCode: Long) -> Unit)? = null,
)
/**
@@ -515,6 +539,26 @@ class WtPeerStreamDemux(
driver?.wakeup()
}
}
// RESET_STREAM is meaningful only on streams whose write side
// belongs to us. Same null pattern as [send] / [finish] for
// peer-initiated uni streams.
val reset: (suspend (Long) -> Unit)? =
if (isUni) {
null
} else {
{ errorCode ->
stream.resetStream(errorCode)
driver?.wakeup()
}
}
// STOP_SENDING is meaningful on the receive half — every
// stripped stream we surface here has one (uni: peer is
// sender; bidi: peer's send side is our receive side), so
// wire unconditionally.
val stopSending: (suspend (Long) -> Unit) = { errorCode ->
stream.stopSending(errorCode)
driver?.wakeup()
}
// Suspending send instead of `trySend` so a slow application
// back-pressures the demux instead of silently dropping the
// stream. With a bounded buffer ([readyStreamsBuffer]),
@@ -529,6 +573,8 @@ class WtPeerStreamDemux(
data = data,
send = send,
finish = finish,
reset = reset,
stopSending = stopSending,
),
)
}