Files
alknet/docs/architecture/crates/channels/channel-client.md
T
glm-5.2 2313c51f12 docs(arch): add alknet-channels specs — ADRs 071-080, 7 spec docs, OQ-56/57
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).
2026-07-12 12:57:18 +00:00

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)): AlknetClient core extraction — blocked on a second transport's client. ChannelClient does not unblock it.

References

  • ADR-080: ChannelClient (the decision)
  • ADR-073: channel lifecycle operations (open_channel sends channel/open)
  • ADR-074: ChannelBidiStreamSource (what Channel.source wraps)
  • ADR-075: ChannelManager (the shared state ChannelClient holds)
  • OQ-55: AlknetClient / client establishment extraction
  • docs/architecture/crates/call/client-and-adapters.md — CallClient (the shape ChannelClient mirrors)