Merge branch 'wt/review-002-cov13-dead-code'
# Conflicts: # src/gateway/dispatch.rs
This commit is contained in:
+20
-71
@@ -508,7 +508,6 @@ pub fn to_mcp_service(dispatch: Arc<GatewayDispatch>) -> ToMcpService {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alkcall::core::auth::{AuthToken, IdentityProvider};
|
||||
use alkcall::core::types::Capabilities;
|
||||
use alkcall::registry::context::{OperationContext, ScopedPeerEnv};
|
||||
use alkcall::registry::discovery::{
|
||||
@@ -522,41 +521,6 @@ mod tests {
|
||||
use futures::StreamExt;
|
||||
use rmcp::model::Extensions;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
struct StaticIdentityProvider {
|
||||
tokens: StdMutex<HashMap<String, Identity>>,
|
||||
}
|
||||
|
||||
impl StaticIdentityProvider {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
tokens: StdMutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_token(self, token: &str, identity: Identity) -> Self {
|
||||
self.tokens
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.insert(token.to_string(), identity);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IdentityProvider for StaticIdentityProvider {
|
||||
fn resolve_from_fingerprint(&self, _fp: &str) -> Option<Identity> {
|
||||
None
|
||||
}
|
||||
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
|
||||
let token_str = String::from_utf8_lossy(&token.raw);
|
||||
self.tokens
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(token_str.as_ref())
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
fn identity_with_scopes(id: &str, scopes: &[&str]) -> Identity {
|
||||
Identity {
|
||||
@@ -681,15 +645,8 @@ mod tests {
|
||||
Arc::new(dispatch_registry)
|
||||
}
|
||||
|
||||
fn dispatch(
|
||||
registry: Arc<OperationRegistry>,
|
||||
provider: Arc<dyn alkcall::core::auth::IdentityProvider>,
|
||||
) -> Arc<GatewayDispatch> {
|
||||
Arc::new(GatewayDispatch::new(registry, provider))
|
||||
}
|
||||
|
||||
fn provider() -> Arc<dyn alkcall::core::auth::IdentityProvider> {
|
||||
Arc::new(StaticIdentityProvider::new())
|
||||
fn dispatch(registry: Arc<OperationRegistry>) -> Arc<GatewayDispatch> {
|
||||
Arc::new(GatewayDispatch::new(registry))
|
||||
}
|
||||
|
||||
fn extensions_with_identity(identity: Option<Identity>) -> Extensions {
|
||||
@@ -734,7 +691,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_tools_returns_exactly_four_gateway_tools() {
|
||||
let _gateway = ToMcpGateway::new(dispatch(full_registry_with_ops(vec![]), provider()));
|
||||
let _gateway = ToMcpGateway::new(dispatch(full_registry_with_ops(vec![])));
|
||||
let tools = gateway_tools();
|
||||
let names: Vec<String> = tools.iter().map(|t| t.name.to_string()).collect();
|
||||
assert_eq!(names.len(), 4);
|
||||
@@ -751,7 +708,7 @@ mod tests {
|
||||
OperationType::Query,
|
||||
AccessControl::default(),
|
||||
)]);
|
||||
let _gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let _gateway = ToMcpGateway::new(dispatch(registry));
|
||||
let tools = gateway_tools();
|
||||
for tool in &tools {
|
||||
assert_ne!(tool.name, "fs/readFile");
|
||||
@@ -783,9 +740,7 @@ mod tests {
|
||||
AccessControl::default(),
|
||||
),
|
||||
]);
|
||||
let idp: Arc<dyn alkcall::core::auth::IdentityProvider> =
|
||||
Arc::new(StaticIdentityProvider::new());
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, idp));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let result = invoke_tool(
|
||||
&gateway,
|
||||
@@ -839,7 +794,7 @@ mod tests {
|
||||
AccessControl::default(),
|
||||
),
|
||||
]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let result = invoke_tool(&gateway, "search", None, None).await;
|
||||
assert_eq!(result.is_error, Some(false));
|
||||
@@ -877,7 +832,7 @@ mod tests {
|
||||
AccessControl::default(),
|
||||
),
|
||||
]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert("query".to_string(), Value::String("fs".to_string()));
|
||||
@@ -906,7 +861,7 @@ mod tests {
|
||||
OperationType::Query,
|
||||
AccessControl::default(),
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert("name".to_string(), Value::String("fs/readFile".to_string()));
|
||||
@@ -932,7 +887,7 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert("name".to_string(), Value::String("fs/readFile".to_string()));
|
||||
@@ -962,7 +917,7 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert(
|
||||
@@ -1033,7 +988,7 @@ mod tests {
|
||||
Capabilities::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let gateway = ToMcpGateway::new(dispatch(Arc::new(dispatch_registry), provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(Arc::new(dispatch_registry)));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert("name".to_string(), Value::String("secret/op".to_string()));
|
||||
@@ -1072,7 +1027,7 @@ mod tests {
|
||||
OperationType::Query,
|
||||
AccessControl::default(),
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert(
|
||||
@@ -1101,7 +1056,7 @@ mod tests {
|
||||
OperationType::Query,
|
||||
AccessControl::default(),
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert(
|
||||
@@ -1122,7 +1077,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn call_argument_errors_carry_retryable_and_truthy_messages() {
|
||||
let registry = full_registry_with_ops(vec![]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let missing = invoke_tool(&gateway, "call", None, None).await;
|
||||
assert_eq!(missing.is_error, Some(true));
|
||||
@@ -1160,7 +1115,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn call_returns_structured_error_for_call_error() {
|
||||
let registry = full_registry_with_ops(vec![]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert(
|
||||
@@ -1255,7 +1210,7 @@ mod tests {
|
||||
OperationType::Query,
|
||||
AccessControl::default(),
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert(
|
||||
@@ -1312,9 +1267,7 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
)]);
|
||||
let idp: Arc<dyn alkcall::core::auth::IdentityProvider> =
|
||||
Arc::new(StaticIdentityProvider::new());
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, idp));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let mut args = Map::new();
|
||||
args.insert(
|
||||
@@ -1333,7 +1286,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_tool_name_returns_not_found_structured_error() {
|
||||
let gateway = ToMcpGateway::new(dispatch(Arc::new(OperationRegistry::new()), provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(Arc::new(OperationRegistry::new())));
|
||||
let result = invoke_tool(&gateway, "bogus", None, None).await;
|
||||
assert_eq!(result.is_error, Some(true));
|
||||
let structured = result.structured_content.expect("structured error present");
|
||||
@@ -1353,11 +1306,7 @@ mod tests {
|
||||
..Default::default()
|
||||
},
|
||||
)]);
|
||||
let idp: Arc<dyn alkcall::core::auth::IdentityProvider> = Arc::new(
|
||||
StaticIdentityProvider::new()
|
||||
.with_token("alk_admin", identity_with_scopes("admin-peer", &["admin"])),
|
||||
);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, idp));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let admin_identity = identity_with_scopes("admin-peer", &["admin"]);
|
||||
let extensions = extensions_with_identity(Some(admin_identity.clone()));
|
||||
@@ -1431,7 +1380,7 @@ mod tests {
|
||||
OperationType::Query,
|
||||
AccessControl::default(),
|
||||
)]);
|
||||
let gateway = ToMcpGateway::new(dispatch(registry, provider()));
|
||||
let gateway = ToMcpGateway::new(dispatch(registry));
|
||||
|
||||
let search_result = invoke_tool(&gateway, "search", None, None).await;
|
||||
let ops = search_result
|
||||
|
||||
+16
-58
@@ -3,8 +3,9 @@
|
||||
//!
|
||||
//! Thin concrete struct (not a trait — the alknet research ruled out a
|
||||
//! trait with an associated output type). Holds `Arc<OperationRegistry>`
|
||||
//! and `Arc<dyn IdentityProvider>` and exposes a `resolve_bearer()` and
|
||||
//! `invoke()` method pair returning the neutral `ResponseEnvelope`. Each
|
||||
//! and exposes an `invoke()` family returning the neutral
|
||||
//! `ResponseEnvelope` — identity is supplied per-call, resolved upstream
|
||||
//! in the auth middleware. Each
|
||||
//! gateway maps the envelope to its own wire shape (`to_openapi` → HTTP
|
||||
//! `Response`, `to_mcp` → `CallToolResult`).
|
||||
//!
|
||||
@@ -34,7 +35,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use alkcall::core::auth::{AuthToken, Identity, IdentityProvider};
|
||||
use alkcall::core::auth::Identity;
|
||||
use alkcall::core::types::Capabilities;
|
||||
use alkcall::protocol::wire::{CallError, ResponseEnvelope};
|
||||
use alkcall::registry::context::{AbortPolicy, OperationContext, ScopedPeerEnv};
|
||||
@@ -45,25 +46,21 @@ use serde_json::Value;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// The shared dispatch spine: registry + identity provider, wired for
|
||||
/// The shared dispatch spine over the registry, invoking operations for
|
||||
/// the neutral `ResponseEnvelope` result shape both gateway projections
|
||||
/// map to their wire formats.
|
||||
/// map to their wire formats. Identity arrives per-call as
|
||||
/// `Option<Identity>` — bearer resolution happens upstream in the auth
|
||||
/// middleware, not here.
|
||||
pub struct GatewayDispatch {
|
||||
registry: Arc<OperationRegistry>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
invoke_count: AtomicUsize,
|
||||
}
|
||||
|
||||
impl GatewayDispatch {
|
||||
/// Assemble a dispatch spine over a registry and an identity
|
||||
/// provider.
|
||||
pub fn new(
|
||||
registry: Arc<OperationRegistry>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
) -> Self {
|
||||
/// Assemble a dispatch spine over a registry.
|
||||
pub fn new(registry: Arc<OperationRegistry>) -> Self {
|
||||
Self {
|
||||
registry,
|
||||
identity_provider,
|
||||
invoke_count: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
@@ -73,11 +70,6 @@ impl GatewayDispatch {
|
||||
&self.registry
|
||||
}
|
||||
|
||||
/// The identity provider bearer tokens resolve against.
|
||||
pub fn identity_provider(&self) -> &Arc<dyn IdentityProvider> {
|
||||
&self.identity_provider
|
||||
}
|
||||
|
||||
/// How many [`GatewayDispatch::invoke`] calls this spine has
|
||||
/// served. A test-spy accessor: the over-cap batch tests assert it
|
||||
/// stays at zero to prove no dispatch happened before the cap
|
||||
@@ -86,11 +78,6 @@ impl GatewayDispatch {
|
||||
self.invoke_count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Resolve a bearer token to an identity (the auth-middleware hook).
|
||||
pub fn resolve_bearer(&self, token: &AuthToken) -> Option<Identity> {
|
||||
self.identity_provider.resolve_from_token(token)
|
||||
}
|
||||
|
||||
/// Invoke a Query/Mutation op under the 30 s gateway deadline; a
|
||||
/// hung handler surfaces as a `TIMEOUT` error envelope (504).
|
||||
pub async fn invoke(
|
||||
@@ -233,33 +220,10 @@ mod tests {
|
||||
};
|
||||
use alkcall::registry::spec::{AccessControl, OperationSpec, OperationType, Visibility};
|
||||
use futures::StreamExt;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
// The tests below run against the module's real 30 s deadline; only
|
||||
// the elapsed-time bound (< 60 s, anti-flake) is asserted by hand.
|
||||
|
||||
struct StaticIdentityProvider {
|
||||
tokens: StdMutex<HashMap<String, Identity>>,
|
||||
}
|
||||
|
||||
impl StaticIdentityProvider {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
tokens: StdMutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IdentityProvider for StaticIdentityProvider {
|
||||
fn resolve_from_fingerprint(&self, _fp: &str) -> Option<Identity> {
|
||||
None
|
||||
}
|
||||
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity> {
|
||||
let token_str = String::from_utf8_lossy(&token.raw);
|
||||
self.tokens.lock().unwrap().get(token_str.as_ref()).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
fn spec(name: &str, visibility: Visibility, op_type: OperationType) -> OperationSpec {
|
||||
OperationSpec::new(
|
||||
name,
|
||||
@@ -292,8 +256,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn invoke_external_op_round_trips() {
|
||||
let dispatch =
|
||||
GatewayDispatch::new(echo_registry(), Arc::new(StaticIdentityProvider::new()));
|
||||
let dispatch = GatewayDispatch::new(echo_registry());
|
||||
let envelope = dispatch
|
||||
.invoke(None, "/echo/run", serde_json::json!({ "x": 1 }))
|
||||
.await;
|
||||
@@ -302,8 +265,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn invoke_unknown_op_returns_not_found() {
|
||||
let dispatch =
|
||||
GatewayDispatch::new(echo_registry(), Arc::new(StaticIdentityProvider::new()));
|
||||
let dispatch = GatewayDispatch::new(echo_registry());
|
||||
let envelope = dispatch
|
||||
.invoke(None, "/missing/op", serde_json::json!({}))
|
||||
.await;
|
||||
@@ -328,8 +290,7 @@ mod tests {
|
||||
Capabilities::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let dispatch =
|
||||
GatewayDispatch::new(Arc::new(registry), Arc::new(StaticIdentityProvider::new()));
|
||||
let dispatch = GatewayDispatch::new(Arc::new(registry));
|
||||
let envelope = dispatch
|
||||
.invoke(None, "/internal/op", serde_json::json!({}))
|
||||
.await;
|
||||
@@ -357,8 +318,7 @@ mod tests {
|
||||
Capabilities::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let dispatch =
|
||||
GatewayDispatch::new(Arc::new(registry), Arc::new(StaticIdentityProvider::new()));
|
||||
let dispatch = GatewayDispatch::new(Arc::new(registry));
|
||||
let started = std::time::Instant::now();
|
||||
let envelope = dispatch
|
||||
.invoke(None, "/hung/op", serde_json::json!({}))
|
||||
@@ -379,8 +339,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn invoke_completes_within_the_deadline_for_a_fast_handler() {
|
||||
let dispatch =
|
||||
GatewayDispatch::new(echo_registry(), Arc::new(StaticIdentityProvider::new()));
|
||||
let dispatch = GatewayDispatch::new(echo_registry());
|
||||
let envelope = dispatch
|
||||
.invoke(None, "/echo/run", serde_json::json!({}))
|
||||
.await;
|
||||
@@ -406,8 +365,7 @@ mod tests {
|
||||
Capabilities::new(),
|
||||
))
|
||||
.unwrap();
|
||||
let dispatch =
|
||||
GatewayDispatch::new(Arc::new(registry), Arc::new(StaticIdentityProvider::new()));
|
||||
let dispatch = GatewayDispatch::new(Arc::new(registry));
|
||||
let mut stream =
|
||||
dispatch.invoke_streaming(None, "/tick/stream", serde_json::json!({ "count": 3 }));
|
||||
let mut ticks = Vec::new();
|
||||
|
||||
+3
-13
@@ -24,7 +24,7 @@ use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use alkcall::core::auth::{Identity, IdentityProvider};
|
||||
use alkcall::core::auth::Identity;
|
||||
use alkcall::protocol::wire::{CallError, ResponseEnvelope};
|
||||
use alkcall::registry::registration::OperationRegistry;
|
||||
use alkcall::registry::spec::{AccessResult, Visibility};
|
||||
@@ -83,38 +83,28 @@ type ByteStream = futures::stream::BoxStream<'static, Result<Bytes, axum::Error>
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct GatewayState {
|
||||
registry: Arc<OperationRegistry>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache,
|
||||
}
|
||||
|
||||
impl GatewayState {
|
||||
pub(crate) fn new(
|
||||
registry: Arc<OperationRegistry>,
|
||||
identity_provider: Arc<dyn IdentityProvider>,
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache,
|
||||
) -> Self {
|
||||
Self {
|
||||
registry,
|
||||
identity_provider,
|
||||
publish_schemas,
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch(&self) -> GatewayDispatch {
|
||||
GatewayDispatch::new(
|
||||
Arc::clone(&self.registry),
|
||||
Arc::clone(&self.identity_provider),
|
||||
)
|
||||
GatewayDispatch::new(Arc::clone(&self.registry))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<RouterState> for GatewayState {
|
||||
fn from_ref(state: &RouterState) -> Self {
|
||||
GatewayState::new(
|
||||
Arc::clone(&state.registry),
|
||||
Arc::clone(&state.identity_provider),
|
||||
state.publish_schemas.clone(),
|
||||
)
|
||||
GatewayState::new(Arc::clone(&state.registry), state.publish_schemas.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -285,10 +285,7 @@ fn build_router(state: RouterState, extra_routes: Option<Router>) -> Router {
|
||||
|
||||
#[cfg(feature = "mcp")]
|
||||
let mcp_router: Router<RouterState> = {
|
||||
let dispatch = crate::gateway::GatewayDispatch::new(
|
||||
Arc::clone(&state.registry),
|
||||
Arc::clone(&state.identity_provider),
|
||||
);
|
||||
let dispatch = crate::gateway::GatewayDispatch::new(Arc::clone(&state.registry));
|
||||
Router::new()
|
||||
.nest_service(
|
||||
"/mcp",
|
||||
|
||||
@@ -71,18 +71,6 @@ impl axum::extract::FromRef<RouterState> for DecoyConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::extract::FromRef<RouterState> for Arc<OperationRegistry> {
|
||||
fn from_ref(state: &RouterState) -> Self {
|
||||
Arc::clone(&state.registry)
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::extract::FromRef<RouterState> for Arc<dyn IdentityProvider> {
|
||||
fn from_ref(state: &RouterState) -> Self {
|
||||
Arc::clone(&state.identity_provider)
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::extract::FromRef<RouterState> for crate::server::adapter::CachedOpenAPIDoc {
|
||||
fn from_ref(state: &RouterState) -> Self {
|
||||
state.openapi_doc.clone()
|
||||
|
||||
@@ -141,12 +141,6 @@ impl SessionState {
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::extract::FromRef<SessionState> for Arc<OperationRegistry> {
|
||||
fn from_ref(state: &SessionState) -> Self {
|
||||
Arc::clone(&state.registry)
|
||||
}
|
||||
}
|
||||
|
||||
/// `FromRef` chain: a bare `Arc<OperationRegistry>` router state lifts
|
||||
/// into the handler's [`SessionState`]; a full `RouterState` carries
|
||||
/// the shared [`WsSessions`] instance and lifts through its own impl.
|
||||
|
||||
Reference in New Issue
Block a user