docs: port architecture specs + 45 ADRs from alknet, renumbered
Port the call + channels architecture documentation from the alknet mono-repo into docs/architecture/, renumbered as alkcall ADR-001..045. Renumbering map (alknet -> alkcall): Core: 001,002,004,006,007,011,065,070,092,014,050,091 -> 001-012 Call: 005,064,012,023,015,022,024,016,049,017,028,029,030,032,066,069,067,068 -> 013-030 Shared: 003,009,013 -> 031-033 Channels: 071,093,072,073,074,075,076,094,079,080,081,089 -> 034-045 3 superseded/reversed ADRs kept for historical trail: - ADR-013 (irpc foundation, superseded by ADR-014) - ADR-023 (peer-scoped filtering, superseded by ADR-024) - ADR-077 (TTY inside channels, reversed by ADR-035 — not ported, TTY-only) Ported docs (11 spec files + README + open-questions): - call-README.md, call-protocol.md, operation-registry.md, client-and-adapters.md - channels-README.md, channels-overview.md, channels-wire.md, channels-connection.md, channels-adapter.md, channel-operations.md, channel-client.md - README.md (index with doc table, ADR table grouped by category, key principles) - open-questions.md (lean — 30 OQs, renumbered OQ-01..030; includes new OQ-22 for the pub/sub gap) Cross-reference rewriting: - All ADR-NNN references rewritten single-pass (no chaining bug) - Markdown link paths fixed - Title lines aligned with filenames - Non-ported ADR refs (052, 082, 086, etc.) left as-is with README note The open-questions.md includes OQ-22 (new): the call protocol pub/sub gap — subscribe exists but pub does not, needed for channels channel/resources/subscribe fan-out. This is the next ADR to write (alkcall ADR-046).
This commit is contained in:
300
docs/architecture/decisions/043-channelclient.md
Normal file
300
docs/architecture/decisions/043-channelclient.md
Normal file
@@ -0,0 +1,300 @@
|
||||
# ADR-043: ChannelClient — the Client Side of a Channels Connection
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (amended 2026-07-12 — see "Amendment: transport-agnostic API"
|
||||
below; amended 2026-07-16 — `connect_quic` removed per ADR-045 §5, see
|
||||
"Amendment: `connect_quic` removed" below; **amended 2026-07-18 by
|
||||
ADR-035 — `stream_types` field removed from `open_channel` and `Channel`;
|
||||
the channels layer has no `stream_type` concept, see "Amendment
|
||||
(ADR-035, 2026-07-18)" below**)
|
||||
|
||||
## Amendment (ADR-035, 2026-07-18)
|
||||
|
||||
The `stream_types: &[u8]` field is **removed** from `open_channel`'s
|
||||
signature, and `pub stream_types: Vec<u8>` is **removed** from the
|
||||
`Channel` struct. The channels layer has no `stream_type` concept
|
||||
(ADR-035) — the handler owns its sub-stream multiplexing on the
|
||||
`BiStream` it receives via `Channel.source` (a `ChannelBidiStreamSource`
|
||||
whose `accept_bi` yields a `BiStream` per ADR-009). The handler's
|
||||
sub-stream set is implicit in its ALPN's wire format (e.g., TTY's 5-byte
|
||||
format declares its own `stream_type` set internally; the channels
|
||||
layer carries the bytes transparently). The `into_sub_streams()` reference
|
||||
in the `Channel.source` doc comment is moot — `into_sub_streams()` is
|
||||
removed by ADR-035 (amending ADR-038).
|
||||
|
||||
The body below describes the **original** (with `stream_types`) shape;
|
||||
the amendment above is the operative decision. See ADR-035 for the
|
||||
resolution rationale and the cross-ADR impacts.
|
||||
|
||||
## Amendment: `connect_quic` removed (2026-07-16, per ADR-045 §5)
|
||||
|
||||
The `connect_quic(addr, credentials)` convenience constructor is
|
||||
**removed**. Keeping it as a thin wrapper over
|
||||
`AlknetClient::dial_quic` would make `alknet-channels-call` depend on
|
||||
`alknet-client`, contradicting the dep graph (the protocol crates are
|
||||
parallel to the dial, not downstream of it). Callers compose
|
||||
`AlknetClient::dial_quic(...).await?` + `ChannelClient::from_connection(conn).await?`.
|
||||
The `from_connection` primary constructor (the 2026-07-12 amendment
|
||||
below) is unchanged and remains the one-way-door surface. The
|
||||
`connect_quic` references in the body of this ADR are the historical
|
||||
shape; they do not survive into the implementation. See ADR-045 §5 for
|
||||
the full rationale and the breaking-change acknowledgment.
|
||||
|
||||
## Amendment: transport-agnostic API (2026-07-12)
|
||||
|
||||
The original Decision named `connect(addr: SocketAddr, credentials)` as the
|
||||
primary constructor and framed it as "QUIC-only initially" — to be
|
||||
generalized when a second transport's client exists. That framing welded
|
||||
the client-side one-way-door API to QUIC, the same welding ADR-007 unwound
|
||||
on the server side, and masked it as a two-way-door deferral
|
||||
(anti-patterns #8, #9, #11). "Can be generalized later" meant "can be
|
||||
rewritten later" — the expensive reversal the one-way-door classification
|
||||
exists to prevent.
|
||||
|
||||
The channels protocol is transport-agnostic by design (ADR-034 substrate
|
||||
modes; `Connection::from_stream`/`from_bidi`/`from_source` accept any
|
||||
`AsyncRead + AsyncWrite`). The client side is half of that protocol and
|
||||
must not be coupled to a transport. This amendment splits the constructor
|
||||
surface:
|
||||
|
||||
- **`from_connection(connection: Connection)`** — the transport-agnostic
|
||||
primary constructor and the one-way-door API. Takes a pre-established
|
||||
`Connection` (produced by any transport — TCP+TLS via `from_bidi`,
|
||||
WebTransport `BiStream`, SSH `direct-tcpip`, a quinn connection, a
|
||||
WebSocket carrying `alknet/channels` per ADR-044), installs channel 0,
|
||||
spawns the demux/mux, returns the client. Mirrors the server-side
|
||||
`ChannelsAdapter::handle(Connection)` (substrate-agnostic) and the
|
||||
existing `CallClient::spawn_dispatch(Connection)` pattern.
|
||||
- **`connect_quic(addr, credentials)`** — a QUIC convenience constructor:
|
||||
dial QUIC, then `from_connection`. Additive and two-way-door.
|
||||
`connect_tcp_tls`, `connect_webtransport`, etc. join it as transports
|
||||
are added, without touching the one-way-door surface.
|
||||
|
||||
The dial+TLS seam (the transport-specific work each dial helper does —
|
||||
verifier selection per ADR-034, handshake, produce a `Connection`) is the
|
||||
correct scope of OQ-55's deferral. `AlknetClient` is the eventual shared
|
||||
*dial*; `from_connection` is the shared *channels-take-over*. Separating
|
||||
them now, before the one-way-door API is cast, is the point — not a
|
||||
deferral. The "QUIC-only initially" framing is removed; it was the
|
||||
anti-pattern this amendment corrects.
|
||||
|
||||
The door-type classification is unchanged: `from_connection` is one-way
|
||||
(the handler-facing surface), `connect_quic` is two-way (additive
|
||||
convenience). The `AlknetClient` extraction remains deferred (OQ-55) — but
|
||||
what is deferred is the shared *dial*, not a QUIC-welded client API.
|
||||
|
||||
## Context
|
||||
|
||||
Both sides of a channels connection do the demux/mux work. The server side
|
||||
is a `ProtocolHandler` (`ChannelsAdapter::handle`, ADR-039). The client side
|
||||
needs a symmetric type — `ChannelClient` — that 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.
|
||||
|
||||
The phase-0 research (`docs/research/alknet-channels/phase-0-findings.md`
|
||||
§OQ-CH-14) clarified that there are two concerns here:
|
||||
|
||||
1. **`ChannelClient` (channels-specific):** the client type for channels
|
||||
connections. Decision-ready — build it in `alknet-channels`, same shape
|
||||
as `CallClient`. The *take-over* half is transport-agnostic
|
||||
(`from_connection`); the *dial* half is transport-specific
|
||||
(`connect_quic` and future transport helpers).
|
||||
2. **`AlknetClient` (core, transport-polymorphic):** the shared *dial+TLS*
|
||||
seam — the transport-specific work (open socket, TLS handshake, ADR-034
|
||||
verifier selection, produce a `Connection`) that each transport's dial
|
||||
helper rebuilds. Genuinely deferred — blocked on a second *transport's*
|
||||
dial existing (OQ-55 tracks this correctly). A single QUIC dial
|
||||
(`connect_quic`) does not give enough information to extract the
|
||||
transport-polymorphic dial seam; two different transport dials do.
|
||||
|
||||
This ADR decides #1 — `ChannelClient`, with `from_connection` as the
|
||||
transport-agnostic primary constructor and `connect_quic` as a transport-
|
||||
specific dial helper. #2 (the shared `AlknetClient` dial+TLS seam) stays
|
||||
deferred per OQ-55.
|
||||
|
||||
## Decision
|
||||
|
||||
### `ChannelClient` in `alknet-channels`
|
||||
|
||||
```rust
|
||||
pub struct ChannelClient {
|
||||
manager: ChannelManager,
|
||||
// The transport-side demux/mux, running in a background task.
|
||||
...
|
||||
}
|
||||
|
||||
impl ChannelClient {
|
||||
/// Transport-agnostic primary constructor. Takes a pre-established
|
||||
/// `Connection` (any transport — TCP+TLS via `from_bidi`,
|
||||
/// WebTransport BiStream, SSH direct-tcpip, a quinn connection, a
|
||||
/// WebSocket per ADR-044), installs channel 0 (alknet/call), spawns
|
||||
/// the demux/mux, and returns the client. Mirrors the server-side
|
||||
/// `ChannelsAdapter::handle(Connection)`. This is the one-way-door
|
||||
/// API surface — it must not be coupled to a transport (ADR-034,
|
||||
/// ADR-007).
|
||||
pub async fn from_connection(connection: Connection)
|
||||
-> Result<Self, ChannelError>;
|
||||
|
||||
/// QUIC convenience constructor. Dials a QUIC connection to `addr`
|
||||
/// on ALPN `alknet/channels` (credentials → TLS handshake,
|
||||
/// ADR-034 verifier selection), then calls `from_connection`.
|
||||
/// Additive and two-way-door — `connect_tcp_tls`,
|
||||
/// `connect_webtransport`, etc. join it as transports are added.
|
||||
///
|
||||
/// **REMOVED per ADR-045 §5.** The dial is extracted into
|
||||
/// `AlknetClient`; `connect_quic` is deleted, not delegated.
|
||||
/// Callers compose `AlknetClient::dial_quic` + `from_connection`.
|
||||
/// The `CallCredentials` parameter is moot — `CallCredentials` is
|
||||
/// removed per ADR-012 (amended 2026-07-17); the dial consumes
|
||||
/// `ConnectionCredentials` from `alknet-core`.
|
||||
pub async fn connect_quic(
|
||||
addr: SocketAddr,
|
||||
credentials: CallCredentials, // REMOVED — CallCredentials is removed
|
||||
) -> 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's sub-streams.
|
||||
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-037 channel/resources/subscribe). Part of
|
||||
/// the one-way-door handler-facing surface (see Door type below).
|
||||
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 struct Channel {
|
||||
pub channel_id: u32,
|
||||
pub stream_types: Vec<u8>,
|
||||
/// The sub-streams, accessible via the BidiStreamSource (accept_bi) or
|
||||
/// into_sub_streams() — ADR-038.
|
||||
pub source: ChannelBidiStreamSource,
|
||||
}
|
||||
```
|
||||
|
||||
### Transport-agnostic by construction
|
||||
|
||||
`ChannelClient` is the client side of the channels protocol, which is
|
||||
transport-agnostic (ADR-034 substrate modes; ADR-007 `from_stream`/`from_bidi`). The primary constructor — `from_connection(connection: Connection)` — takes a pre-established `Connection` from any
|
||||
transport and takes over channels establishment. This mirrors the
|
||||
server-side `ChannelsAdapter::handle(Connection)`, which is
|
||||
substrate-agnostic by the same mechanism: the server receives a
|
||||
`Connection` (QUIC-native, TCP+TLS via `from_bidi`, WebTransport, SSH
|
||||
`direct-tcpip`, …) and runs the demux loop unchanged; the client receives
|
||||
a `Connection` the same way and runs the same logic from the dialing side.
|
||||
|
||||
`connect_quic(addr, credentials)` is a convenience over `from_connection`:
|
||||
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.
|
||||
|
||||
This is the client-side analogue of the server-side generalization ADR-007
|
||||
made. Welding the client's one-way-door API to QUIC would repeat the
|
||||
welding ADR-007 explicitly unwound.
|
||||
|
||||
### Bidirectionality preserved
|
||||
|
||||
The channels protocol is bidirectional — either side can open a channel
|
||||
(ADR-037 §direction semantics). `ChannelClient::open_channel` supports both
|
||||
`ChannelDirection::InitiatorToResponder` and
|
||||
`ChannelDirection::ResponderToInitiator`. The client is not "the client
|
||||
side" in the sense of only initiating — 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).
|
||||
|
||||
This means `ChannelClient` is not purely a "client" in the request/response
|
||||
sense — it's one endpoint of a bidirectional channels connection. The name
|
||||
`ChannelClient` follows the `CallClient` convention (the side that dialed),
|
||||
not a request/response role.
|
||||
|
||||
### Relationship to `AlknetClient` (OQ-55 — deferred)
|
||||
|
||||
`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-007 unwound on the server side.
|
||||
|
||||
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.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:**
|
||||
- `ChannelClient` gives the channels crate a symmetric client/server pair,
|
||||
matching the call protocol's `CallAdapter`/`CallClient` shape.
|
||||
- Bidirectionality is preserved — the client can both initiate and receive
|
||||
`channel/open`.
|
||||
- The `AlknetClient` deferral (OQ-55) is not blocked by `ChannelClient` —
|
||||
they are independent concerns. `ChannelClient` builds standalone; the
|
||||
core extraction happens later when the blocker clears.
|
||||
|
||||
**Negative:**
|
||||
- Each transport-specific dial helper duplicates ~20 lines of
|
||||
verifier-selection boilerplate (from `connect_quic`). This is the known
|
||||
cost of not extracting `AlknetClient` yet (OQ-55). Acceptable until the
|
||||
second transport's dial exists, at which point `AlknetClient` extracts
|
||||
the shared dial+TLS seam. The `from_connection` API — the one-way-door
|
||||
surface — is unaffected; only the dial helpers carry the duplication.
|
||||
|
||||
## Door type
|
||||
|
||||
**One-way.** The `ChannelClient::from_connection` / `open_channel` /
|
||||
`call` / `subscribe_resources` API is the handler-facing surface;
|
||||
changing it after consumers exist is a rewrite. `from_connection` is the
|
||||
one-way-door primary constructor (transport-agnostic).
|
||||
|
||||
`connect_quic` (and future `connect_tcp_tls` / `connect_webtransport` /
|
||||
…) are **two-way** doors — additive convenience constructors over
|
||||
`from_connection`. Adding, removing, or changing a dial helper is cheap
|
||||
and does not touch the one-way-door surface.
|
||||
|
||||
The `AlknetClient` extraction is a **deferred decision** (OQ-55,
|
||||
deferred(scope)), not a door-type attribute. Its door type is two-way (the
|
||||
extraction is a refactor, not a wire-format change), but it is not decided
|
||||
in this ADR — see OQ-55 for the blocking condition. What is deferred is the
|
||||
shared *dial+TLS* seam; `from_connection`'s transport-agnostic contract is
|
||||
decided now.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-037: channel lifecycle operations (`open_channel` sends `channel/open`)
|
||||
- ADR-038: ChannelBidiStreamSource (what `Channel.source` wraps, as
|
||||
amended by ADR-035 — `accept_bi` yields a `BiStream`)
|
||||
- ADR-035: channels pure channel multiplexing (`stream_types` field
|
||||
removed from `open_channel` and `Channel`; handler owns sub-stream
|
||||
multiplexing)
|
||||
- ADR-039: ChannelManager (the shared state `ChannelClient` holds)
|
||||
- OQ-55: AlknetClient / client establishment extraction (the deferred core
|
||||
concern this ADR does NOT block on)
|
||||
- `docs/research/alknet-channels/phase-0-findings.md` §OQ-CH-14 (the
|
||||
research-scope question this ADR carries forward)
|
||||
- `docs/architecture/crates/call/client-and-adapters.md` — `CallClient` (the
|
||||
shape `ChannelClient` mirrors)
|
||||
Reference in New Issue
Block a user