The BidiStreamSource trait (ADR-070) made Connection hold Box<dyn BidiStreamSource> so downstream crates can add connection shapes without editing core — but the public constructor for that path was missing. The source field is private; the channels crate (or any future crate) had no way to build a Connection from its own BidiStreamSource impl. The trait was unusable from outside core. Add Connection::from_source(source: impl BidiStreamSource, alpn: Vec<u8>) -> Self: the extension point. Takes impl BidiStreamSource (not Box<dyn>) to match the ergonomic style of from_stream / from_bidi; boxes the source internally. No feature gate (always available, like from_stream). Initializes alpn and identity: OnceLock::new() the same as the other constructors. Test: a custom RecordingSource BidiStreamSource impl (not a built-in) constructed via from_source, verifying remote_alpn / remote_addr / accept_bi (round-trips real bytes via tokio::io::duplex) / open_bi (StreamClosed) / close (records code+reason) all delegate to the custom impl. Verified: cargo test + clippy clean across default, --no-default-features, --features iroh, --all-features; cargo check -p alknet-call succeeds unchanged; cargo fmt --check passes.
6.2 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level |
|---|---|---|---|---|---|---|---|
| core/connection-from-source-constructor | Add Connection::from_source constructor (ADR-070 gap — the public extension point) | completed | narrow | low | component | implementation |
Description
ADR-070 made Connection hold Box<dyn BidiStreamSource> so downstream
crates can add connection shapes without editing core. The trait exists,
the three built-in impls exist, but the public constructor that lets a
downstream crate construct a Connection from its own BidiStreamSource
impl is missing. The source field is private; there is no way for
the channels crate (or any future crate) to build a Connection from a
ChannelBidiStreamSource — the trait is currently unusable from outside
core.
This is a gap in the ADR-070 implementation, not a new architectural
decision. ADR-070 says "the channels crate implements
ChannelBidiStreamSource in its own crate and constructs Connection
from it" (§Consequences), but the constructor for that path was never
added. The alknet-channels POC update surfaced this when it tried to use
the trait directly and found no from_source.
Current state (crates/alknet-core/src/types.rs)
pub struct Connection {
source: Box<dyn BidiStreamSource>, // private
alpn: Vec<u8>,
identity: OnceLock<Identity>,
}
impl Connection {
// from_quinn, from_quinn_with_alpn, from_iroh, from_stream, from_bidi
// — all wrap crate-private impls. No constructor takes a
// caller-supplied BidiStreamSource.
}
Target state
impl Connection {
/// Construct from a caller-supplied `BidiStreamSource` impl. The
/// extension point for downstream crates — implement the trait and
/// construct a `Connection` from it without editing core. See ADR-070.
pub fn from_source(source: impl BidiStreamSource, alpn: Vec<u8>) -> Self {
Self {
source: Box::new(source),
alpn,
identity: OnceLock::new(),
}
}
}
The signature takes impl BidiStreamSource (not Box<dyn BidiStreamSource>) to match the ergonomic style of from_stream /
from_bidi (which take impl AsyncWrite + Send + Unpin + 'static). The
trait already requires Send + Sync + 'static, so no extra bounds are
needed. The Box::new(source) is an internal implementation detail.
Why not just use from_stream
from_stream wraps a StreamBidiStreamSource (yield-once). A downstream
crate that implements BidiStreamSource for a multi-stream source (e.g.
channels' ChannelBidiStreamSource that yields one stream per channel)
cannot use from_stream — it needs from_source to wrap its own impl.
Using from_stream for a multi-stream source would give yield-once
behavior, which is wrong. This is exactly the gap the channels POC hit.
Test
Add a test that constructs a Connection from a minimal custom
BidiStreamSource impl (a mock that yields a fixed SendStream /
RecvStream pair) and verifies accept_bi / open_bi / remote_addr /
close delegate correctly. The test impl does not need to be realistic —
it just needs to prove the public constructor works end-to-end with a
non-built-in BidiStreamSource.
#[cfg(test)]
mod from_source_tests {
use super::*;
struct MockSource {
stream: Option<(SendStream, RecvStream)>,
addr: Option<SocketAddr>,
}
#[async_trait]
impl BidiStreamSource for MockSource {
async fn accept_bi(&self) -> Result<(SendStream, RecvStream), StreamError> {
// yield-once mock
}
async fn open_bi(&self) -> Result<(SendStream, RecvStream), StreamError> {
Err(StreamError::StreamClosed)
}
fn remote_addr(&self) -> Option<SocketAddr> { self.addr }
fn close(&self, _code: u32, _reason: &str) {}
}
#[tokio::test]
async fn from_source_delegates_to_custom_impl() {
let conn = Connection::from_source(
MockSource { stream: None, addr: None },
b"alknet/test".to_vec(),
);
assert_eq!(conn.remote_alpn(), b"alknet/test");
assert_eq!(conn.remote_addr(), None);
// accept_bi delegates to MockSource::accept_bi
}
}
(The test shape is illustrative — the implementer should make the mock
yield a real SendStream/RecvStream pair via tokio::io::duplex so
accept_bi returns a usable stream, and verify the stream round-trips.)
Acceptance Criteria
Connection::from_source(source: impl BidiStreamSource, alpn: Vec<u8>) -> Selfadded totypes.rsfrom_sourceispuband has no feature gate (always available, likefrom_stream)from_sourceboxes the source internally:source: Box::new(source)from_sourceinitializesalpnandidentity: OnceLock::new()(same as the other constructors)- Unit test: construct a
Connectionfrom a customBidiStreamSourceimpl viafrom_source, verifyremote_alpn/remote_addr/accept_bi/open_bi/closeall delegate to the custom impl - Existing tests pass unchanged (
from_quinn/from_iroh/from_streampaths are not affected) cargo test -p alknet-coresucceeds (all feature combos)cargo clippy -p alknet-coresucceeds with no warnings (all feature combos)cargo fmt --check -p alknet-corepasses
References
- docs/architecture/decisions/070-bidistreamsource-trait.md — ADR-070 (§Constructors, §Consequences — the "constructs
Connectionfrom it" clause that requires this constructor) - docs/architecture/crates/core/core-types.md —
Connectionimpl block (updated to includefrom_source), BidiStreamSource section, built-in implementations table
Notes
This is a gap fix, not a new feature. The ADR-070 design always intended the trait to be the extension point for downstream crates; the constructor is how a downstream crate uses that extension point. Without it, the trait is unusable from outside core — the channels crate cannot build a
ConnectionfromChannelBidiStreamSource, which is the entire reason ADR-070 exists. The fix is one constructor (~6 lines) + one test. The risk is low:from_sourceis additive, the existing constructors are untouched, and the constructor's body is the same field-init pattern the other four constructors already use.