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:
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user