Skip to content

feat(core): prefix-filtered AcceptChannelsAsync overload#97

Merged
Kiryuumaru merged 7 commits into
masterfrom
feat/accept-channels-prefix-overload
May 20, 2026
Merged

feat(core): prefix-filtered AcceptChannelsAsync overload#97
Kiryuumaru merged 7 commits into
masterfrom
feat/accept-channels-prefix-overload

Conversation

@Kiryuumaru
Copy link
Copy Markdown
Owner

Summary

Unifies IStreamMultiplexer.AcceptChannelsAsync to a single signature with an optional channelIdPrefix:

IAsyncEnumerable<IReadChannel> AcceptChannelsAsync(string? channelIdPrefix = null, CancellationToken ct = default);
  • channelIdPrefix == null (default) — yields every inbound channel not claimed by a specific AcceptChannel call. 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-discard pattern stole host-app channels whenever the overlay was iterating. This routes them at the registry layer instead.

Implementation

ChannelRegistry tracks prefix subscriptions and dispatches each inbound channel in EnqueueForAccept by:

  1. Exact-ID pending accept (AcceptChannel(id))
  2. Prefix subscription (first-match, ordinal)
  3. Default queue (unfiltered AcceptChannelsAsync())

Breaking Change

Per project policy (unreleased, no compatibility constraint), callsites that passed CancellationToken positionally must now use named-argument syntax:

// before
mux.AcceptChannelsAsync(ct)

// after
mux.AcceptChannelsAsync(ct: ct)

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.0
  • NetConduit.UnitTests — 333 passed / 1 skipped (pre-existing) / 0 failed
  • NetConduit.Transport.Tcp.IntegrationTests — 3/3 passed
  • Targeted AcceptChannel tests — 12/12 passed

Kiryuumaru and others added 4 commits May 19, 2026 15:49
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.
Kiryuumaru and others added 3 commits May 20, 2026 13:55
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 Kiryuumaru merged commit 85cd5e9 into master May 20, 2026
35 checks passed
@Kiryuumaru Kiryuumaru deleted the feat/accept-channels-prefix-overload branch May 20, 2026 07:16
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant