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:
190
docs/architecture/channel-client.md
Normal file
190
docs/architecture/channel-client.md
Normal file
@@ -0,0 +1,190 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-07-18
|
||||
---
|
||||
|
||||
# channel-client.md — ChannelClient
|
||||
|
||||
The client side of a channels connection. ADR-043 is the decision; this doc
|
||||
specifies the API.
|
||||
|
||||
## What
|
||||
|
||||
`ChannelClient` is the symmetric counterpart to `ChannelsAdapter` (ADR-039).
|
||||
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
|
||||
|
||||
```rust
|
||||
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-043). It must not be
|
||||
/// coupled to a transport — the channels protocol is
|
||||
/// transport-agnostic (ADR-034, as amended by ADR-035; ADR-007,
|
||||
/// ADR-009), 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-037 channel/resources/subscribe). Each
|
||||
/// event carries the JSON `output.resources` array from ADR-037'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-038 as amended by ADR-035).
|
||||
pub source: ChannelBidiStreamSource,
|
||||
}
|
||||
|
||||
/// One event from `channel/resources/subscribe`. Wraps the JSON `output`
|
||||
/// object from ADR-037'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-037'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-034 substrate modes, as amended by
|
||||
ADR-035; `Connection::from_bidi`/`from_source` from ADR-007/070/092 take
|
||||
any `AsyncRead + AsyncWrite`). The client side must not be welded to a
|
||||
transport — that would repeat the server-side welding ADR-007 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-045), 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-037 §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`](../client/README.md), 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-045](decisions/089-alknetclient-native-dial-seam.md) for the
|
||||
full decision and [OQ-55](../../questions/055-alknetclient-establishment-extraction.md)
|
||||
(resolved).
|
||||
|
||||
## Design Decisions
|
||||
|
||||
All design decisions are documented as ADRs in [decisions/](decisions/).
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [080](decisions/080-channelclient.md) | ChannelClient | Client side; transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045, resolves OQ-55) |
|
||||
| [093](decisions/093-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | No `stream_types` on `open_channel`/`Channel`; handler owns sub-stream multiplexing |
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-55** (resolved by ADR-045): `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-045](decisions/089-alknetclient-native-dial-seam.md).
|
||||
|
||||
## References
|
||||
|
||||
- ADR-043: ChannelClient (the decision)
|
||||
- ADR-035: channels pure channel multiplexing (no `stream_types`)
|
||||
- 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-039: 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)
|
||||
Reference in New Issue
Block a user