Files
alknet/tasks/core/connection-from-source-constructor.md
T
glm-5.2 e8bbc7465f refactor(core): add Connection::from_source public constructor (ADR-070 gap)
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.
2026-07-12 11:07:17 +00:00

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>) -> Self added to types.rs
  • from_source is pub and has no feature gate (always available, like from_stream)
  • from_source boxes the source internally: source: Box::new(source)
  • from_source initializes alpn and identity: OnceLock::new() (same as the other constructors)
  • Unit test: construct a Connection from a custom BidiStreamSource impl via from_source, verify remote_alpn / remote_addr / accept_bi / open_bi / close all delegate to the custom impl
  • Existing tests pass unchanged (from_quinn / from_iroh / from_stream paths are not affected)
  • cargo test -p alknet-core succeeds (all feature combos)
  • cargo clippy -p alknet-core succeeds with no warnings (all feature combos)
  • cargo fmt --check -p alknet-core passes

References

  • docs/architecture/decisions/070-bidistreamsource-trait.md — ADR-070 (§Constructors, §Consequences — the "constructs Connection from it" clause that requires this constructor)
  • docs/architecture/crates/core/core-types.md — Connection impl block (updated to include from_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 Connection from ChannelBidiStreamSource, which is the entire reason ADR-070 exists. The fix is one constructor (~6 lines) + one test. The risk is low: from_source is additive, the existing constructors are untouched, and the constructor's body is the same field-init pattern the other four constructors already use.