docs(arch): channels substrate simplification + stream_type decomposition + sub-crate split
Three simplifications to the channels spec, all flowing from the review: 1. Substrate simplification (ADR-071 revised): the 9-byte chunk header is used in ALL substrates — in-line (TCP+TLS, WebTransport), native (QUIC bidi streams), and multi-connection. The ChannelsAdapter reads headers off every bidi stream it accepts; the transport's native multiplexing is a performance optimization (independent flow-control windows), not a protocol change. One wire format, one code path, one handler experience. The channel_id in the header is the correlation key across substrates. 2. Stream_type decomposition (ADR-071 revised): every stream_type is unidirectional. Bidirectionality is two stream_types (write + read), not one 'bidirectional' stream_type. Grouped in threes: 0/1/2 = data write/read/err, 3/4/5 = control write/read/err, % 3 formula. This resolves the TTY control channel's 'not actually bidirectional' flaw — control is now 3 (write, client→server) + 4 (read, server→client), each with its own reassembly buffer and EOF. Channel 0 uses [0,1] (call frames bidirectional via 0=in, 1=out). TTY uses [0,1,2,3,4]. ADR-072, 073, 074, 077 updated for the new stream_type assignments. 3. Sub-crate decomposition (ADR-081 new): channels-core (pure multiplexer — wire format, demux/mux, ChannelBidiStreamSource, ChannelManager; depends on alknet-core only, no call dependency, ALPN-blind) / channels-call (channel 0 pre-negotiation + lifecycle op registrations; depends on channels-core + alknet-call) / channels-hub (relay) / channels-worker (ChannelClient). Isolates the call-protocol coupling from the pure multiplexer so the dependency graph is honest. ADR-077 (TTY inside channels) updated: TTY now uses 5 sub-streams [0,1,2,3,4] with control properly bidirectional via 3/4; amends ADR-052's stream_type assignments for direct mode too (direct alknet/tty now uses 0-4, not 0-3). Spec docs updated: channels-wire, channels-connection, channels-adapter, channel-operations, overview, README.
This commit is contained in:
1 parent
2313c51f12
commit
fd83fc1685
14 files changed
+734
-251
No files matched your search
@@ -30,16 +30,17 @@ protocol work itself.
|
||||
|
||||
| ADR | Title | Relevance |
|
||||
|-----|-------|-----------|
|
||||
| [071](../../decisions/071-channels-wire-format.md) | channels Wire Format — 9-Byte Chunk Header | The chunk format; one-way door |
|
||||
| [072](../../decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alknet/call` | Channel 0 = call protocol, no special control plane |
|
||||
| [071](../../decisions/071-channels-wire-format.md) | channels Wire Format — 9-Byte Chunk Header | The chunk format; unidirectional stream_types in groups of 3; substrate-agnostic; one-way door |
|
||||
| [072](../../decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alknet/call` | Channel 0 = call protocol, stream_types [0,1]; no special control plane |
|
||||
| [073](../../decisions/073-channel-lifecycle-operations.md) | Channel Lifecycle Operations on the Call Protocol | `channel/open`/`close`/`control`/`resources/subscribe`; `direction` semantics; subscribe not poll |
|
||||
| [074](../../decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel `BidiStreamSource` impl; `into_sub_streams()` accessor |
|
||||
| [075](../../decisions/075-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | The read/demux + reassemble/allocate split; REQ-CH-01..04 contracts |
|
||||
| [074](../../decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel `BidiStreamSource` impl; `into_sub_streams()` with `SubStreamHandle` enum (Send/Recv) |
|
||||
| [075](../../decisions/075-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts |
|
||||
| [076](../../decisions/076-backpressure-channel-limits-id-reuse.md) | Backpressure, Channel Limits, and ID Reuse | Bounded-buffer (1 MiB default), 256-channel cap, monotonic IDs with wrap |
|
||||
| [077](../../decisions/077-tty-inside-channels.md) | TTY Inside Channels — Sub-Streams, Not Wire Format | TTY's two modes (direct vs channels); amends ADR-052 scope |
|
||||
| [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` in alknet-channels, QUIC-only; `AlknetClient` deferred (OQ-55) |
|
||||
| [080](../../decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, QUIC-only; `AlknetClient` deferred (OQ-55) |
|
||||
| [081](../../decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling) / hub / worker |
|
||||
| [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 |
|
||||
| [052](../../decisions/052-alknet-tty-wire-format-and-two-carriage.md) | alknet-tty Wire Format | The 5-byte format the 9-byte format generalizes (amended by ADR-077 — scoped to direct TTY) |
|
||||
|
||||
@@ -32,7 +32,7 @@ Request (on channel 0):
|
||||
| field | type | meaning |
|
||||
|-------|------|---------|
|
||||
| `alpn` | string | The ALPN the channel will carry. Responder looks this up in its `HandlerRegistry`. |
|
||||
| `stream_types` | `[u8]` | Which sub-stream types this channel will use. E.g. `[0,1,2,3]` for TTY, `[0,1]` for a tunnel. |
|
||||
| `stream_types` | `[u8]` | Which sub-stream types this channel will use. E.g. `[0,1,2,3,4]` for TTY (data in/out/err + control in/out), `[0,1]` for a tunnel, `[0,1]` for channel 0. See ADR-071 §stream_type decomposition. |
|
||||
| `params` | object | ALPN-specific parameters. For `alknet/tty` this is `NegotiateRequest`. For `alknet/tunnel` this is the target resource. The channels layer does not interpret `params`. |
|
||||
| `direction` | string | `initiator-to-responder` or `responder-to-initiator`. See "Direction semantics" below. |
|
||||
|
||||
|
||||
@@ -14,14 +14,14 @@ invariants.
|
||||
|
||||
| Component | Role | What it knows |
|
||||
|-----------|------|---------------|
|
||||
| `ChannelsAdapter` | `ProtocolHandler` on `alknet/channels`; reads 9-byte chunk headers off the transport and routes to `ChannelManager` | The transport stream; the `ChannelManager` handle. ALPN-blind. |
|
||||
| `ChannelManager` | Shared state; holds `channel_id → ChannelState`, `HandlerRegistry`, `OperationRegistry`. Constructs `ChannelBidiStreamSource` per channel. What `channel/open` closes over. | The channel map; the handler registry for ALPN lookup. ALPN-blind (looks up ALPNs, doesn't parse their protocols). |
|
||||
| `ChannelsAdapter` | `ProtocolHandler` on `alknet/channels`; reads 9-byte chunk headers off every bidi stream the transport yields and routes to `ChannelManager`. Substrate-agnostic (ADR-071 §substrate modes). | The transport stream(s); the `ChannelManager` handle. ALPN-blind. |
|
||||
| `ChannelManager` | Shared state; holds `channel_id → ChannelState`, `HandlerRegistry`. Constructs `ChannelBidiStreamSource` per channel. What `channel/open` closes over (in `channels-call`). | The channel map; the handler registry for ALPN lookup. ALPN-blind (looks up ALPNs, doesn't parse their protocols). |
|
||||
|
||||
The split mirrors the TTY crate's `ChunkReader`/`ChunkWriter` + adapter
|
||||
pattern, generalized to N channels: the adapter drives N channels, and
|
||||
channel 0 is special only in that it's pre-allocated.
|
||||
channel 0 is special only in that it's pre-allocated (by `channels-call`).
|
||||
|
||||
## `ChannelsAdapter::handle`
|
||||
## `ChannelsAdapter::handle` (substrate-agnostic)
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
@@ -31,46 +31,47 @@ impl ProtocolHandler for ChannelsAdapter {
|
||||
async fn handle(&self, connection: Connection, auth: &AuthContext)
|
||||
-> Result<(), HandlerError>
|
||||
{
|
||||
// 1. One bidi stream carries all channels.
|
||||
// 1. Channel 0 is pre-negotiated (ADR-072). The first bidi stream
|
||||
// the transport yields is channel 0. The consumer (channels-call)
|
||||
// installs the CallAdapter on it.
|
||||
let (send, recv) = connection.accept_bi().await?;
|
||||
|
||||
// 2. Channel 0 is pre-negotiated as alknet/call (ADR-072).
|
||||
// Construct reassembly buffers, wrap as a Connection via
|
||||
// from_source(ChannelBidiStreamSource), hand to the CallAdapter.
|
||||
self.manager.preinstall_channel_0(send, recv, auth).await?;
|
||||
|
||||
// 3. Run the demux loop.
|
||||
self.manager.run_demux_loop(recv).await
|
||||
// 2. Accept remaining bidi streams and read 9-byte headers off each.
|
||||
// On an in-line transport, accept_bi() yields once and the header
|
||||
// demuxes N channels from that stream. On QUIC native, accept_bi()
|
||||
// yields repeatedly — each stream carries one logical channel.
|
||||
// Same code path, same wire format (ADR-071 §substrate modes).
|
||||
self.manager.run_demux_loop(connection).await
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `preinstall_channel_0`
|
||||
The `preinstall_channel_0` step (provided by `channels-call`, ADR-081)
|
||||
constructs the reassembly buffers for `channel_id = 0` using stream_types
|
||||
[0, 1] (ADR-072), wraps them as a `Connection` via `Connection::from_source`
|
||||
with a `ChannelBidiStreamSource` (ADR-074), and hands that `Connection` to
|
||||
the `CallAdapter`. The `ChannelsAdapter` in `channels-core` exposes the
|
||||
hook; `channels-call` provides the implementation.
|
||||
|
||||
The only special case: constructs the reassembly buffers for `channel_id =
|
||||
0`, wraps them as a `Connection` (via `Connection::from_source` with a
|
||||
`ChannelBidiStreamSource` — ADR-070/074), and hands that `Connection` to
|
||||
the `CallAdapter` — exactly as if `alknet/call` had been the top-level
|
||||
ALPN. The `CallAdapter` is looked up in the same `HandlerRegistry` as every
|
||||
other ALPN. The `CallAdapter` is none the wiser: it calls `accept_bi()`,
|
||||
gets one bidi stream (the channel-0 reassembled stream), and runs its
|
||||
dispatch loop. `EventEnvelope` frames ride on `stream_type = 0` of channel 0.
|
||||
|
||||
### `run_demux_loop`
|
||||
|
||||
Reads 9-byte headers off the transport, looks up `channel_id` in the
|
||||
`ChannelManager`'s `channels` map, and pushes the payload into the right
|
||||
`ReassemblyBuffer` for `(channel_id, stream_type)`. If the buffer is full
|
||||
(bounded-buffer backpressure, ADR-076), the loop stops reading that
|
||||
channel's chunks until the consumer drains — other channels keep flowing.
|
||||
`run_demux_loop` continues accepting bidi streams from the transport. For
|
||||
each stream, it reads 9-byte headers and routes payloads to the matching
|
||||
`(channel_id, stream_type)` reassembly buffer. On an in-line transport,
|
||||
there is only one stream (channel 0 rides inside it via the header); the
|
||||
header demuxes all channels. On QUIC, each subsequent stream is a new
|
||||
channel; the header's `channel_id` correlates it. The loop is the same;
|
||||
only the transport's stream count differs.
|
||||
|
||||
## `ChannelManager`
|
||||
|
||||
```rust
|
||||
// In alknet-channels-core:
|
||||
pub struct ChannelManager {
|
||||
channels: Mutex<HashMap<u32, ChannelState>>,
|
||||
handlers: Arc<HandlerRegistry>,
|
||||
call_ops: Arc<OperationRegistry>,
|
||||
// Note: no call_ops field — the call-protocol coupling lives in
|
||||
// channels-call (ADR-081). The ChannelManager is ALPN-blind and
|
||||
// call-protocol-blind.
|
||||
next_id: AtomicU32, // monotonic; wraps at u32::MAX
|
||||
buffer_cap: usize, // default 1 MiB (ADR-076)
|
||||
max_channels: usize, // default 256 (ADR-076)
|
||||
|
||||
@@ -11,8 +11,13 @@ access paths.
|
||||
|
||||
## What
|
||||
|
||||
Each channel is reassembled into a set of `AsyncRead + AsyncWrite` handles
|
||||
—one per active `stream_type` (declared at `channel/open` time, ADR-073).
|
||||
Each channel is reassembled into a set of **unidirectional** handles — one
|
||||
per active `stream_type` (declared at `channel/open` time, ADR-073). Every
|
||||
stream_type is unidirectional (ADR-071 §stream_type decomposition);
|
||||
bidirectionality is two stream_types (write + read), not one shared
|
||||
"bidirectional" stream. Write stream_types (`% 3 == 0`) carry a
|
||||
`SendStream`; read stream_types (`% 3 == 1 or 2`) carry a `RecvStream`.
|
||||
|
||||
These handles are wrapped as a `ChannelBidiStreamSource` that implements
|
||||
`alknet-core`'s `BidiStreamSource` trait (ADR-070), and a `Connection` is
|
||||
constructed from it via `Connection::from_source(source, alpn)`.
|
||||
@@ -21,9 +26,9 @@ The handler receives a `Connection` and can either:
|
||||
1. Call `accept_bi()` once to get the main data pair (`stream_type` 0/1) —
|
||||
the generic handler path (tunnel, SSH).
|
||||
2. Call `into_sub_streams()` on the `ChannelBidiStreamSource` to get all
|
||||
active sub-streams as typed `(stream_type, SendStream, RecvStream)`
|
||||
tuples — the typed handler path (TTY, which needs stdin/stdout/stderr/
|
||||
control).
|
||||
active sub-streams as typed `(stream_type, SubStreamHandle)` tuples —
|
||||
the typed handler path (TTY, which needs stdin/stdout/stderr/control-in/
|
||||
control-out).
|
||||
|
||||
Both paths operate on the same reassembly buffers; the difference is how the
|
||||
handler accesses them.
|
||||
@@ -115,20 +120,24 @@ For handlers that need `stream_type` 2 (stderr) or 3 (control) in addition
|
||||
to 0/1:
|
||||
|
||||
```rust
|
||||
// In alknet-channels:
|
||||
// In alknet-channels-core:
|
||||
pub struct ChannelSubStreams {
|
||||
/// (stream_type, send_half, recv_half) for each active stream_type.
|
||||
pub streams: Vec<(u8, SendStream, RecvStream)>,
|
||||
/// (stream_type, handle) for each active stream_type. Each handle is
|
||||
/// unidirectional: write stream_types (0, 3, 6, ...) carry a SendStream;
|
||||
/// read stream_types (1, 2, 4, 5, 7, ...) carry a RecvStream.
|
||||
/// See ADR-071 §stream_type decomposition.
|
||||
pub streams: Vec<(u8, SubStreamHandle)>,
|
||||
}
|
||||
|
||||
impl ChannelSubStreams {
|
||||
pub fn get(&self, stream_type: u8) -> Option<(&SendStream, &RecvStream)> { ... }
|
||||
pub enum SubStreamHandle {
|
||||
Send(SendStream), // write half (stream_type % 3 == 0)
|
||||
Recv(RecvStream), // read half (stream_type % 3 == 1 or 2)
|
||||
}
|
||||
|
||||
impl ChannelBidiStreamSource {
|
||||
/// Returns all active sub-streams, keyed by stream_type. Consumes the
|
||||
/// source — call this instead of accept_bi() if the handler needs
|
||||
/// direct access to stream_types 2/3.
|
||||
/// direct access to stream_types 2/3/4.
|
||||
pub fn into_sub_streams(self) -> ChannelSubStreams { ... }
|
||||
}
|
||||
```
|
||||
@@ -138,15 +147,16 @@ The handler crate destructures `ChannelSubStreams` into its typed names:
|
||||
```rust
|
||||
// In alknet-tty (inside-channels mode, ADR-077):
|
||||
let sub = channel_source.into_sub_streams();
|
||||
let stdin = sub.get(0).unwrap(); // SendStream
|
||||
let stdout = sub.get(1).unwrap(); // RecvStream
|
||||
let stderr = sub.get(2); // Option<&RecvStream>
|
||||
let control = sub.get(3).unwrap(); // RecvStream (JSON control)
|
||||
let stdin = sub.get_send(0).unwrap(); // SendStream (write, client→server)
|
||||
let stdout = sub.get_recv(1).unwrap(); // RecvStream (read, server→client)
|
||||
let stderr = sub.get_recv(2); // Option<RecvStream> (read, optional)
|
||||
let ctrl_in = sub.get_send(3).unwrap(); // SendStream (write, client→server)
|
||||
let ctrl_out = sub.get_recv(4).unwrap();// RecvStream (read, server→client)
|
||||
```
|
||||
|
||||
**The channels crate does not know about TTY's `stream_type` semantics.**
|
||||
It exposes `(stream_type, SendStream, RecvStream)` tuples. The handler crate
|
||||
maps stream_types to its typed names. This preserves ADR-003's
|
||||
**Every stream_type is unidirectional** (ADR-071). The channels crate
|
||||
exposes `(stream_type, SubStreamHandle)` tuples. The handler crate maps
|
||||
stream_types to its typed names. This preserves ADR-003's
|
||||
no-handler-depends-on-another-handler rule and keeps the channels crate
|
||||
ALPN-blind.
|
||||
|
||||
@@ -159,7 +169,7 @@ include the 0/1 pair, so `into_sub_streams()` is the superset.
|
||||
| Handler shape | Path | Examples |
|
||||
|---------------|------|---------|
|
||||
| Main data pair only (0/1) | `accept_bi()` | tunnel, SSH (SSH multiplexes internally) |
|
||||
| Needs stderr/control (2/3) | `into_sub_streams()` | TTY (stdin/stdout/stderr/control) |
|
||||
| Needs stderr/control (2/3/4) | `into_sub_streams()` | TTY (stdin/stdout/stderr/ctrl-in/ctrl-out) |
|
||||
|
||||
The handler chooses based on its ALPN's `stream_type` set (declared at
|
||||
`channel/open` time). The `ChannelsAdapter` (ADR-075) passes the handler a
|
||||
|
||||
@@ -37,26 +37,64 @@ stream — the demux drops the chunk and continues. The header is always
|
||||
exactly 9 bytes, so the demux can always resync by reading the next 9-byte
|
||||
header.
|
||||
|
||||
## Stream types
|
||||
## Stream types — unidirectional, grouped in threes
|
||||
|
||||
| stream_type | direction | purpose |
|
||||
|-------------|-----------|---------|
|
||||
| 0 | write half | data flowing in (stdin equivalent) |
|
||||
| 1 | read half | data flowing out (stdout equivalent) |
|
||||
| 2 | read half (optional) | error/diagnostic output (stderr equivalent) |
|
||||
| 3 | bidirectional | control messages (ALPN-specific JSON) |
|
||||
| 4-255 | reserved | future sub-stream types |
|
||||
**Every stream_type is unidirectional.** Bidirectionality is two
|
||||
stream_types (write + read), not one "bidirectional" stream_type. The
|
||||
stream_types are grouped in threes:
|
||||
|
||||
| Group | stream_type | direction | purpose |
|
||||
|-------|-------------|-----------|---------|
|
||||
| Data | 0 | write (client→server) | data in (stdin equivalent) |
|
||||
| | 1 | read (server→client) | data out (stdout equivalent) |
|
||||
| | 2 | read (server→client) | data err (stderr equivalent, optional) |
|
||||
| Control | 3 | write (client→server) | control in (resize, signal, eof) |
|
||||
| | 4 | read (server→client) | control out (exit, keepalive response) |
|
||||
| | 5 | read (server→client) | control err (optional) |
|
||||
| Future | 6/7/8 | write/read/read | next group, same pattern |
|
||||
| | ... | | |
|
||||
|
||||
**Formula:** `stream_type % 3 == 0` → write half (in), `stream_type % 3 ==
|
||||
1` → read half (out), `stream_type % 3 == 2` → diagnostic read half (err).
|
||||
|
||||
256 values / 3 = 85 groups. The `u32` channel_id space combined with 85
|
||||
stream_type groups is effectively unlimited for the intended use cases.
|
||||
|
||||
**Why unidirectional:** each stream_type gets its own reassembly buffer, its
|
||||
own flow control, its own EOF. Control is bidirectional via two halves
|
||||
(3 in, 4 out), not one shared stream both sides write to. This resolves the
|
||||
TTY control channel's "not actually bidirectional" flaw (ADR-077).
|
||||
|
||||
Not all channels use all sub-streams. The active set is declared at
|
||||
`channel/open` time (ADR-073 `stream_types` field) and fixed for the
|
||||
channel's lifetime.
|
||||
|
||||
| Channel ALPN | Active stream_types |
|
||||
|--------------|---------------------|
|
||||
| `alknet/call` (channel 0) | 0 (EventEnvelope frames) |
|
||||
| `alknet/tty` | 0, 1, 2, 3 (stdin, stdout, stderr, control) |
|
||||
| `alknet/tunnel` | 0, 1 (data-in, data-out) |
|
||||
| `alknet/ssh` | 0, 1 (data-in, data-out — SSH multiplexes internally) |
|
||||
| Channel ALPN | Active stream_types | Why |
|
||||
|--------------|---------------------|-----|
|
||||
| `alknet/call` (channel 0) | [0, 1] | call frames bidirectional via 0=in, 1=out |
|
||||
| `alknet/tty` | [0, 1, 2, 3, 4] | data in/out/err + control in/out |
|
||||
| `alknet/tunnel` | [0, 1] | data in/out only (no channels-layer control needed) |
|
||||
| `alknet/ssh` | [0, 1] | SSH multiplexes internally, including its own control |
|
||||
|
||||
## Substrate modes — same wire format, different stream counts
|
||||
|
||||
The 9-byte header is used in all substrates, on every bidi stream. The
|
||||
difference between substrates is only **how many bidi streams the transport
|
||||
yields**:
|
||||
|
||||
| Substrate | Transport | Streams | Header role |
|
||||
|-----------|-----------|---------|--------------|
|
||||
| In-line | TCP+TLS, WebTransport session, SSH `direct-tcpip` | 1 | Header demuxes N channels from that 1 stream |
|
||||
| Native | QUIC (quinn/iroh) | N | Each stream carries 1 logical channel; header provides `stream_type` + `channel_id` correlation |
|
||||
| Multi-connection | Any, N connections | N × M | Each connection is self-contained (own channel 0, own demux); header is per-connection |
|
||||
|
||||
The `ChannelsAdapter::handle` loop: `accept_bi()` → for each stream, read
|
||||
the 9-byte header → route by `(channel_id, stream_type)` → reassemble. On
|
||||
an in-line transport, `accept_bi()` yields once then `ConnectionClosed` —
|
||||
the header does all the demux. On QUIC, `accept_bi()` yields repeatedly —
|
||||
each stream is a channel, and the header provides `stream_type` and
|
||||
`channel_id` correlation. Same code path, same wire format, same handler
|
||||
experience. See ADR-071 §substrate modes, ADR-075.
|
||||
|
||||
## Channel 0 — pre-negotiated `alknet/call`
|
||||
|
||||
@@ -64,9 +102,11 @@ Channel 0 is not a special "control plane" with its own framing. It is
|
||||
`alknet/call` pre-negotiated (ADR-072): both sides know `channel_id = 0` is
|
||||
routed to the `CallAdapter` without an explicit `channel/open` exchange.
|
||||
|
||||
Channel 0 uses only `stream_type` 0 for `EventEnvelope` frames (JSON,
|
||||
length-prefixed — the call protocol wire format, ADR-064). `stream_type`
|
||||
1-255 on channel 0 are reserved for future call-protocol sub-streams.
|
||||
Channel 0 uses stream_types [0, 1] — call frames bidirectional via 0=in
|
||||
(client→server), 1=out (server→client). The call protocol's `(SendStream,
|
||||
RecvStream)` pair maps directly: `SendStream` backed by stream_type 0,
|
||||
`RecvStream` backed by stream_type 1. stream_types 2-255 on channel 0 are
|
||||
reserved for future call-protocol sub-streams.
|
||||
|
||||
Channel 0's chunks have `channel_id = 0` in the header — same format as
|
||||
every other channel. Disambiguation between channel 0 and data channels is
|
||||
@@ -80,9 +120,9 @@ The 9-byte header is always exactly 9 bytes. `length` is bounded by
|
||||
`ChunkTooLarge`), the demux resyncs by reading the next 9-byte header —
|
||||
the format is self-synchronizing.
|
||||
|
||||
Within a channel, `stream_type` 0 (stdin) from the server is invalid, so
|
||||
`0x00` as the first byte of a chunk payload from the server is unambiguous
|
||||
(carried from ADR-052 §5).
|
||||
Within a channel, `stream_type` 0 (write half) from the server is invalid,
|
||||
so `0x00` as the first byte of a chunk payload from the server is
|
||||
unambiguous (carried from ADR-052 §5).
|
||||
|
||||
## Zero-length sentinel = EOF
|
||||
|
||||
@@ -182,7 +222,7 @@ tokio-dependent shell. The POC validated the sync core compiles under
|
||||
|-------|-----------|-----------|
|
||||
| Open | `channel/open` call operation on channel 0; responder allocates `channel_id`, returns it | ADR-073 |
|
||||
| Data | chunks with `channel_id` routed to reassembly buffers; handler sees `AsyncRead + AsyncWrite` | this doc, [channels-connection.md](channels-connection.md) |
|
||||
| Control (data-ordered) | `stream_type 3` chunks on the data channel (JSON, in-order with data) | ADR-073 §DP-4 |
|
||||
| Control (data-ordered) | `stream_type 3` (write) and `stream_type 4` (read) chunks on the data channel (JSON, in-order with data) | ADR-073 §DP-4 |
|
||||
| Control (out-of-band) | `channel/control` call operation on channel 0 | ADR-073 |
|
||||
| Close | `channel/close` call operation on channel 0; data chunks flushed before close | ADR-073, REQ-CH-06 |
|
||||
|
||||
@@ -194,10 +234,10 @@ invariant: the side closing must observe the data-channel pump complete
|
||||
before issuing the call operation.
|
||||
|
||||
For TTY this is the exit-chunk-is-last invariant (ADR-055) carried forward:
|
||||
the exit control message on `stream_type 3` is the last data before
|
||||
`channel/close`. For tunnels it is the last data byte before close. The
|
||||
channels layer's close handler observes the pump completion; the call
|
||||
operation is issued after.
|
||||
the exit control message on `stream_type 4` (read, server→client) is the
|
||||
last data before `channel/close`. For tunnels it is the last data byte
|
||||
before close. The channels layer's close handler observes the pump
|
||||
completion; the call operation is issued after.
|
||||
|
||||
This invariant crosses two channels (the data channel and channel 0), so
|
||||
the channels layer owns the ordering guarantee — it is not a handler
|
||||
|
||||
@@ -94,29 +94,48 @@ design.
|
||||
## Crate dependencies
|
||||
|
||||
```
|
||||
alknet-channels
|
||||
alknet-channels-core
|
||||
├── alknet-core (ProtocolHandler, Connection, HandlerRegistry,
|
||||
│ BidiStreamSource, SendStream, RecvStream, AuthContext)
|
||||
├── alknet-call (OperationRegistry, HandlerKind, make_handler,
|
||||
│ make_streaming_handler, CallError, ResponseEnvelope)
|
||||
├── tokio (spawn, mpsc, io)
|
||||
├── bytes (Bytes for chunk payloads)
|
||||
├── async-trait
|
||||
├── thiserror
|
||||
└── tracing
|
||||
|
||||
alknet-channels-call
|
||||
├── alknet-channels-core (ChannelManager, ChannelsAdapter,
|
||||
│ ChannelBidiStreamSource)
|
||||
├── alknet-call (OperationRegistry, HandlerKind, make_handler,
|
||||
│ make_streaming_handler, CallError, ResponseEnvelope)
|
||||
└── tokio
|
||||
|
||||
alknet-channels-hub (or the hub role within alknet-hub)
|
||||
├── alknet-channels-call
|
||||
└── alknet-call (from_call, CallAdapter, forwarded_for — ADR-079)
|
||||
|
||||
alknet-channels-worker (or the worker/client role)
|
||||
├── alknet-channels-call
|
||||
└── alknet-call (CallClient-equivalent)
|
||||
```
|
||||
|
||||
`alknet-channels` depends on `alknet-call` because channel lifecycle
|
||||
operations (`channel/open`, `channel/close`, `channel/control`,
|
||||
`channel/resources/subscribe`) are registered on the call protocol's
|
||||
`OperationRegistry` (ADR-073). This is the one handler-crate →
|
||||
`alknet-call` dependency; it is sound because channels *is* a call-protocol
|
||||
extension (channel lifecycle is call operations), not a peer handler.
|
||||
`alknet-channels-core` is the pure multiplexer — wire format, demux/mux,
|
||||
`ChannelBidiStreamSource`, `ChannelManager`. It depends on `alknet-core`
|
||||
only. No `alknet-call` dependency. ALPN-blind, call-protocol-blind,
|
||||
transport-blind. This is where the "streams are streams" insight lives.
|
||||
|
||||
The dependency direction is: handlers depend on `alknet-core`;
|
||||
`alknet-channels` depends on `alknet-core` and `alknet-call`; nothing
|
||||
depends on `alknet-channels` except the assembly layer and
|
||||
`alknet-tty`/`alknet-docker` behind their `channels`/`tty` feature gates.
|
||||
`alknet-channels-call` is the call-protocol coupling — channel 0
|
||||
pre-negotiation as `alknet/call` (ADR-072) and the four lifecycle operations
|
||||
(ADR-073) registered on the call protocol's `OperationRegistry`. This is
|
||||
where the call-protocol coupling lives, isolated from the pure multiplexer.
|
||||
|
||||
`alknet-channels-hub` and `alknet-channels-worker` (or the hub/worker roles
|
||||
within `channels-call`) are the two roles: the relay (ADR-079) and the
|
||||
`ChannelClient` (ADR-080). Whether these are separate sub-crates or
|
||||
feature-gated modules within `channels-call` is a two-way-door packaging
|
||||
decision (ADR-081).
|
||||
|
||||
See ADR-081 for the full decomposition rationale.
|
||||
|
||||
## ALPN
|
||||
|
||||
@@ -220,16 +239,17 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
|
||||
|
||||
| ADR | Decision | Summary |
|
||||
|-----|----------|---------|
|
||||
| [071](../../decisions/071-channels-wire-format.md) | channels Wire Format | 9-byte chunk header; one-way door |
|
||||
| [072](../../decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = `alknet/call`, no special control plane |
|
||||
| [071](../../decisions/071-channels-wire-format.md) | channels Wire Format | 9-byte chunk header; unidirectional stream_types in groups of 3; one-way door |
|
||||
| [072](../../decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = `alknet/call`, stream_types [0,1] |
|
||||
| [073](../../decisions/073-channel-lifecycle-operations.md) | Channel Lifecycle Operations | `channel/open`/`close`/`control`/`resources/subscribe`; subscribe not poll; `direction` pinned |
|
||||
| [074](../../decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel `BidiStreamSource`; `into_sub_streams()` accessor |
|
||||
| [075](../../decisions/075-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | The read/demux + reassemble/allocate split; REQ-CH-01..04 |
|
||||
| [074](../../decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel `BidiStreamSource`; `into_sub_streams()` with `SubStreamHandle` enum |
|
||||
| [075](../../decisions/075-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 |
|
||||
| [076](../../decisions/076-backpressure-channel-limits-id-reuse.md) | Backpressure, Limits, ID Reuse | Bounded-buffer (1 MiB), 256-channel cap, monotonic IDs |
|
||||
| [077](../../decisions/077-tty-inside-channels.md) | TTY Inside Channels | Two modes (direct vs channels); ADR-052 scoped to direct |
|
||||
| [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) |
|
||||
| [081](../../decisions/081-channels-subcrate-decomposition.md) | Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling) / hub / worker |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
Reference in new issue
Block a user