docs(adr): 092 — remove Connection::from_stream, from_bidi is the only public constructor

The earlier draft kept from_stream as an 'escape hatch' for already-split
transports. That bakes the split into the constructor API — the same
split-leaf shape pushed one step earlier. The cleaner normalization: the
split never crosses a crate boundary as part of a constructor.

- Connection::from_bidi is the only public stream constructor.
- Connection::from_stream(send, recv, ...) is removed.
- The channels reassembly path joins MpscSendStream/MpscRecvStream itself
  via tokio::io::join (one line) and calls from_bidi.
- The call crate's 5 test stub sites do tokio::io::split(x) then
  from_stream — they become from_bidi(x) directly. The split was always
  gratuitous at the call site.
- SendStream::from_stream / RecvStream::from_stream (per-half boxing for
  into_sub_streams() and the SubStreamHandle leaves) are retained — not
  constructors that feed Connection.
This commit is contained in:
glm-5.2 committed 2026-07-18 03:37:22 +00:00
1 parent f8d4650dce
commit 528cfa0367
1 file changed
+79 -30
@@ -188,19 +188,54 @@ The `QuicStream` wrapper (`adapter.rs:271-314`, 44 lines) becomes
`BiStream::from_joined(send, recv)` — one line, in core, invisible to `BiStream::from_joined(send, recv)` — one line, in core, invisible to
handlers. The same applies to iroh. The join is no longer per-handler. handlers. The same applies to iroh. The join is no longer per-handler.
### `Connection::from_bidi` becomes the universal constructor ### `Connection::from_bidi` is the only public stream constructor;
`from_stream` is removed
Today `from_bidi` is a convenience wrapper that calls Today `from_bidi` is a convenience wrapper that calls
`tokio::io::split(stream)` then `from_stream(send, recv)`. With `tokio::io::split(stream)` then `from_stream(send, recv)`, and
`BiStream` as the leaf, `from_bidi` is the natural constructor — the `from_stream` bakes the split into the constructor API — the same
underlying `StreamBidiStreamSource::accept_bi` yields the `BiStream` split-leaf shape pushed one step earlier. With `BiStream` as the leaf,
once. `from_stream(send, recv)` is retained as the escape hatch for `from_bidi` is the only public constructor that takes a joined stream.
transports that arrive already split (e.g. the channels reassembly `Connection::from_stream(send, recv, ...)` is **removed**.
path, which produces separate `MpscSendStream` / `MpscRecvStream` and
wants to join them at construction time); it internally does The rule this normalizes: **the split never crosses a crate boundary
`tokio::io::join(send, recv)` then routes through `from_bidi`. Both as part of a constructor.** A crate that produces split halves
constructors are retained; `from_bidi` is the primary, `from_stream` naturally (the channels reassembly path, which produces
the escape hatch. `MpscSendStream` / `MpscRecvStream` as distinct async types) joins
them *itself* via `tokio::io::join(send, recv)` (one line) and calls
`from_bidi`. A crate that has a joined stream (`TcpStream`,
`TlsStream<TcpStream>`, `russh::Channel::into_stream()`,
`WsBidiStream`, even a test `DuplexStream`) calls `from_bidi` directly.
`Connection` only ever holds a `BiStream`. The split is a crate-internal
concern of wherever it naturally arises.
The existing `from_stream` call sites update mechanically:
- `crates/alknet-client/src/dial/tcp_tls.rs` already uses `from_bidi`
(no change).
- `crates/alknet-endpoint/src/accept/tcp_tls.rs` already uses
`from_bidi` (no change).
- The call crate's test stubs
(`call_client.rs:91`, `protocol/connection.rs:465`,
`protocol/dispatch.rs:465`, `protocol/adapter.rs:294`,
`client/from_call.rs:428`) today do
`tokio::io::split(x)` then `from_stream(send, recv, ...)` — they
become `from_bidi(x, ...)` directly, one call, no split.
- The channels reassembly path (per ADR-074, the future
`ChannelBidiStreamSource::accept_bi` impl) joins its
`MpscSendStream` / `MpscRecvStream` via `tokio::io::join` and calls
`from_bidi` — the join is in the channels crate (where the split
exists), not in the core constructor API.
- The core test at `types.rs:768` and the `from_source_tests` helper
become `from_bidi` calls (or construct `BiStream` directly via
`BiStream::from_joined`).
`SendStream::from_stream` / `RecvStream::from_stream` (the per-half
constructors, `types.rs:267` / `types.rs:289`) are **retained** — they
are the per-half boxing for `into_sub_streams()` (ADR-074) and the
channels reassembly path's `SubStreamHandle` leaves, not constructors
that feed `Connection`. The split lives where it is natural (channels
reassembly → `SubStreamHandle`), doesn't leak into `Connection`'s API.
### `SendStream` / `RecvStream` collapse to thin newtypes ### `SendStream` / `RecvStream` collapse to thin newtypes
@@ -364,6 +399,13 @@ redesign.
- **`Connection::from_quinn` / `from_iroh`** — preserved as - **`Connection::from_quinn` / `from_iroh`** — preserved as
convenience wrappers; internally wrap the `QuinnBidiStreamSource` / convenience wrappers; internally wrap the `QuinnBidiStreamSource` /
`IrohBidiStreamSource` whose `accept_bi` does the join. `IrohBidiStreamSource` whose `accept_bi` does the join.
- **`Connection::from_bidi`** — promoted to the only public stream
constructor. `Connection::from_stream` is removed (the split no
longer crosses a crate boundary as part of a constructor).
- **`SendStream::from_stream` / `RecvStream::from_stream`** (per-half
constructors) — retained, but only as the boxing for
`into_sub_streams()` and the channels reassembly path's
`SubStreamHandle` leaves. Not constructors that feed `Connection`.
- **The endpoint's accept loops** (quinn/iroh) — unchanged. - **The endpoint's accept loops** (quinn/iroh) — unchanged.
## Consequences ## Consequences
@@ -407,6 +449,13 @@ redesign.
send) = tokio::io::split(bidi)`), but it touches every handler. This send) = tokio::io::split(bidi)`), but it touches every handler. This
is the one-time cost of the unification; the alternative is is the one-time cost of the unification; the alternative is
per-handler wrappers forever. per-handler wrappers forever.
- Every `Connection::from_stream(send, recv, ...)` call site is
removed. The call crate's test stubs (5 sites) become `from_bidi`
calls. The channels reassembly path gains a one-line
`tokio::io::join` before `from_bidi`. No caller outside core and
the channels reassembly path was ever doing anything other than
`tokio::io::split` then `from_stream` — the split was always
gratuitous at the call site.
- ADR-070's `accept_bi` return shape is amended. ADR-070 explicitly - ADR-070's `accept_bi` return shape is amended. ADR-070 explicitly
preserved the split-pair shape to keep the `Connection` API verbatim; preserved the split-pair shape to keep the `Connection` API verbatim;
this ADR reverses that preservation. The trade is: one type change this ADR reverses that preservation. The trade is: one type change
@@ -458,16 +507,22 @@ compilable:
constructor. Change `BidiStreamSource::accept_bi` / `open_bi` return constructor. Change `BidiStreamSource::accept_bi` / `open_bi` return
types to `BiStream`. Update `QuinnBidiStreamSource` / types to `BiStream`. Update `QuinnBidiStreamSource` /
`IrohBidiStreamSource` / `StreamBidiStreamSource` impls to do the `IrohBidiStreamSource` / `StreamBidiStreamSource` impls to do the
join. `Connection::accept_bi` / `open_bi` delegate verbatim. This join. `Connection::accept_bi` / `open_bi` delegate verbatim. Remove
is a single-crate change; every `accept_bi` caller breaks `Connection::from_stream` (the split-pair constructor); promote
mechanically. `Connection::from_bidi` to the only public stream constructor.
2. **Update every handler's call sites.** `HttpAdapter::handle` This is a single-crate change; every `accept_bi` and `from_stream`
becomes 4 lines (drop `QuicStream`). `TtyAdapter::handle` calls caller breaks mechanically.
2. **Update every handler's `accept_bi` call sites and every
`from_stream` call site.** `HttpAdapter::handle` becomes 4 lines
(drop `QuicStream`). `TtyAdapter::handle` calls
`tokio::io::split(bidi)` for its pump halves (or uses `tokio::io::split(bidi)` for its pump halves (or uses
`into_sub_streams()` in channels mode — unchanged). The channels `into_sub_streams()` in channels mode — unchanged). The channels
POC's `TunnelHandler` and `EchoHandler` get the same POC's `TunnelHandler` and `EchoHandler` get the same
`tokio::io::split` treatment. `CallAdapter::handle` (wherever it `tokio::io::split` treatment. `CallAdapter::handle` (wherever it
consumes `accept_bi`) gets the same. consumes `accept_bi`) gets the same. The call crate's test stubs
(5 `from_stream` sites) become `from_bidi` calls (drop the
`tokio::io::split` they were doing immediately before). The channels
reassembly path gains a one-line `tokio::io::join` before `from_bidi`.
3. **Collapse `SendStream` / `RecvStream` to thin newtypes.** Remove 3. **Collapse `SendStream` / `RecvStream` to thin newtypes.** Remove
`SendStreamKind` / `RecvStreamKind` enums; the quinn/iroh `SendStreamKind` / `RecvStreamKind` enums; the quinn/iroh
constructors become thin-boxing. Used only by the channels constructors become thin-boxing. Used only by the channels
@@ -479,10 +534,10 @@ compilable:
largest single change and can land after (1)-(3) — the WS path is largest single change and can land after (1)-(3) — the WS path is
independent of the handler call-site updates. independent of the handler call-site updates.
5. **Update ADR-065, ADR-070, ADR-074, ADR-077** to reflect the 5. **Update ADR-065, ADR-070, ADR-074, ADR-077** to reflect the
`BiStream` return shape. ADR-065's `from_stream` / `BiStream` return shape. ADR-065's `from_stream` constructor is
`from_bidi` constructors are amended (`from_bidi` primary, removed; `from_bidi` is the only public stream constructor (the
`from_stream` escape hatch with internal join). ADR-070's rule: the split never crosses a crate boundary as part of a
`accept_bi` return type is amended. ADR-074's constructor). ADR-070's `accept_bi` return type is amended. ADR-074's
`ChannelBidiStreamSource::accept_bi` return type is amended; `ChannelBidiStreamSource::accept_bi` return type is amended;
`into_sub_streams()` is unchanged. ADR-077's two-mode TTY design is `into_sub_streams()` is unchanged. ADR-077's two-mode TTY design is
unchanged (the modes differ in *how* the adapter gets sub-streams, unchanged (the modes differ in *how* the adapter gets sub-streams,
@@ -515,13 +570,6 @@ fix (unify the leaf so the wrapper moves into core).
`into_sub_streams()`). Default: stay in `alknet-core` for now (the `into_sub_streams()`). Default: stay in `alknet-core` for now (the
channels crate is not yet extracted); revisit at the channels channels crate is not yet extracted); revisit at the channels
extraction. extraction.
- **`from_stream` vs `from_bidi` primacy.** `from_bidi` is the
primary constructor (matches the ecosystem convention); `from_stream`
is the escape hatch for already-split transports (channels
reassembly). The naming is slightly asymmetric (`from_bidi` is
primary but `from_stream` sounds more general). Default: keep both
names; `from_bidi` is the documented primary, `from_stream` carries
the "already split" doc note. Renaming is a two-way door.
## References ## References
@@ -529,8 +577,9 @@ fix (unify the leaf so the wrapper moves into core).
trait is removed, the bounds survive as implied bounds on the trait is removed, the bounds survive as implied bounds on the
concrete struct) concrete struct)
- ADR-065: `Connection::from_stream` / `from_bidi` (amended — - ADR-065: `Connection::from_stream` / `from_bidi` (amended —
`from_bidi` is the primary constructor, `from_stream` is the `from_stream` is removed; `from_bidi` is the only public stream
escape hatch with an internal join) constructor; the split never crosses a crate boundary as part of a
constructor)
- ADR-070: `BidiStreamSource` trait (amended — `accept_bi` / `open_bi` - ADR-070: `BidiStreamSource` trait (amended — `accept_bi` / `open_bi`
return `BiStream`, not the split pair; the trait shape and the return `BiStream`, not the split pair; the trait shape and the
`from_source` extension point are preserved) `from_source` extension point are preserved)