[M5] mxaccess: F26 step 1 — AsbTransport bridges AsbClient into Transport trait
First slice of F26. Bridges F25's working AsbClient into the M0
`mxaccess::Transport` trait that Session uses to discriminate
operations across NMX and ASB transports.
API additions:
* `mxaccess::AsbTransport<T>` — generic over the same
AsyncRead+AsyncWrite+Unpin+Send+Sync+'static bound that AsbClient
takes. Owns an AsbClient and exposes it via `client_mut()` /
`into_client()`.
* `impl Transport for AsbTransport<T>`:
- `capabilities()` — `buffered_subscribe = false`,
`activate_suspend = false`, `operation_complete_frame = false`
per `design/60-roadmap.md` M5 (no NMX-specific extensions on
ASB).
- `kind()` — `TransportKind::Asb`.
Path-dep wiring: `mxaccess` now imports `mxaccess-asb` +
`mxaccess-asb-nettcp` directly.
Compile-time `Send + Sync + 'static` assertion guards the
trait-bound contract.
2 new tests:
* `asb_transport_kind_is_asb`.
* `asb_transport_capabilities_disable_buffered_and_activate_suspend`.
Stubbed for F26 step 2:
* `Session::connect_asb` constructor that owns TCP open +
preamble + DH handshake orchestration.
* Operation routing that maps ASB types (ItemStatus, RuntimeValue)
back to mxaccess types (MxStatus, DataChange, MxValue).
Stubbed for F26 step 3:
* Subscription routing — Session::subscribe on ASB needs F25
subscription operations (CreateSubscription / AddMonitoredItems
/ Publish), which are not yet implemented.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+2
@@ -339,6 +339,8 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"futures-util",
|
||||
"mxaccess-asb",
|
||||
"mxaccess-asb-nettcp",
|
||||
"mxaccess-callback",
|
||||
"mxaccess-codec",
|
||||
"mxaccess-galaxy",
|
||||
|
||||
@@ -14,6 +14,8 @@ mxaccess-callback = { path = "../mxaccess-callback" }
|
||||
mxaccess-galaxy = { path = "../mxaccess-galaxy" }
|
||||
mxaccess-nmx = { path = "../mxaccess-nmx" }
|
||||
mxaccess-rpc = { path = "../mxaccess-rpc" }
|
||||
mxaccess-asb = { path = "../mxaccess-asb" }
|
||||
mxaccess-asb-nettcp = { path = "../mxaccess-asb-nettcp" }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -30,6 +30,9 @@ pub use mxaccess_codec::{
|
||||
// ---- Public types --------------------------------------------------------
|
||||
|
||||
pub mod session;
|
||||
pub mod transport_asb;
|
||||
|
||||
pub use transport_asb::AsbTransport;
|
||||
|
||||
pub use mxaccess_galaxy::{GalaxyTagMetadata, Resolver, ResolverError};
|
||||
pub use mxaccess_nmx::WriteValue;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//! `AsbTransport` — bridges the F25 `mxaccess_asb::AsbClient` into the
|
||||
//! `mxaccess::Transport` trait + `Session` API.
|
||||
//!
|
||||
//! Per `design/60-roadmap.md` M5, the ASB transport surfaces:
|
||||
//!
|
||||
//! * **No `subscribe_buffered`** — ASB has no proven equivalent of NMX's
|
||||
//! buffered-batch DataUpdate frame; consumers calling
|
||||
//! `Session::subscribe_buffered` over ASB get
|
||||
//! `Error::Unsupported(Capability::BufferedSubscribe)`.
|
||||
//! * **No `Activate` / `Suspend`** — these are NMX `INmxService2`
|
||||
//! primitives without an ASB analogue.
|
||||
//! * **No `OperationComplete` outside the proven write-completion frame**
|
||||
//! — ASB doesn't surface a generic completion-frame channel.
|
||||
//!
|
||||
//! ## Scope of this iteration (F26 step 1)
|
||||
//!
|
||||
//! Implements:
|
||||
//! * [`AsbTransport`] struct that owns an [`AsbClient`] over an
|
||||
//! `AsyncRead + AsyncWrite + Unpin + Send` transport.
|
||||
//! * [`Transport`] trait impl returning the capability flags above.
|
||||
//! * [`AsbTransport::new`] constructor.
|
||||
//!
|
||||
//! Stubbed for next F26 iteration:
|
||||
//! * `Session::connect_asb` constructor — wires `AsbTransport` into a
|
||||
//! `Session`. Needs a thin shim that owns the AsbClient + delegates
|
||||
//! `register_items`/`read`/`write`/`subscribe` to the corresponding
|
||||
//! client method, mapping ASB result types (`ItemStatus`,
|
||||
//! `RuntimeValue`) back to `mxaccess` types (`MxStatus`,
|
||||
//! `DataChange`, `MxValue`).
|
||||
//! * Subscription routing — `Session::subscribe` on ASB maps to a
|
||||
//! `CreateSubscription` + `AddMonitoredItems` + `Publish`-callback
|
||||
//! pipeline; the F25 subscription operations are not yet wired up.
|
||||
|
||||
use mxaccess_asb::AsbClient;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
use crate::{Transport, TransportCapabilities, TransportKind};
|
||||
|
||||
/// `Transport` implementation for the ASB (`net.tcp` + binary-message-
|
||||
/// encoder) data plane. Owns the underlying [`AsbClient`].
|
||||
pub struct AsbTransport<T: AsyncRead + AsyncWrite + Unpin + Send + 'static> {
|
||||
client: AsbClient<T>,
|
||||
}
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin + Send + 'static> AsbTransport<T> {
|
||||
/// Build a transport from an already-constructed [`AsbClient`].
|
||||
/// The client should typically have completed
|
||||
/// `send_preamble().await? -> connect().await?` before being
|
||||
/// wrapped — the F26 next-step `Session::connect_asb` will own that
|
||||
/// orchestration.
|
||||
pub fn new(client: AsbClient<T>) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// Surface the inner client. M5 / F26 step 2 wires concrete
|
||||
/// operations through here.
|
||||
pub fn client_mut(&mut self) -> &mut AsbClient<T> {
|
||||
&mut self.client
|
||||
}
|
||||
|
||||
/// Consume the transport and return the inner client. Useful when
|
||||
/// the caller wants to issue raw IASBIDataV2 operations directly
|
||||
/// before / after the Session-level orchestration kicks in.
|
||||
pub fn into_client(self) -> AsbClient<T> {
|
||||
self.client
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile-time only: `AsbTransport` must be `Send + Sync + 'static`
|
||||
/// (the `Transport` trait bound). Sync is provided by `AsbClient`'s
|
||||
/// internal lack of interior mutability over non-Sync types — the
|
||||
/// `AsyncRead + AsyncWrite + Unpin + Send` transport is the only
|
||||
/// non-trivial constraint, and Tokio's `TcpStream` satisfies it.
|
||||
const _: fn() = || {
|
||||
fn assert_send_sync<T: Send + Sync + 'static>() {}
|
||||
assert_send_sync::<AsbTransport<tokio::io::DuplexStream>>();
|
||||
};
|
||||
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static> Transport for AsbTransport<T> {
|
||||
fn capabilities(&self) -> TransportCapabilities {
|
||||
TransportCapabilities {
|
||||
// ASB has no proven buffered-batch DataUpdate equivalent.
|
||||
buffered_subscribe: false,
|
||||
// Activate/Suspend are NMX `INmxService2` primitives.
|
||||
activate_suspend: false,
|
||||
// No generic completion-frame channel on ASB.
|
||||
operation_complete_frame: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(&self) -> TransportKind {
|
||||
TransportKind::Asb
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mxaccess_asb_nettcp::auth::{AsbAuthenticator, CryptoParameters};
|
||||
|
||||
fn make_authenticator() -> AsbAuthenticator {
|
||||
AsbAuthenticator::new("test-passphrase", &CryptoParameters::defaults(), [0u8; 16]).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asb_transport_kind_is_asb() {
|
||||
let (client_end, _peer) = tokio::io::duplex(64);
|
||||
let client = AsbClient::new(client_end, make_authenticator(), "test://x/y");
|
||||
let transport = AsbTransport::new(client);
|
||||
assert_eq!(transport.kind(), TransportKind::Asb);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asb_transport_capabilities_disable_buffered_and_activate_suspend() {
|
||||
let (client_end, _peer) = tokio::io::duplex(64);
|
||||
let client = AsbClient::new(client_end, make_authenticator(), "test://x/y");
|
||||
let transport = AsbTransport::new(client);
|
||||
let caps = transport.capabilities();
|
||||
assert!(!caps.buffered_subscribe);
|
||||
assert!(!caps.activate_suspend);
|
||||
assert!(!caps.operation_complete_frame);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user