feat(core): prefix-filtered AcceptChannelsAsync overload#97
Merged
Conversation
Replace AcceptChannelsAsync(CancellationToken) with a single unified signature: AcceptChannelsAsync(string? channelIdPrefix = null, CancellationToken ct = default). When channelIdPrefix is null (default), behaves as before: yields every inbound channel not claimed by a specific AcceptChannel call. When channelIdPrefix is supplied, only channels whose ID starts with it are yielded; matched channels are routed exclusively to that enumeration and are NOT yielded by the unfiltered (null prefix) overload. This lets an overlay protocol (e.g. mesh routing) share a multiplexer with the host application by subscribing to a reserved prefix. ChannelRegistry tracks prefix subscriptions and routes inbound channels in EnqueueForAccept by: exact-id pending accept -> prefix subscription -> default queue. Breaking change: all callsites that passed CancellationToken positionally must now use named argument syntax (ct: token). Per project policy (unreleased), backward compatibility is not maintained.
Three architectural fixes on the prefix-routing path introduced by this PR: C3 - Subscription cleanup on cancellation. Previously a cancelled AcceptChannelsAsync(prefix, ct) left its PrefixSubscription in the dispatch list forever; matching channels were silently queued into an abandoned, reader-less channel (memory leak + silent host-side channel drop). Now the iterator wraps enumeration in try/finally that removes the subscription from the dispatch list, completes its queue writer, and re-routes any still-buffered channels back into the unfiltered _acceptQueue so the host application can observe them. C4 - Prefix overlap rejection. Previously two subscriptions whose prefixes were a prefix of one another had implicit, undocumented first-registration-wins routing. AcceptChannelsAsync now throws MultiplexerException(ChannelExists) at registration time when a candidate prefix is a prefix of, or has as a prefix, any existing subscription (covers equality and both directions of containment). Policy is documented in IStreamMultiplexer xmldoc. C5 - Stale TCS cleanup in ChannelRegistry.AcceptChannelAsync. Defensive: the ct.Register callback now removes the entry from _pendingAccepts before cancelling the TCS, preventing a future EnqueueForAccept from routing into a permanently-cancelled TCS. Regression tests in tests/NetConduit.UnitTests/PrefixSubscriptionTests.cs: - PrefixSubscription_RoutesMatchingChannels_AwayFromDefaultStream - PrefixSubscription_DuplicatePrefix_ThrowsChannelExists - PrefixSubscription_OverlappingPrefix_ThrowsChannelExists - PrefixSubscription_CancellationFreesPrefix_AllowsResubscribe - PrefixSubscription_CancellationReRoutesBufferedChannels_ToDefaultStream Full NetConduit.UnitTests suite: 355 passed / 0 failed / 1 pre-existing skip.
…s synchronous Closes Residual #2 from the architect review: the overlap check runs when the method is invoked, not lazily on first MoveNextAsync. Callers must wrap the method call itself (or the GetAsyncEnumerator call) in try/catch — not the await foreach body — to observe ChannelExists. Documentation-only change; no behavior impact.
Kiryuumaru
added a commit
that referenced
this pull request
May 20, 2026
The previous summary claimed StreamMultiplexer was 'a pure router. Channels do all heavy lifting.' That description was inaccurate when written and is now provably wrong on master: GoAway drain orchestration (PR #124), channel-id validation (PR #120), shutdown gating (PR #124), and prefix-routing dispatch (PR #97) all live in or directly around this class. Replace the summary with an honest description of the class's actual responsibilities (connection lifecycle, handshake, reader/writer/flusher/keepalive loops, control frames, accept dispatch) and clarify which concerns DO live on the channels (slabs, frame construction, flow control, replay state). Doc-only. No behavior change. No public API change. Addresses architect critique arch-002 Option D from the initial-architecture-audit session (the re-evaluation found Option D had never been adopted despite the class accumulating more responsibility).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Unifies
IStreamMultiplexer.AcceptChannelsAsyncto a single signature with an optionalchannelIdPrefix:channelIdPrefix == null(default) — yields every inbound channel not claimed by a specificAcceptChannelcall. Same behavior as before.channelIdPrefix != null— yields only channels whose ID starts with the prefix. Matched channels are routed exclusively to that enumeration and are not yielded by the unfiltered overload.Why
Overlay protocols (e.g. mesh routing) and host applications need to share a multiplexer without stealing each other's channels. The previous
accept-and-discardpattern stole host-app channels whenever the overlay was iterating. This routes them at the registry layer instead.Implementation
ChannelRegistrytracks prefix subscriptions and dispatches each inbound channel inEnqueueForAcceptby:AcceptChannel(id))AcceptChannelsAsync())Breaking Change
Per project policy (unreleased, no compatibility constraint), callsites that passed
CancellationTokenpositionally must now use named-argument syntax:All in-repo callsites (5 samples + 11 test files) updated.
Verification
dotnet build NetConduit.slnx— 0 warnings, 0 errors across net8.0/net9.0/net10.0NetConduit.UnitTests— 333 passed / 1 skipped (pre-existing) / 0 failedNetConduit.Transport.Tcp.IntegrationTests— 3/3 passedAcceptChanneltests — 12/12 passed