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:
@@ -0,0 +1,251 @@
|
||||
# ADR-044: alknet-channels Sub-Crate Decomposition
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (amended 2026-07-18 by ADR-035 — the 9-byte wire format is now
|
||||
8-byte; `ChannelSubStreams` / `SubStreamHandle` removed; the channels
|
||||
layer has no `stream_type` concept — see "Amendment (ADR-035, 2026-07-18)"
|
||||
below)
|
||||
|
||||
## Amendment (ADR-035, 2026-07-18)
|
||||
|
||||
The wire format in `channels-core` is now **8-byte** (not 9-byte); the
|
||||
`ChannelSubStreams` / `SubStreamHandle` typed destructure accessor is
|
||||
**removed** (the channels layer has no `stream_type` concept —
|
||||
`accept_bi` yields a `BiStream`, and the handler owns its sub-stream
|
||||
multiplexing). The channel 0 pre-negotiation in `channels-call` no
|
||||
longer constructs "reassembly buffers with `stream_types` [0, 1]" — it
|
||||
constructs one reassembly buffer for `channel_id = 0`, yielding a
|
||||
`BiStream` to the `CallAdapter`. The "What moves where" table's "9-byte
|
||||
wire format" row is now "8-byte wire format"; the `ChannelSubStreams`
|
||||
row is removed. The two-crate split (`channels-core` /
|
||||
`channels-call`), the dep graph, and the "hub and worker are consumers"
|
||||
principle are unchanged. See ADR-035 for the resolution rationale.
|
||||
|
||||
## Context
|
||||
|
||||
The initial channels spec (ADR-034-080) framed `alknet-channels` as a
|
||||
single crate depending on both `alknet-core` and `alknet-call`. The
|
||||
dependency on `alknet-call` arises because channel lifecycle operations
|
||||
(`channel/open`, `channel/close`, `channel/control`,
|
||||
`channel/resources/subscribe`) register on the call protocol's
|
||||
`OperationRegistry`, and channel 0 is pre-negotiated as `alknet/call`
|
||||
(ADR-036).
|
||||
|
||||
This creates two issues:
|
||||
|
||||
1. **The dependency graph conflates the pure multiplexer with the call-
|
||||
protocol coupling.** The wire format, demux/mux, and
|
||||
`ChannelBidiStreamSource` are ALPN-blind and call-protocol-blind — they
|
||||
depend on `alknet-core` only. The channel 0 pre-negotiation and lifecycle
|
||||
op registrations are the call-protocol coupling. Baking both into one
|
||||
crate means any consumer that wants the multiplexer also pulls in the
|
||||
call-protocol coupling, even if they don't need channel 0 to be
|
||||
`alknet/call`.
|
||||
|
||||
2. **The "no special-casing for downstream crates" principle is
|
||||
violated.** The user's constraint: "we don't want to be doing anything
|
||||
special for downstream crates unless it's needed/useful to the point of
|
||||
they all kind of benefit." The call-protocol coupling is a channels-crate
|
||||
concern (channels needs call for orchestration), not a downstream-crate
|
||||
concern. Separating them makes the dependency graph honest: the pure
|
||||
multiplexer has no opinion about channel 0; the call-protocol coupling
|
||||
is isolated where it belongs.
|
||||
|
||||
The substrate simplification (ADR-034 revised) strengthened this: the wire
|
||||
format is the same across all substrates, and the `ChannelsAdapter` is
|
||||
substrate-agnostic. The pure multiplexer (`channels-core`) is cleanly
|
||||
separable from the call-protocol orchestration (`channels-call`).
|
||||
|
||||
## Decision
|
||||
|
||||
### 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.
|
||||
```
|
||||
|
||||
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-043) to dial. The hub relay logic (ADR-042) lives
|
||||
in `alknet-hub`, not in a channels sub-crate.
|
||||
|
||||
### `alknet-channels-core`
|
||||
|
||||
The pure multiplexer. Contains:
|
||||
|
||||
- The 9-byte chunk wire format (`parse_header` / `write_header` — ADR-034)
|
||||
- The demux/mux (`Demux`, `MuxHandle`/`MuxRunner` — ADR-039)
|
||||
- `ChannelBidiStreamSource` (implements `BidiStreamSource` — ADR-038)
|
||||
- `ChannelSubStreams` / `SubStreamHandle` (the typed destructure accessor)
|
||||
- `ChannelManager` (the shared state — channel_id → ChannelState, but
|
||||
**without** the `call_ops: Arc<OperationRegistry>` field; the manager is
|
||||
ALPN-blind and call-protocol-blind)
|
||||
- `ChannelsAdapter` (the `ProtocolHandler` on `alknet/channels` — the
|
||||
read/demux loop, substrate-agnostic per ADR-034 revised)
|
||||
|
||||
Depends on `alknet-core` only. No `alknet-call` dependency. No opinion about
|
||||
what channel 0 carries — that's the consumer's concern.
|
||||
|
||||
The `ChannelsAdapter::handle` in `channels-core` does NOT preinstall channel
|
||||
0 as `alknet/call`. It runs the demux loop and routes chunks by
|
||||
`channel_id`. Channel 0 is just another channel; what ALPN it carries is
|
||||
determined by the consumer (the `channels-call` crate pre-negotiates it as
|
||||
`alknet/call`; a hypothetical other consumer could pre-negotiate it
|
||||
differently).
|
||||
|
||||
### `alknet-channels-call`
|
||||
|
||||
The call-protocol coupling. Contains:
|
||||
|
||||
- Channel 0 pre-negotiation as `alknet/call` (ADR-036) — the
|
||||
`preinstall_channel_0` logic that constructs channel 0's reassembly
|
||||
buffers with stream_types [0, 1] and hands the `Connection` to the
|
||||
`CallAdapter`.
|
||||
- The four lifecycle operations (ADR-037): `channel/open`,
|
||||
`channel/close`, `channel/control`, `channel/resources/subscribe` —
|
||||
registered on the call protocol's `OperationRegistry` at assembly time.
|
||||
- `ChannelOperations` (the registration helper that closes over a
|
||||
`ChannelManager` clone).
|
||||
- `ChannelClient` (ADR-043) — 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.
|
||||
|
||||
### Hub and worker are consumers, not sub-crates
|
||||
|
||||
The hub and worker are architectural roles, not channels sub-crates:
|
||||
|
||||
- **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-042 — 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.
|
||||
|
||||
- **A worker** is any crate that uses `ChannelClient` (ADR-043, 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
|
||||
|
||||
| Component | Original (ADR-034-080) | Now |
|
||||
|-----------|------------------------|-----|
|
||||
| 9-byte wire format | `alknet-channels` | `channels-core` |
|
||||
| Demux/Mux | `alknet-channels` | `channels-core` |
|
||||
| `ChannelBidiStreamSource` | `alknet-channels` | `channels-core` |
|
||||
| `ChannelSubStreams` | `alknet-channels` | `channels-core` |
|
||||
| `ChannelManager` (without `call_ops`) | `alknet-channels` | `channels-core` |
|
||||
| `ChannelsAdapter` (demux loop only) | `alknet-channels` | `channels-core` |
|
||||
| Channel 0 pre-negotiation | `alknet-channels` (ADR-036) | `channels-call` |
|
||||
| `channel/open`/`close`/`control`/`resources/subscribe` ops | `alknet-channels` (ADR-037) | `channels-call` |
|
||||
| `ChannelOperations` registration helper | `alknet-channels` | `channels-call` |
|
||||
| `ChannelClient` (ADR-043) | `alknet-channels` | `channels-call` |
|
||||
| Hub relay (ADR-042) | `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. With
|
||||
channels as the substrate, `alknet-hub` gains a dependency on
|
||||
`channels-call` and incorporates the relay logic (ADR-042). 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
|
||||
|
||||
**Positive:**
|
||||
- The dependency graph is honest: `channels-core` is the pure multiplexer
|
||||
with no call dependency; the call-protocol coupling is isolated in
|
||||
`channels-call`. A consumer that wants the multiplexer without the
|
||||
call-protocol orchestration can depend on `channels-core` only.
|
||||
- The "no special-casing for downstream crates" principle is preserved:
|
||||
`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.
|
||||
- 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:**
|
||||
- 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
|
||||
channel 0. `channels-call` provides the `preinstall_channel_0`
|
||||
implementation; a different consumer could provide a different one. This
|
||||
is a slightly more complex adapter shape than "channel 0 is always call,"
|
||||
but it's the cost of the clean separation.
|
||||
|
||||
## Door type
|
||||
|
||||
**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
|
||||
|
||||
- ADR-031: crate decomposition (no-handler-depends-on-another-handler —
|
||||
preserved; the channels sub-crates depend on core/call, not on handlers)
|
||||
- ADR-034: channels wire format (revised — substrate simplification; the
|
||||
wire format is in `channels-core`; **amended by ADR-035 — 8-byte header,
|
||||
no `stream_type`**)
|
||||
- ADR-035: channels pure channel multiplexing (amends this ADR — 8-byte
|
||||
wire format; `ChannelSubStreams` / `SubStreamHandle` removed; the
|
||||
channels layer has no `stream_type` concept)
|
||||
- ADR-036: channel 0 pre-negotiated (moves to `channels-call`)
|
||||
- ADR-037: channel lifecycle operations (move to `channels-call`)
|
||||
- ADR-038: ChannelBidiStreamSource (in `channels-core`; **amended by
|
||||
ADR-035 — `into_sub_streams` removed, `accept_bi` yields `BiStream`**)
|
||||
- ADR-039: ChannelsAdapter and ChannelManager (split: core demux in
|
||||
`channels-core`, call coupling in `channels-call`)
|
||||
- ADR-042: hub relay (in `alknet-hub` — the hub crate consumes
|
||||
`channels-call`; there is no `channels-hub` sub-crate, per this ADR's
|
||||
Decision §"No hub/worker sub-crates")
|
||||
- ADR-043: ChannelClient (in `channels-call`; there is no
|
||||
`channels-worker` sub-crate — a worker is any crate that uses
|
||||
`ChannelClient` to dial)
|
||||
- `docs/architecture/crates/hub/README.md` — the existing hub crate (the
|
||||
relay's consumer)
|
||||
Reference in New Issue
Block a user