docs(arch): unweld ChannelClient from QUIC — from_connection primary, connect_quic convenience (ADR-080 amendment)
The channels protocol is transport-agnostic by design (ADR-071 substrate
modes; ADR-065 unwound the server-side QUIC-welding via
Connection::from_stream/from_bidi). ADR-080 had welded the client-side
one-way-door API to QUIC and masked it as a deferral ('QUIC-only
initially', 'can be generalized later') — anti-patterns #8/#9/#11
(door-type-as-deferral + resolved-with-escape-hatch). 'Can be
generalized later' meant 'can be rewritten later' — the expensive
reversal the one-way-door classification exists to prevent.
Fix: split the constructor surface.
- from_connection(connection: Connection) — transport-agnostic primary,
one-way door. Mirrors server-side ChannelsAdapter::handle(Connection)
and the existing CallClient::spawn_dispatch pattern.
- connect_quic(addr, credentials) — QUIC convenience, two-way door,
additive. Future connect_tcp_tls / connect_webtransport join it
without touching the one-way-door surface.
OQ-55 reframed: the deferred thing is the shared dial+TLS seam
(AlknetClient), not a QUIC-welded client API. The client take-over APIs
(CallClient::spawn_dispatch, ChannelClient::from_connection) are
transport-agnostic and decided; only the shared dial across transports
is blocked on a second transport's dial existing.
Files:
- decisions/080-channelclient.md: amendment section, Decision code
block, Transport-agnostic by construction (replaces QUIC-only
initially), Consequences, Door type, subscribe_resources added to
Decision block (was referenced by Door type but missing)
- crates/channels/channel-client.md: from_connection primary API,
Transport-agnostic by construction section, OQ-55 relationship
- crates/channels/overview.md, crates/channels/README.md,
crates/core/README.md, README.md, open-questions.md,
questions/055-...md: cross-reference summaries aligned
This commit is contained in:
1 parent
3006e29afc
commit
73c621bf21
8 files changed
+276
-103
No files matched your search
@@ -24,7 +24,7 @@ protocol work itself.
|
||||
| [channels-connection.md](channels-connection.md) | draft | `ChannelBidiStreamSource` (implements `BidiStreamSource` — ADR-070/074), `into_sub_streams()` typed destructure, recursive composition |
|
||||
| [channels-adapter.md](channels-adapter.md) | draft | `ChannelsAdapter` (`ProtocolHandler` on `alknet/channels`), `ChannelManager`, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) |
|
||||
| [channel-operations.md](channel-operations.md) | draft | `channel/open`, `channel/close`, `channel/control`, `channel/resources/subscribe` — call-protocol operations on channel 0, ACL flow, `direction` semantics, the hub relay contract (ADR-079) |
|
||||
| [channel-client.md](channel-client.md) | draft | `ChannelClient` — the client side of a channels connection, QUIC-only initially, bidirectionality preserved |
|
||||
| [channel-client.md](channel-client.md) | draft | `ChannelClient` — the client side of a channels connection; transport-agnostic `from_connection` primary, `connect_quic` convenience; bidirectionality preserved |
|
||||
|
||||
## Applicable ADRs
|
||||
|
||||
@@ -39,7 +39,7 @@ protocol work itself.
|
||||
| [077](../../decisions/077-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); 5 sub-streams; control bidirectional via 3/4; amends ADR-052 scope |
|
||||
| [078](../../decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer |
|
||||
| [079](../../decisions/079-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite |
|
||||
| [080](../../decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, QUIC-only; `AlknetClient` deferred (OQ-55) |
|
||||
| [080](../../decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, transport-agnostic `from_connection` primary + `connect_quic` convenience; `AlknetClient` dial-seam deferred (OQ-55) |
|
||||
| [081](../../decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers, not sub-crates |
|
||||
| [070](../../decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements |
|
||||
| [065](../../decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on |
|
||||
@@ -52,7 +52,7 @@ protocol work itself.
|
||||
|
||||
| OQ | Title | Status | Relevance |
|
||||
|----|-------|--------|-----------|
|
||||
| OQ-55 | AlknetClient / Client Establishment Extraction | deferred(scope) | `ChannelClient` is decided (ADR-080); `AlknetClient` core extraction stays deferred — blocked on a second *transport's* client, not a second client |
|
||||
| OQ-55 | AlknetClient / Client Establishment Extraction | deferred(scope) | `ChannelClient`'s API is decided (ADR-080): transport-agnostic `from_connection` primary, `connect_quic` convenience. `AlknetClient` core extraction stays deferred — blocked on a second *transport's* dial (the shared dial+TLS seam), not a second client |
|
||||
| OQ-56 | Full channel-level flow-control windowing | deferred(scope) | Bounded-buffer is decided (ADR-076); full windowing is an extension blocked on "a real deployment observes HOL blocking on a saturated channel where bounded buffer is insufficient" |
|
||||
| OQ-57 | Two-pump helper extraction to alknet-core | deferred(scope) | The shutdown-on-completion *contract* is decided (ADR-078); the *helper* extraction is blocked on a second two-pump handler existing (shape convergence) |
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ specifies the API.
|
||||
|
||||
`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
|
||||
client side takes over an established transport `Connection`, runs the
|
||||
demux/mux, and exposes `open_channel(alpn, params) -> Channel` to the
|
||||
application.
|
||||
|
||||
@@ -29,12 +29,37 @@ pub struct ChannelClient {
|
||||
}
|
||||
|
||||
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)
|
||||
/// 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_stream`/`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, ADR-065), and the client side is
|
||||
/// half of that protocol.
|
||||
pub async fn from_connection(connection: Connection)
|
||||
-> Result<Self, ChannelError>;
|
||||
|
||||
/// QUIC convenience constructor. Dials a QUIC connection to `addr`
|
||||
/// on ALPN `alknet/channels` (using `credentials` for the TLS
|
||||
/// handshake — ADR-034 verifier selection), then calls
|
||||
/// `from_connection`. This is the "I just want QUIC" one-liner;
|
||||
/// it is additive over `from_connection` and is a two-way door —
|
||||
/// `connect_tcp_tls`, `connect_webtransport`, etc. can be added
|
||||
/// alongside it without touching the one-way-door surface.
|
||||
pub async fn connect_quic(
|
||||
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.
|
||||
@@ -87,14 +112,37 @@ pub struct ResourceEntry {
|
||||
}
|
||||
```
|
||||
|
||||
## QUIC-only initially
|
||||
## Transport-agnostic by construction
|
||||
|
||||
`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`.
|
||||
`ChannelClient` is the client side of the channels protocol. The channels
|
||||
protocol is transport-agnostic (ADR-071 substrate modes;
|
||||
`Connection::from_stream`/`from_bidi`/`from_source` from ADR-065/070 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_stream`, 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.
|
||||
|
||||
`connect_quic(addr, credentials)` is a **convenience** constructor — dial
|
||||
QUIC, then `from_connection`. It is additive and two-way-door: transport-specific dial helpers (`connect_tcp_tls`, `connect_webtransport`, …)
|
||||
join it as transports are added, none of which touch the `from_connection`
|
||||
contract. The dial helper set is open-ended by design.
|
||||
|
||||
The credential/verifier-selection rule (ADR-034) lives in the transport's
|
||||
own dial path, not in `from_connection` — `from_connection` receives an
|
||||
already-established, already-authenticated `Connection`, exactly as
|
||||
`ChannelsAdapter::handle` does on the server side. The ~20 lines of
|
||||
verifier-selection boilerplate each dial helper rebuilds is the known
|
||||
duplicated cost of not having `AlknetClient` (OQ-55) extracted yet;
|
||||
`from_connection` keeps that boilerplate on the *transport-specific dial*
|
||||
side, not on the channels protocol's one-way-door surface.
|
||||
|
||||
## Bidirectionality preserved
|
||||
|
||||
@@ -113,18 +161,25 @@ 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.
|
||||
`ChannelClient`'s *API* is transport-agnostic — `from_connection` takes a
|
||||
pre-established `Connection`. What is deferred (OQ-55) is the shared
|
||||
*dial+TLS* seam (`AlknetClient`): the transport-specific work each dial
|
||||
helper does — open a socket, run the TLS handshake, apply ADR-034's
|
||||
verifier-selection rule, produce a `Connection`. That dial is genuinely
|
||||
transport-specific (QUIC, TCP+TLS, WebTransport, raw TCP, SSH), and we have
|
||||
one shape implemented (QUIC, in `connect_quic`). Extracting a QUIC-shaped
|
||||
connector now and naming it `AlknetClient` would bake QUIC in as *the*
|
||||
establishment shape — the same welding ADR-065 unwound on the server side.
|
||||
|
||||
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.
|
||||
This is why `from_connection` is the one-way-door surface and
|
||||
`connect_quic` is a two-way-door convenience over it. `AlknetClient` (when
|
||||
extracted, after a second transport's dial exists) becomes the shared
|
||||
*dial*; `from_connection` stays the shared *channels-take-over*. The two
|
||||
concerns are separated now, before the one-way-door API is cast.
|
||||
|
||||
The friction while `AlknetClient` is deferred is duplicated
|
||||
verifier-selection boilerplate across dial helpers (~20 lines each) — not
|
||||
duplicated capability and not a QUIC-welded client API.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
@@ -132,12 +187,14 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; QUIC-only; `AlknetClient` deferred (OQ-55) |
|
||||
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary, `connect_quic` convenience; `AlknetClient` dial-seam deferred (OQ-55) |
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-55** (deferred(scope)): `AlknetClient` core extraction — blocked on
|
||||
a second *transport's* client. `ChannelClient` does not unblock it.
|
||||
- **OQ-55** (deferred(scope)): `AlknetClient` core **dial+TLS seam**
|
||||
extraction — blocked on a second *transport's* dial. `ChannelClient`'s
|
||||
API is transport-agnostic (`from_connection`); the deferred part is the
|
||||
shared *dial* across transports, not the channels protocol.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
|
||||
| [077](../../decisions/077-tty-inside-channels.md) | TTY Inside Channels | Two modes (direct vs channels); 5 sub-streams; control bidirectional via 3/4 |
|
||||
| [078](../../decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Pattern | Shutdown-on-completion contract; handler-level |
|
||||
| [079](../../decisions/079-hub-relay-translate-not-forward.md) | Hub Relay | Translate channel 0, byte-forward data channels with ID rewrite |
|
||||
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; QUIC-only; `AlknetClient` deferred (OQ-55) |
|
||||
| [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary, `connect_quic` convenience; `AlknetClient` dial-seam deferred (OQ-55) |
|
||||
| [081](../../decisions/081-channels-subcrate-decomposition.md) | Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers |
|
||||
|
||||
## Open Questions
|
||||
@@ -257,9 +257,10 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
|
||||
Open questions are tracked in [open-questions.md](../../open-questions.md).
|
||||
Key questions affecting this crate:
|
||||
|
||||
- **OQ-55** (deferred(scope)): `AlknetClient` core extraction — blocked on
|
||||
a second *transport's* client, not a second client. `ChannelClient` is
|
||||
decided (ADR-080).
|
||||
- **OQ-55** (deferred(scope)): `AlknetClient` core **dial+TLS seam**
|
||||
extraction — blocked on a second *transport's* dial. `ChannelClient`'s
|
||||
API is transport-agnostic (`from_connection`); `AlknetClient` is the
|
||||
shared *dial* across transports, not the channels protocol.
|
||||
- **OQ-56** (deferred(scope)): Full channel-level flow-control windowing —
|
||||
bounded-buffer is decided (ADR-076); full windowing is an extension
|
||||
blocked on a real HOL-blocking deployment observation.
|
||||
|
||||
@@ -48,7 +48,7 @@ Core library for ALPN-based protocol dispatch. Every handler crate depends on al
|
||||
| OQ-35 | ~~API key asymmetry~~ | dissolved | `PeerEntry` supports multiple credential paths; `ApiKeyEntry` is for tokens that ARE the identity |
|
||||
| OQ-36 | Concrete persistence adapter shapes | resolved by ADR-035 | Read-sync / write-async split (`IdentityStore`); SQLite adapter caches in memory, honker NOTIFY for no-restart cache invalidation; `alknet-store-sqlite` crate |
|
||||
| OQ-37 | X.509 outgoing-only case | resolved by ADR-034 | Three remote roles (public X.509 endpoint, transport relay, hub); `PeerEntry` asymmetry correct; client-side verifier by `PeerEntry` presence (CA vs fingerprint pin) |
|
||||
| OQ-55 | AlknetClient / Client Establishment Extraction | deferred(scope) | Blocked on a second *transport's* real client (not a second QUIC client); extracting a QUIC-shaped connector now would bake QUIC in as *the* establishment shape — the welding ADR-065 unwound on the server side |
|
||||
| OQ-55 | AlknetClient / Client Establishment Extraction | deferred(scope) | Blocked on a second *transport's* real **dial** (not a second QUIC dial); extracting a QUIC-shaped connector now would bake QUIC in as *the* establishment shape — the welding ADR-065 unwound on the server side. The client take-over APIs (`CallClient::spawn_dispatch`, `ChannelClient::from_connection` — ADR-080) are transport-agnostic and decided; only the shared dial+TLS seam is deferred. |
|
||||
|
||||
## Key Design Principles
|
||||
|
||||
|
||||
Reference in new issue
Block a user