docs(arch): control format is ALPN-specific (not JSON-binding); hub/worker are consumers not sub-crates
Two refinements from the review: 1. Control stream_types (3/4/5) carry ALPN-specific payloads, not JSON. The channels layer is blind to what control stream_types carry — it reassembles bytes and delivers them to the handler. TTY happens to use JSON for its control channel because its control messages map cleanly to JSON; another ALPN might use a binary format. The channels layer does not mandate JSON on control stream_types, the same way it doesn't mandate a format for data stream_types. This prevents the TTY/PTY JSON constraint from becoming a binding constraint on all future channel types. (ADR-071, channels-wire.md) 2. Hub and worker are consumers of channels, not sub-crates. The existing alknet-hub crate IS the channels hub — it depends on channels-call and uses channels as its substrate, with the relay logic (ADR-079) living in alknet-hub. A worker is any crate that uses ChannelClient to dial. There are no channels-hub or channels-worker sub-crates. The dependency direction is: alknet-hub → channels-call → channels-core → alknet-core; worker → channels-call → channels-core → alknet-core. The channels crate has no dependency on alknet-hub or any worker crate. (ADR-081, overview.md)
This commit is contained in:
1 parent
fd83fc1685
commit
3006e29afc
5 files changed
+107
-76
No files matched your search
@@ -40,7 +40,7 @@ protocol work itself.
|
||||
| [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) |
|
||||
| [081](../../decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling) / hub / worker |
|
||||
| [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 |
|
||||
| [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) |
|
||||
|
||||
@@ -48,8 +48,8 @@ stream_types are grouped in threes:
|
||||
| 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) |
|
||||
| Control | 3 | write (client→server) | control in (ALPN-specific format) |
|
||||
| | 4 | read (server→client) | control out (ALPN-specific format) |
|
||||
| | 5 | read (server→client) | control err (optional) |
|
||||
| Future | 6/7/8 | write/read/read | next group, same pattern |
|
||||
| | ... | | |
|
||||
@@ -65,6 +65,13 @@ 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).
|
||||
|
||||
**Control payload format is ALPN-specific.** The channels layer is blind to
|
||||
what stream_types 3/4/5 carry — it reassembles bytes and delivers them to
|
||||
the handler. TTY happens to use JSON for its control channel; another ALPN
|
||||
might use a binary format. The channels layer does not mandate JSON on
|
||||
control stream_types, the same way it doesn't mandate a format for data
|
||||
stream_types.
|
||||
|
||||
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.
|
||||
|
||||
@@ -105,18 +105,17 @@ alknet-channels-core
|
||||
|
||||
alknet-channels-call
|
||||
├── alknet-channels-core (ChannelManager, ChannelsAdapter,
|
||||
│ ChannelBidiStreamSource)
|
||||
│ ChannelBidiStreamSource, ChannelClient)
|
||||
├── alknet-call (OperationRegistry, HandlerKind, make_handler,
|
||||
│ make_streaming_handler, CallError, ResponseEnvelope)
|
||||
└── tokio
|
||||
|
||||
alknet-channels-hub (or the hub role within alknet-hub)
|
||||
alknet-hub (the existing hub crate — consumes channels)
|
||||
├── 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)
|
||||
worker crates (any crate that dials a hub — consumes channels)
|
||||
└── alknet-channels-call (ChannelClient — ADR-080)
|
||||
```
|
||||
|
||||
`alknet-channels-core` is the pure multiplexer — wire format, demux/mux,
|
||||
@@ -125,15 +124,17 @@ only. No `alknet-call` dependency. ALPN-blind, call-protocol-blind,
|
||||
transport-blind. This is where the "streams are streams" insight lives.
|
||||
|
||||
`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.
|
||||
pre-negotiation as `alknet/call` (ADR-072), the four lifecycle operations
|
||||
(ADR-073) registered on the call protocol's `OperationRegistry`, and
|
||||
`ChannelClient` (ADR-080). 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).
|
||||
The hub and worker are **consumers**, not sub-crates. The existing
|
||||
`alknet-hub` crate IS the channels hub — it depends on `channels-call` and
|
||||
uses channels as its substrate, with the relay logic (ADR-079) living in
|
||||
`alknet-hub` alongside its existing peer lifecycle and service discovery
|
||||
responsibilities. A worker is any crate that uses `ChannelClient` to dial.
|
||||
There are no `channels-hub` or `channels-worker` sub-crates.
|
||||
|
||||
See ADR-081 for the full decomposition rationale.
|
||||
|
||||
@@ -249,7 +250,7 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/).
|
||||
| [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 |
|
||||
| [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
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ stream_types are grouped in threes:
|
||||
| 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) |
|
||||
| Control | 3 | write (client→server) | control in (ALPN-specific format) |
|
||||
| | 4 | read (server→client) | control out (ALPN-specific format) |
|
||||
| | 5 | read (server→client) | control err (optional) |
|
||||
| Future | 6/7/8 | write/read/read | next group, same pattern |
|
||||
| | 9/10/11 | write/read/read | next group |
|
||||
@@ -120,6 +120,15 @@ isn't actually bidirectional" problem the TTY crate has today. The same
|
||||
principle applies to any future channel type — control is two halves, not
|
||||
one shared stream.
|
||||
|
||||
**Control payload format is ALPN-specific, not channels-enforced.** The
|
||||
channels layer is blind to what stream_types 3/4/5 carry — it reassembles
|
||||
bytes and delivers them to the handler. The TTY crate happens to use JSON
|
||||
for its control channel (resize, signal, eof, exit) because its control
|
||||
messages map cleanly to JSON; another ALPN might use a binary control
|
||||
format. The channels layer does not mandate JSON on control stream_types.
|
||||
This is the same ALPN-blindness principle that applies to the data
|
||||
stream_types: the channels layer routes bytes, the handler interprets them.
|
||||
|
||||
### Per-ALPN stream_type sets
|
||||
|
||||
| ALPN | Active stream_types | Why |
|
||||
|
||||
@@ -41,23 +41,27 @@ separable from the call-protocol orchestration (`channels-call`).
|
||||
|
||||
## Decision
|
||||
|
||||
### Three crates
|
||||
### Two crates, not four
|
||||
|
||||
```
|
||||
alknet-channels-core — the pure multiplexer (wire format, demux/mux,
|
||||
│ ChannelBidiStreamSource, ChannelManager). Depends
|
||||
│ on alknet-core only. ALPN-blind, call-protocol-blind,
|
||||
│ transport-blind.
|
||||
├── alknet-channels-call — channel 0 pre-negotiation + lifecycle op
|
||||
│ │ registrations on the call protocol's
|
||||
│ │ OperationRegistry. Depends on channels-core +
|
||||
│ │ alknet-call.
|
||||
│ ├── alknet-channels-hub — the relay (ADR-079). Depends on
|
||||
│ │ channels-call. The hub-side role.
|
||||
│ └── alknet-channels-worker — ChannelClient (ADR-080). Depends on
|
||||
│ channels-call. The client/worker-side role.
|
||||
└── alknet-channels-call — channel 0 pre-negotiation + lifecycle op
|
||||
registrations on the call protocol's
|
||||
OperationRegistry. Depends on channels-core +
|
||||
alknet-call.
|
||||
```
|
||||
|
||||
There are no `channels-hub` or `channels-worker` sub-crates. The hub and
|
||||
worker are **consumers** of channels, not sub-crates of it. The existing
|
||||
`alknet-hub` crate (`docs/architecture/crates/hub/README.md`) IS the hub —
|
||||
it depends on `channels-call` and uses the channels protocol as its
|
||||
substrate. A worker is just a worker — it depends on `channels-call` and
|
||||
uses `ChannelClient` (ADR-080) to dial. The hub relay logic (ADR-079) lives
|
||||
in `alknet-hub`, not in a channels sub-crate.
|
||||
|
||||
### `alknet-channels-core`
|
||||
|
||||
The pure multiplexer. Contains:
|
||||
@@ -95,31 +99,38 @@ The call-protocol coupling. Contains:
|
||||
registered on the call protocol's `OperationRegistry` at assembly time.
|
||||
- `ChannelOperations` (the registration helper that closes over a
|
||||
`ChannelManager` clone).
|
||||
- `ChannelClient` (ADR-080) — the client-side type that dials a transport,
|
||||
establishes the channels connection, and exposes `open_channel(alpn,
|
||||
params) -> Channel`. This is the worker/client entry point; it lives here
|
||||
because it needs channel 0 pre-negotiation (which is in `channels-call`).
|
||||
|
||||
Depends on `channels-core` + `alknet-call`. This is where the
|
||||
call-protocol coupling lives, isolated from the pure multiplexer.
|
||||
|
||||
### `alknet-channels-hub` and `alknet-channels-worker`
|
||||
### Hub and worker are consumers, not sub-crates
|
||||
|
||||
The two roles. These may be sub-crates or feature-gated modules within
|
||||
`channels-call`; the exact packaging is a two-way-door implementation
|
||||
detail. The contract is:
|
||||
The hub and worker are architectural roles, not channels sub-crates:
|
||||
|
||||
- **`channels-hub`** (or the hub role): the relay (ADR-079). Translates
|
||||
`channel/open` on channel 0, byte-forwards data channels with
|
||||
`channel_id` rewrite. Depends on `channels-call` (for the call-protocol
|
||||
translation) + the hub's own `CallAdapter` / `from_call` machinery.
|
||||
- **`channels-worker`** (or the worker/client role): `ChannelClient`
|
||||
(ADR-080). Dials a transport, establishes the channels connection, runs
|
||||
the demux/mux, exposes `open_channel(alpn, params) -> Channel`. Depends
|
||||
on `channels-call` (for channel 0 pre-negotiation) + `alknet-call`'s
|
||||
`CallClient`-equivalent.
|
||||
- **The hub** is the existing `alknet-hub` crate. It depends on
|
||||
`channels-call` and uses the channels protocol as its substrate. The hub
|
||||
relay logic (ADR-079 — translate `channel/open` on channel 0,
|
||||
byte-forward data channels with `channel_id` rewrite) lives in
|
||||
`alknet-hub`, alongside its existing peer lifecycle, aggregated env, and
|
||||
service discovery responsibilities. There is no `channels-hub` sub-crate;
|
||||
`alknet-hub` IS the channels hub.
|
||||
|
||||
The naming (hub/worker vs server/client) is a two-way-door detail. The
|
||||
user noted "server/client" is muddy because a hub/worker can act as both
|
||||
depending on the use case (bidirectionality — ADR-073 §direction
|
||||
semantics). The roles are "the side that relays" (hub) and "the side that
|
||||
dials" (worker/client), but both can open channels in either direction.
|
||||
- **A worker** is any crate that uses `ChannelClient` (ADR-080, in
|
||||
`channels-call`) to dial a hub. There is no `channels-worker` sub-crate;
|
||||
a worker depends on `channels-call` and uses `ChannelClient` directly.
|
||||
The worker may be a CLI binary, a docker-side connector, an SSH-side
|
||||
connector, or any other role that dials into a hub's channels connection.
|
||||
|
||||
This means the channels crate provides the substrate (`channels-core` +
|
||||
`channels-call`); the hub and worker crates are consumers that build on it.
|
||||
The dependency direction is: `alknet-hub` → `channels-call` →
|
||||
`channels-core` → `alknet-core`; a worker → `channels-call` →
|
||||
`channels-core` → `alknet-core`. The channels crate has no dependency on
|
||||
`alknet-hub` or any worker crate.
|
||||
|
||||
### What moves where
|
||||
|
||||
@@ -134,22 +145,24 @@ dials" (worker/client), but both can open channels in either direction.
|
||||
| Channel 0 pre-negotiation | `alknet-channels` (ADR-072) | `channels-call` |
|
||||
| `channel/open`/`close`/`control`/`resources/subscribe` ops | `alknet-channels` (ADR-073) | `channels-call` |
|
||||
| `ChannelOperations` registration helper | `alknet-channels` | `channels-call` |
|
||||
| Hub relay (ADR-079) | `alknet-channels` (spec) / `alknet-hub` (impl) | `channels-hub` (or `alknet-hub` — see below) |
|
||||
| `ChannelClient` (ADR-080) | `alknet-channels` | `channels-worker` |
|
||||
| `ChannelClient` (ADR-080) | `alknet-channels` | `channels-call` |
|
||||
| Hub relay (ADR-079) | `alknet-channels` (spec) | `alknet-hub` (the existing hub crate, consuming `channels-call`) |
|
||||
|
||||
### Relationship to `alknet-hub`
|
||||
|
||||
The existing `alknet-hub` crate (`docs/architecture/crates/hub/README.md`)
|
||||
is the hub pattern: peer lifecycle, aggregated env, service discovery. The
|
||||
channels hub relay (ADR-079) is a channels-specific concern that the hub
|
||||
crate consumes. The split: `channels-hub` (or the hub role within
|
||||
`channels-call`) provides the relay logic; `alknet-hub` wires it into the
|
||||
hub runtime alongside the call-protocol peer management. Whether the relay
|
||||
lives in a `channels-hub` sub-crate or directly in `alknet-hub` is a
|
||||
packaging decision — the contract (ADR-079) is the same either way. The
|
||||
user's preference for decomposing channels into core/hub/worker suggests a
|
||||
`channels-hub` sub-crate that `alknet-hub` depends on, but this is not
|
||||
one-way and can be revisited during implementation.
|
||||
is the hub pattern: peer lifecycle, aggregated env, service discovery. With
|
||||
channels as the substrate, `alknet-hub` gains a dependency on
|
||||
`channels-call` and incorporates the relay logic (ADR-079). The hub spec
|
||||
(`crates/hub/README.md`) will be updated to reflect that the hub uses
|
||||
channels as its transport substrate — one channels connection per leg
|
||||
(browser↔hub, hub↔spoke), with the relay translating `channel/open` and
|
||||
byte-forwarding data channels. The hub's existing responsibilities (peer
|
||||
lifecycle, aggregated env, service discovery, worker supervision) are
|
||||
unchanged; channels is the substrate they run on.
|
||||
|
||||
This makes "channels hub" and "hub" the same thing — the hub IS built on
|
||||
channels. There is no separate channels-hub concept.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -162,20 +175,22 @@ one-way and can be revisited during implementation.
|
||||
`channels-core` doesn't know about `alknet-call`, `alknet-tty`, or any
|
||||
handler crate. The call-protocol coupling is a channels-crate concern,
|
||||
not a downstream-crate concern.
|
||||
- The sub-crate split matches the existing pattern (`alknet-tty` +
|
||||
`alknet-tty-local`, `alknet-docker` + its `tty` feature) — core in one
|
||||
crate, consumer-specific wiring in another.
|
||||
- The hub and worker roles are separated, matching the user's
|
||||
decomposition preference and the bidirectionality of the channels
|
||||
protocol (both sides can open channels; the roles are about who relays
|
||||
vs who dials, not about request/response direction).
|
||||
- Hub and worker are consumers, not sub-crates. The existing `alknet-hub`
|
||||
crate IS the channels hub — it depends on `channels-call` and uses
|
||||
channels as its substrate. A worker depends on `channels-call` and uses
|
||||
`ChannelClient`. The channels crate has no dependency on `alknet-hub` or
|
||||
any worker crate. This is the cleanest dependency direction: channels
|
||||
provides the substrate; hub and worker consume it.
|
||||
- The WASM and cross-platform story gets easier: `channels-core` is
|
||||
WASM-compatible by construction (pure byte manipulation, no platform
|
||||
deps); `channels-call` inherits the call protocol's WASM constraints; the
|
||||
hub and worker crates are platform-specific as needed.
|
||||
|
||||
**Negative:**
|
||||
- Three crates instead of one. The assembly layer must depend on
|
||||
`channels-core` + `channels-call` (and optionally `channels-hub` or
|
||||
`channels-worker`) instead of one `alknet-channels`. This is the cost of
|
||||
the clean separation; the assembly layer already wires multiple crates,
|
||||
so this is consistent with the existing pattern.
|
||||
- Two channels crates instead of one. The assembly layer must depend on
|
||||
`channels-core` + `channels-call` instead of one `alknet-channels`. This
|
||||
is the cost of the clean separation; the assembly layer already wires
|
||||
multiple crates, so this is consistent with the existing pattern.
|
||||
- The `ChannelsAdapter` in `channels-core` doesn't preinstall channel 0 —
|
||||
the consumer does. This means `channels-core`'s `ChannelsAdapter::handle`
|
||||
exposes a hook (callback or trait method) for the consumer to install
|
||||
@@ -186,12 +201,11 @@ one-way and can be revisited during implementation.
|
||||
|
||||
## Door type
|
||||
|
||||
**One-way (crate structure) + two-way (role packaging).** The three-crate
|
||||
split (`channels-core` / `channels-call` / roles) is one-way — once
|
||||
consumers depend on `channels-core` without `channels-call`, re-merging
|
||||
them is a breaking change. The hub/worker packaging (separate sub-crates
|
||||
vs feature-gated modules within `channels-call`) is two-way — an
|
||||
implementation detail that can change without breaking the contract.
|
||||
**One-way (crate structure).** The two-crate split (`channels-core` /
|
||||
`channels-call`) is one-way — once consumers depend on `channels-core`
|
||||
without `channels-call`, re-merging them is a breaking change. The hub and
|
||||
worker being consumers (not sub-crates) is also one-way — it establishes
|
||||
the dependency direction (hub/worker → channels, not channels → hub/worker).
|
||||
|
||||
## References
|
||||
|
||||
|
||||
Reference in new issue
Block a user