Phase 1 architecture for alknet-channels (multiplexing proxy on alknet/channels). Grounded in the completed de-risk POC (28 tests) and the landed ADR-070 (BidiStreamSource trait + Connection::from_source). ADRs: - 071: 9-byte chunk wire format (generalizes TTY's 5-byte) - 072: channel 0 pre-negotiated as alknet/call (no special control plane) - 073: channel lifecycle operations on the call protocol — channel/open, close, control, resources/subscribe; direction field pinned; subscribe from day one (not poll-for-v1 — StreamingHandler machinery exists) - 074: ChannelBidiStreamSource implements BidiStreamSource (ADR-070); into_sub_streams() typed accessor for TTY; accept_bi() generic path - 075: ChannelsAdapter + ChannelManager; REQ-CH-01..04 wire invariants - 076: bounded-buffer backpressure (1 MiB), 256-channel cap, monotonic IDs - 077: TTY inside channels uses sub-streams, not own wire format; amends ADR-052 scope to direct-connect TTY; channels feature on tty - 078: two-pump shutdown-on-completion contract (handler-level) - 079: hub relay translates channel 0, byte-forwards data channels - 080: ChannelClient (QUIC-only); AlknetClient core extraction deferred (OQ-55) Spec docs: overview, channels-wire, channels-connection, channels-adapter, channel-operations, channel-client. OQ-56 (full windowing) and OQ-57 (two-pump helper extraction) are genuine deferred(scope) deferrals with concrete blocking conditions; the contracts are decided, only the extensions are deferred. Hedging audit converted three research hedges into decisions: resources/subscribe (not poll), server-assigned IDs (not if-zero-RTT), bounded-buffer (not if-HOL-becomes-a-problem).
5.6 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-07-12 |
channel-client.md — ChannelClient
The client side of a channels connection. ADR-080 is the decision; this doc specifies the API.
What
ChannelClient is the symmetric counterpart to ChannelsAdapter (ADR-075).
The server side is a ProtocolHandler (ChannelsAdapter::handle); the
client side dials a transport, establishes the channels connection, runs the
demux/mux, and exposes open_channel(alpn, params) -> Channel to the
application.
This is the channels analogue of CallClient (server: CallAdapter;
client: CallClient) in the call protocol.
API
pub struct ChannelClient {
manager: ChannelManager,
// The transport-side demux/mux, running in a background task.
...
}
impl ChannelClient {
/// Open a channels connection to a peer. Dials the transport (QUIC
/// initially), establishes the channels connection, preinstalls
/// channel 0 (alknet/call), and returns the client.
pub async fn connect(addr: SocketAddr, credentials: CallCredentials)
-> Result<Self, ChannelError>;
/// Open a data channel with the given ALPN and params. Sends
/// `channel/open` on channel 0, waits for the response, and returns
/// the channel.
pub async fn open_channel(
&self,
alpn: &str,
stream_types: &[u8],
params: Value,
direction: ChannelDirection,
) -> Result<Channel, ChannelError>;
/// Subscribe to the peer's resource updates. Returns a stream of
/// resource-set events (ADR-073 channel/resources/subscribe). Each
/// event carries the JSON `output.resources` array from ADR-073's
/// `channel/resources/subscribe` response shape.
pub async fn subscribe_resources(&self)
-> Result<BoxStream<ResourceEvent>, ChannelError>;
/// The call-protocol connection on channel 0, for invoking channel
/// lifecycle operations and any other call ops the peer exposes.
pub fn call(&self) -> &CallConnection;
}
pub enum ChannelDirection {
InitiatorToResponder,
ResponderToInitiator,
}
pub struct Channel {
pub channel_id: u32,
pub stream_types: Vec<u8>,
/// The sub-streams, accessible via accept_bi() (ADR-074 generic path)
/// or into_sub_streams() (ADR-074 typed path).
pub source: ChannelBidiStreamSource,
}
/// One event from `channel/resources/subscribe`. Wraps the JSON `output`
/// object from ADR-073's subscribe response — the `resources` array
/// describing what ALPNs the peer exposes and with what `access` preview.
/// The channels crate maps the JSON to this typed struct; the fields mirror
/// ADR-073's response shape.
pub struct ResourceEvent {
pub resources: Vec<ResourceEntry>,
}
pub struct ResourceEntry {
pub alpn: String,
pub backends_or_targets: Vec<String>, // ALPN-specific enumeration
pub access: Value, // preview of AccessControl (advisory)
}
QUIC-only initially
ChannelClient::connect dials a QUIC connection (via the same quinn
endpoint CallClient uses) and wraps it as a channels connection. This is
the same transport shape as CallClient. When a second transport's client
exists (HTTP, TCP+TLS, WebTransport — per OQ-55), the dial can be
generalized. Until then, ChannelClient is QUIC-only — the same posture as
CallClient.
Bidirectionality preserved
The channels protocol is bidirectional — either side can open a channel
(ADR-073 §direction semantics). ChannelClient::open_channel supports both
ChannelDirection::InitiatorToResponder and
ChannelDirection::ResponderToInitiator. The client is not "the client
side" in the request/response sense — it can also receive channel/open
requests from the peer (the peer initiates, the client's ChannelManager
responds). This mirrors the call protocol's operation overlay (each side
populates what operations they expose).
ChannelClient is one endpoint of a bidirectional channels connection. The
name follows the CallClient convention (the side that dialed), not a
request/response role.
Relationship to AlknetClient (OQ-55 — deferred)
ChannelClient is a standalone client, not a specialization of a core
AlknetClient. The AlknetClient extraction (OQ-55) is genuinely deferred:
blocked on a second transport's client existing, not on a second client
existing. ChannelClient over QUIC is a second client but the same
transport shape as CallClient — it doesn't give enough information to
extract the transport-polymorphic dial seam.
When AlknetClient is eventually extracted (after a second transport's
client exists), ChannelClient and CallClient both refactor onto it.
Until then, they are independent clients with duplicated boilerplate (each
rebuilds verifier selection — ~20 lines). The friction is duplicated
boilerplate, not a missing capability.
Design Decisions
All design decisions are documented as ADRs in decisions/.
| ADR | Decision | Summary |
|---|---|---|
| 080 | ChannelClient | Client side; QUIC-only; AlknetClient deferred (OQ-55) |
Open Questions
- OQ-55 (deferred(scope)):
AlknetClientcore extraction — blocked on a second transport's client.ChannelClientdoes not unblock it.
References
- ADR-080: ChannelClient (the decision)
- ADR-073: channel lifecycle operations (
open_channelsendschannel/open) - ADR-074: ChannelBidiStreamSource (what
Channel.sourcewraps) - ADR-075: ChannelManager (the shared state
ChannelClientholds) - OQ-55: AlknetClient / client establishment extraction
docs/architecture/crates/call/client-and-adapters.md—CallClient(the shapeChannelClientmirrors)