feat(websocket): ride alkcall 0.5.0 — OpenableAlpn establisher + timeout (ADR-049 Unit 3)

Mechanical pass for the alkcall 0.5.0 bump (review 006 sequencing:
ADR-049 → alkcall 0.5.0 → alkhttp mechanical pass):

- bump alkcall 0.4 → 0.5 (gateway feature unchanged)
- OpenableAlpn gains establisher: Option<OpenEstablisher> and
  establisher_timeout: Option<Duration> (both default None via
  OpenableAlpn::new — existing constructions compile unchanged) plus
  a with_establisher(establisher, timeout) builder
- the install_channel_zero ferry calls
  register_openable_with_establisher, threading both fields through
- docs updated (websocket.md, adapter.rs doc comments)

The alkcall 0.5.0 open_channel error-type change (ADR-049 §4) needs
no change here — this crate never calls open_channel (from_wss uses
ChannelClient::from_connection only).

Verification: cargo test (454 passed), cargo test --all-features
(587 passed), cargo clippy --all-targets/-D warnings (default +
all-features), cargo fmt --check
This commit is contained in:
2026-09-06 20:04:25 +00:00
parent 5f12abf80e
commit 80d6ba0e5a
5 changed files with 73 additions and 21 deletions
Generated
+5 -5
View File
@@ -27,9 +27,9 @@ dependencies = [
[[package]] [[package]]
name = "alkcall" name = "alkcall"
version = "0.4.1" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9badefe048a194c93eed09bc5326ebf092fc86817e561d88de2cd12b6302753a" checksum = "0bbaeb718c370b74f9136d1d59b4608249bd7d1dd2332c0259f98351d709737f"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@@ -1485,7 +1485,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -1860,7 +1860,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -2679,7 +2679,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
+1 -1
View File
@@ -42,7 +42,7 @@ h2 = ["server", "dep:hyper", "hyper-util/http2", "hyper/http2"]
http1 = ["server", "dep:hyper", "hyper-util/http1", "hyper/http1"] http1 = ["server", "dep:hyper", "hyper-util/http1", "hyper/http1"]
[dependencies] [dependencies]
alkcall = { version = "0.4", features = ["gateway"] } alkcall = { version = "0.5", features = ["gateway"] }
arc-swap = { version = "1", optional = true } arc-swap = { version = "1", optional = true }
axum = { version = "0.8", optional = true, features = ["ws"] } axum = { version = "0.8", optional = true, features = ["ws"] }
bytes = "1" bytes = "1"
+12 -3
View File
@@ -275,9 +275,18 @@ that motivated the alknet WebTransport track) workable over WS: the
SSH byte stream rides a data channel. SSH byte stream rides a data channel.
The openable set is declared with `HttpAdapter::with_ws_openable_alpns` The openable set is declared with `HttpAdapter::with_ws_openable_alpns`
(each `OpenableAlpn { spec, open_handler }`; the ALPN-specific handlers (each `OpenableAlpn { spec, open_handler, establisher,
stay in the ALPN crates — alkhttp ferries the registrations), with the establisher_timeout }`; the ALPN-specific handlers and establishers
`OpenableAlpns` request-extension fallback for bare-registry/custom stay in the ALPN crates — alkhttp ferries the registrations). The
establisher (alkcall 0.5.0 / ADR-049) is the awaited establishment
phase of the open op: `None` (the default) keeps the pre-0.5 shape —
the open replies as soon as the pump handler spawns; `Some`
semantically-validating hook dials/prepares the backend before the
reply, and a bounded failure resolves `channel:open_failed` with
`details.reason` (the channel never exists consumer-side). The
per-registration timeout override bounds the establisher when the
dispatch carries no deadline. The `OpenableAlpns` request-extension
fallback is available for bare-registry/custom
upgrade routes. Cap policy is the `ChannelsPolicy` extension (one upgrade routes. Cap policy is the `ChannelsPolicy` extension (one
instance consulted by both the open wrappers and the demux teardown). instance consulted by both the open wrappers and the demux teardown).
Peer-announced ops (`op/register`) land in the connection-local Peer-announced ops (`op/register`) land in the connection-local
+4 -3
View File
@@ -270,9 +270,10 @@ impl HttpAdapter {
/// The openable-ALPN set for WS sessions (WS-22, review 006 /// The openable-ALPN set for WS sessions (WS-22, review 006
/// Unit 2): one [`OpenableAlpn`](crate::websocket::OpenableAlpn) /// Unit 2): one [`OpenableAlpn`](crate::websocket::OpenableAlpn)
/// per openable data-channel ALPN — the open-op spec (with the /// per openable data-channel ALPN — the open-op spec (with the
/// `channel_open` marker) and the ALPN-specific /// `channel_open` marker), the ALPN-specific
/// [`OpenHandler`](alkcall::channels::operations::OpenHandler). /// [`OpenHandler`](alkcall::channels::operations::OpenHandler),
/// Each WS /// and the optional establisher + per-registration timeout
/// (ADR-049, threaded through with `None` defaults). Each WS
/// session's per-session fork registers the set (plus the generic /// session's per-session fork registers the set (plus the generic
/// channel ops, bootstrap discovery, and `op/register`), so a WS /// channel ops, bootstrap discovery, and `op/register`), so a WS
/// client can open data channels exactly as any channels consumer /// client can open data channels exactly as any channels consumer
+51 -9
View File
@@ -46,7 +46,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use alkcall::channels::adapter::ChannelsAdapter; use alkcall::channels::adapter::ChannelsAdapter;
use alkcall::channels::operations::{ChannelCore, OpenHandler}; use alkcall::channels::operations::{ChannelCore, OpenEstablisher, OpenHandler};
use alkcall::channels::policy::{ChannelLifecyclePolicy, NoCap}; use alkcall::channels::policy::{ChannelLifecyclePolicy, NoCap};
use alkcall::core::auth::{AuthContext, Identity}; use alkcall::core::auth::{AuthContext, Identity};
use alkcall::core::types::{Connection, ProtocolHandler}; use alkcall::core::types::{Connection, ProtocolHandler};
@@ -257,11 +257,13 @@ impl WsSessions {
/// One deployment-declared openable ALPN for the WS path (WS-22): /// One deployment-declared openable ALPN for the WS path (WS-22):
/// the per-ALPN open-op `OperationSpec` (with the `channel_open` /// the per-ALPN open-op `OperationSpec` (with the `channel_open`
/// marker set via `OperationSpec::with_channel_open`) and the /// marker set via `OperationSpec::with_channel_open`), the
/// ALPN-specific [`OpenHandler`] the data-plane protocol runs on the /// ALPN-specific [`OpenHandler`] the data-plane protocol runs on the
/// allocated channel's `Connection`. The ALPN-specific handler stays /// allocated channel's `Connection`, and the optional establishment
/// in the ALPN crates (alktty et al.); this crate only ferries the /// phase (ADR-049) the open-op wrapper awaits — bounded — before the
/// registration onto each session's fork. /// reply. The ALPN-specific handler and establisher stay in the ALPN
/// crates (alktty et al.); this crate only ferries the registration
/// onto each session's fork.
#[derive(Clone)] #[derive(Clone)]
pub struct OpenableAlpn { pub struct OpenableAlpn {
/// The open-op spec (Query/Mutation/Sub with the `channel_open` /// The open-op spec (Query/Mutation/Sub with the `channel_open`
@@ -271,12 +273,50 @@ pub struct OpenableAlpn {
/// The data-plane protocol handler spawned on the allocated /// The data-plane protocol handler spawned on the allocated
/// channel's `Connection`. /// channel's `Connection`.
pub open_handler: OpenHandler, pub open_handler: OpenHandler,
/// The awaited establishment phase (ADR-049 §1): validate params
/// semantically, consult ownership, prepare/dial the backend —
/// before the open reply. `None` (the default) = an always-OK
/// establisher (the pre-ADR-049 shape; existing registrations
/// behave unchanged).
pub establisher: Option<OpenEstablisher>,
/// The per-registration bound on the establisher await
/// (ADR-049 §2). `None` = [`ESTABLISHMENT_TIMEOUT`] (10s) when the
/// dispatch carries no deadline; the effective bound is the
/// earlier of the dispatch deadline and this override.
///
/// [`ESTABLISHMENT_TIMEOUT`]: alkcall::channels::operations::ESTABLISHMENT_TIMEOUT
pub establisher_timeout: Option<std::time::Duration>,
} }
impl OpenableAlpn { impl OpenableAlpn {
/// Declare one openable ALPN. /// Declare one openable ALPN (no establisher — the pre-ADR-049
/// shape; the open op replies as soon as the pump handler is
/// spawned).
pub fn new(spec: OperationSpec, open_handler: OpenHandler) -> Self { pub fn new(spec: OperationSpec, open_handler: OpenHandler) -> Self {
Self { spec, open_handler } Self {
spec,
open_handler,
establisher: None,
establisher_timeout: None,
}
}
/// Attach an establishment phase (ADR-049 §1): awaited by the
/// open-op wrapper — bounded by the dispatch deadline, this
/// crate's [`ESTABLISHMENT_TIMEOUT`] default, or
/// `timeout` when set — before the reply; on failure the open op
/// resolves `channel:open_failed` with `details.reason` and the
/// channel never exists consumer-side.
///
/// [`ESTABLISHMENT_TIMEOUT`]: alkcall::channels::operations::ESTABLISHMENT_TIMEOUT
pub fn with_establisher(
mut self,
establisher: OpenEstablisher,
timeout: Option<std::time::Duration>,
) -> Self {
self.establisher = Some(establisher);
self.establisher_timeout = timeout;
self
} }
} }
@@ -373,7 +413,7 @@ impl Drop for ConnectionGuard {
/// per-session ops on the fork — the generic channel lifecycle ops /// per-session ops on the fork — the generic channel lifecycle ops
/// (`ChannelOperations::register_on`: `channel/close`, /// (`ChannelOperations::register_on`: `channel/close`,
/// `channel/control`, `channel/resources/subscribe`), the /// `channel/control`, `channel/resources/subscribe`), the
/// deployment's openable ALPNs (`ChannelCore::register_openable`), /// deployment's openable ALPNs (`ChannelCore::register_openable_with_establisher`),
/// the bootstrap discovery set closed over the fork /// the bootstrap discovery set closed over the fork
/// (`install_bootstrap_discovery`, so `services/list` sees the /// (`install_bootstrap_discovery`, so `services/list` sees the
/// session's own openables — the F-06 shape), and `op/register` /// session's own openables — the F-06 shape), and `op/register`
@@ -464,11 +504,13 @@ fn install_channel_zero(
if let Some(openables) = openable_alpns.as_ref() { if let Some(openables) = openable_alpns.as_ref() {
let core = ChannelCore::new(manager, Arc::clone(&policy)); let core = ChannelCore::new(manager, Arc::clone(&policy));
for openable in openables.iter() { for openable in openables.iter() {
core.register_openable( core.register_openable_with_establisher(
openable.spec.clone(), openable.spec.clone(),
openable.establisher.clone(),
Arc::clone(&openable.open_handler), Arc::clone(&openable.open_handler),
&fork, &fork,
auth.clone(), auth.clone(),
openable.establisher_timeout,
)?; )?;
} }
} }