Files
alknet/docs/architecture/crates/channels/channel-client.md
T
glm-5.2 762d9c7bd2 docs(architecture): remove removed-thing remnants from spec docs
Spec docs should describe WHAT IS, not WHAT WAS. ADRs and OQ files are
historical records by design and are left alone; the ADR-index Status
column records ADR status (stable fact). What changed in the specs:

- API code blocks no longer list removed methods. ChannelClient::connect_quic
  (channel-client.md) and CallClient::connect (client-and-adapters.md) are
  gone from the impl blocks; surrounding prose describes the current
  from_connection / spawn_dispatch primary + AlknetClient dial shape.
- Amendment (ADR-093): stream_types field is removed blockquotes dropped
  from channel-client.md and channel-operations.md (the code already
  reflects the current state).
- Historical is-removed / reversed-by / amended-by prose rewritten to
  current state across channels crate, call crate, hub, tls, core,
  endpoint, client READMEs, and the top-level README/overview.
- Dropped the EndpointError — removed subsection from endpoint/README.md
  (the type doesn't exist anymore, so it shouldn't have a subsection).
- Replaced stale connect() references in flow descriptions with the dial
  (in AlknetClient) since connect() is no longer a method.
- Removed strikethrough ADR-028 / from_jsonschema-clause rows from
  client-and-adapters.md and operation-registry.md ADR tables.
- Rewrote the ADR-066 update blockquote in operation-registry.md to
  describe FromJsonSchema's current shape.

19 files modified, net -116 lines. No ADRs or OQ files touched.
2026-07-19 04:07:49 +00:00

8.2 KiB

status, last_updated
status last_updated
draft 2026-07-18

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 takes over an established transport 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 {
    /// Construct a `ChannelClient` over a pre-established transport
    /// `Connection` on ALPN `alknet/channels`. This is the
    /// transport-agnostic primary constructor: the caller (or a
    /// transport-specific dial helper) produces the `Connection` —
    /// via `Connection::from_bidi` (TCP+TLS, WebTransport, SSH
    /// `direct-tcpip`), a quinn connection, or any other `AsyncRead +
    /// AsyncWrite` source — and this method takes over: installs
    /// channel 0 (`alknet/call`), spawns the demux/mux, and returns
    /// the client. Mirrors the server side's transport-agnostic
    /// `ChannelsAdapter::handle(Connection)` and
    /// `CallClient::spawn_dispatch(Connection)`.
    ///
    /// This is the one-way-door API surface (ADR-080). It must not be
    /// coupled to a transport — the channels protocol is
    /// transport-agnostic (ADR-071, as amended by ADR-093; ADR-065,
    /// ADR-092), and the client side is half of that protocol.
    pub async fn from_connection(connection: Connection)
        -> 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,
        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,
    /// The channel's BiStream, accessible via the BidiStreamSource
    /// (accept_bi — ADR-074 as amended by ADR-093).
    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)
}

Transport-agnostic by construction

ChannelClient is the client side of the channels protocol. The channels protocol is transport-agnostic (ADR-071 substrate modes, as amended by ADR-093; Connection::from_bidi/from_source from ADR-065/070/092 take any AsyncRead + AsyncWrite). The client side must not be welded to a transport — that would repeat the server-side welding ADR-065 explicitly unwound.

from_connection(connection: Connection) is the primary constructor and the one-way-door API surface. It takes a pre-established Connection and takes over channels establishment. The transport is the caller's concern: Connection::from_bidi(tls_stream, ...) for TCP+TLS, a quinn Connection, a WebTransport BiStream, an SSH direct-tcpip channel wrapped via from_bidi, a WebSocket carrying alknet/channels (the browser path per ADR-044) — all produce a Connection that from_connection accepts unchanged. This mirrors the server side's ChannelsAdapter::handle(Connection), which is substrate-agnostic by the same mechanism.

The dial (QUIC, TCP+TLS, iroh) lives in AlknetClient (alknet-client, ADR-089), not on ChannelClient. Callers compose AlknetClient::dial_quic(...).await? + ChannelClient::from_connection(conn).await? — two lines, the dial then the take-over. Keeping the dial off ChannelClient avoids alknet-channels-call depending on alknet-client; the protocol crates are parallel to the dial, not downstream of it.

The credential/verifier-selection rule (ADR-034) lives in the dial (AlknetClient), not in from_connection — from_connection receives an already-established, already-authenticated Connection, exactly as ChannelsAdapter::handle does on the server side.

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

ChannelClient's API is transport-agnostic — from_connection takes a pre-established Connection. The shared dial+TLS seam (AlknetClient, OQ-55) is alknet-client, which provides AlknetClient with three dial methods (dial_quic / dial_tcp_tls / dial_iroh), each producing a Connection that from_connection consumes. The dial is transport-specific (QUIC, TCP+TLS, iroh); the take-over (from_connection) is transport-agnostic. The two concerns are separated.

AlknetClient::dial_quic is the dial that feeds from_connection. A caller that needs transport selection (QUIC with TCP+TLS fallback) uses AlknetClient directly; the fallback policy is a caller concern. See ADR-089 for the full decision and OQ-55 (resolved).

Design Decisions

All design decisions are documented as ADRs in decisions/.

ADR Decision Summary
080 ChannelClient Client side; transport-agnostic from_connection primary; dial lives in AlknetClient (ADR-089, resolves OQ-55)
093 channels Pure Channel Multiplexing No stream_types on open_channel/Channel; handler owns sub-stream multiplexing

Open Questions

  • OQ-55 (resolved by ADR-089): AlknetClient core dial+TLS seam — alknet-client with three dial methods. ChannelClient's API is transport-agnostic (from_connection); the dial is the shared seam. See ADR-089.

References

  • ADR-080: ChannelClient (the decision)
  • ADR-093: channels pure channel multiplexing (no stream_types)
  • ADR-073: channel lifecycle operations (open_channel sends channel/open)
  • ADR-074: ChannelBidiStreamSource (what Channel.source wraps, as amended by ADR-093 — accept_bi yields a BiStream)
  • 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)