v0.2.7-pre.002
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 192
|
# version: 193
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.7-pre.1"
|
version = "0.2.7-pre.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/constants.rs
|
// file: crates/ksp-onchain-transport-lib/src/constants.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! Transport-owned tracing constants.
|
//! Transport-owned tracing constants.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
// file: crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
// version: 21
|
// version: 22
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
//! modern/legacy `getTransaction` coverage. `0.2.4` completes the HTTP surface with all ten Blocks and five Economics wrappers, including complete
|
//! modern/legacy `getTransaction` coverage. `0.2.4` completes the HTTP surface with all ten Blocks and five Economics wrappers, including complete
|
||||||
//! modern/legacy `getBlock`, positional inflation rewards, runtime-provided economics values and the final `KSP-TRANSPORT-007` compliance target.
|
//! modern/legacy `getBlock`, positional inflation rewards, runtime-provided economics values and the final `KSP-TRANSPORT-007` compliance target.
|
||||||
//! The candidate surface therefore exposes typed wrappers for all 52 current audited Solana HTTP methods while retaining 14 removed historical descriptors.
|
//! The candidate surface therefore exposes typed wrappers for all 52 current audited Solana HTTP methods while retaining 14 removed historical descriptors.
|
||||||
|
//! `0.2.7-pre.002` adds the provider-neutral WebSocket settings foundation, redacted endpoint URLs, explicit protocol-family discrimination, local session/subscription
|
||||||
|
//! identities, observable lifecycle states and safe snapshots. Physical sockets and subscription execution are intentionally deferred to later prereleases.
|
||||||
|
|
||||||
mod client;
|
mod client;
|
||||||
mod constants;
|
mod constants;
|
||||||
@@ -34,6 +36,8 @@ mod rpc_method;
|
|||||||
mod rpc_tokens;
|
mod rpc_tokens;
|
||||||
mod rpc_transactions;
|
mod rpc_transactions;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
mod ws_lifecycle;
|
||||||
|
mod ws_settings;
|
||||||
|
|
||||||
/// Passive runtime availability reported for one logical HTTP endpoint.
|
/// Passive runtime availability reported for one logical HTTP endpoint.
|
||||||
pub use self::client::HttpEndpointAvailability;
|
pub use self::client::HttpEndpointAvailability;
|
||||||
@@ -291,6 +295,38 @@ pub use self::settings::HttpRoleLimits;
|
|||||||
pub use self::settings::HttpRoleName;
|
pub use self::settings::HttpRoleName;
|
||||||
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
/// Complete runtime settings consumed by the Solana HTTP transport foundation.
|
||||||
pub use self::settings::HttpTransportSettings;
|
pub use self::settings::HttpTransportSettings;
|
||||||
|
/// Stable local identity assigned to one physical WebSocket session.
|
||||||
|
pub use self::ws_lifecycle::WsSessionId;
|
||||||
|
/// Safe runtime snapshot for one physical WebSocket session.
|
||||||
|
pub use self::ws_lifecycle::WsSessionSnapshot;
|
||||||
|
/// Observable lifecycle state of one physical WebSocket session.
|
||||||
|
pub use self::ws_lifecycle::WsSessionState;
|
||||||
|
/// Stable local identity assigned to one logical WebSocket subscription.
|
||||||
|
pub use self::ws_lifecycle::WsSubscriptionId;
|
||||||
|
/// Standard Solana subscription family represented by one logical WebSocket subscription.
|
||||||
|
pub use self::ws_lifecycle::WsSubscriptionKind;
|
||||||
|
/// Safe lifecycle projection for one logical WebSocket subscription.
|
||||||
|
pub use self::ws_lifecycle::WsSubscriptionSnapshot;
|
||||||
|
/// Observable lifecycle state of one logical WebSocket subscription.
|
||||||
|
pub use self::ws_lifecycle::WsSubscriptionState;
|
||||||
|
/// Open cluster or network descriptor used by WebSocket endpoint settings.
|
||||||
|
pub use self::ws_settings::WsClusterName;
|
||||||
|
/// Runtime settings for one named WebSocket endpoint.
|
||||||
|
pub use self::ws_settings::WsEndpointSettings;
|
||||||
|
/// Runtime WebSocket endpoint URL with redacted diagnostics.
|
||||||
|
pub use self::ws_settings::WsEndpointUrl;
|
||||||
|
/// WebSocket protocol family understood by KSP Transport.
|
||||||
|
pub use self::ws_settings::WsProtocolKind;
|
||||||
|
/// Open provider descriptor used by WebSocket endpoint settings.
|
||||||
|
pub use self::ws_settings::WsProviderName;
|
||||||
|
/// Bounded reconnect settings owned by the WebSocket transport runtime.
|
||||||
|
pub use self::ws_settings::WsReconnectSettings;
|
||||||
|
/// Policy controlling logical resubscription after reconnect.
|
||||||
|
pub use self::ws_settings::WsResubscribePolicy;
|
||||||
|
/// Runtime limits and lifecycle settings for one physical WebSocket session.
|
||||||
|
pub use self::ws_settings::WsSessionSettings;
|
||||||
|
/// Complete runtime settings consumed by the KSP WebSocket transport foundation.
|
||||||
|
pub use self::ws_settings::WsTransportSettings;
|
||||||
|
|
||||||
/// Owning tracing target for events emitted by the on-chain transport crate.
|
/// Owning tracing target for events emitted by the on-chain transport crate.
|
||||||
pub(crate) use self::constants::TRACING_TARGET;
|
pub(crate) use self::constants::TRACING_TARGET;
|
||||||
|
|||||||
281
crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
Normal file
281
crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
/// Stable local identity assigned to one physical WebSocket session.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct WsSessionId(std::num::NonZeroU64);
|
||||||
|
|
||||||
|
impl WsSessionId {
|
||||||
|
/// Creates a session identity from a non-zero local value.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(value: std::num::NonZeroU64) -> Self {
|
||||||
|
return Self(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stable local numeric value.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn get(self) -> u64 {
|
||||||
|
return self.0.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stable local identity assigned to one logical WebSocket subscription.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
pub struct WsSubscriptionId(std::num::NonZeroU64);
|
||||||
|
|
||||||
|
impl WsSubscriptionId {
|
||||||
|
/// Creates a subscription identity from a non-zero local value.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(value: std::num::NonZeroU64) -> Self {
|
||||||
|
return Self(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stable local numeric value.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn get(self) -> u64 {
|
||||||
|
return self.0.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Observable lifecycle state of one physical WebSocket session.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum WsSessionState {
|
||||||
|
/// No physical connection is currently active and no connection attempt is running.
|
||||||
|
Disconnected,
|
||||||
|
/// The actor is establishing the physical connection.
|
||||||
|
Connecting,
|
||||||
|
/// The physical connection is active.
|
||||||
|
Active,
|
||||||
|
/// The actor is reconnecting after an unexpected physical disconnect.
|
||||||
|
Reconnecting {
|
||||||
|
/// One-based reconnect attempt currently in progress or waiting for backoff.
|
||||||
|
attempt: u32,
|
||||||
|
},
|
||||||
|
/// Explicit shutdown has started and new subscriptions are refused.
|
||||||
|
Closing,
|
||||||
|
/// Explicit shutdown completed.
|
||||||
|
Closed,
|
||||||
|
/// The session reached a terminal failure state.
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Observable lifecycle state of one logical WebSocket subscription.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum WsSubscriptionState {
|
||||||
|
/// The local subscription exists but its initial subscribe request has not completed.
|
||||||
|
Requested,
|
||||||
|
/// The logical subscription is bound to an active remote subscription.
|
||||||
|
Active,
|
||||||
|
/// The logical subscription is being restored after reconnect.
|
||||||
|
Resubscribing,
|
||||||
|
/// Local cancellation has won and remote cleanup is in progress when possible.
|
||||||
|
Cancelling,
|
||||||
|
/// The logical subscription reached a non-error terminal state.
|
||||||
|
Closed,
|
||||||
|
/// The logical subscription reached a terminal failure state.
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard Solana subscription family represented by one logical WebSocket subscription.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum WsSubscriptionKind {
|
||||||
|
/// `accountSubscribe` family.
|
||||||
|
Account,
|
||||||
|
/// `blockSubscribe` family.
|
||||||
|
Block,
|
||||||
|
/// `logsSubscribe` family.
|
||||||
|
Logs,
|
||||||
|
/// `programSubscribe` family.
|
||||||
|
Program,
|
||||||
|
/// `rootSubscribe` family.
|
||||||
|
Root,
|
||||||
|
/// `signatureSubscribe` family.
|
||||||
|
Signature,
|
||||||
|
/// `slotSubscribe` family.
|
||||||
|
Slot,
|
||||||
|
/// `slotsUpdatesSubscribe` family.
|
||||||
|
SlotsUpdates,
|
||||||
|
/// `voteSubscribe` family.
|
||||||
|
Vote,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsSubscriptionKind {
|
||||||
|
/// Returns the stable KSP descriptor for this standard subscription family.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Account => "account",
|
||||||
|
Self::Block => "block",
|
||||||
|
Self::Logs => "logs",
|
||||||
|
Self::Program => "program",
|
||||||
|
Self::Root => "root",
|
||||||
|
Self::Signature => "signature",
|
||||||
|
Self::Slot => "slot",
|
||||||
|
Self::SlotsUpdates => "slots_updates",
|
||||||
|
Self::Vote => "vote",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe lifecycle projection for one logical WebSocket subscription.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WsSubscriptionSnapshot {
|
||||||
|
id: crate::WsSubscriptionId,
|
||||||
|
kind: crate::WsSubscriptionKind,
|
||||||
|
state: crate::WsSubscriptionState,
|
||||||
|
remote_bound: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsSubscriptionSnapshot {
|
||||||
|
/// Creates one safe subscription lifecycle projection for Transport runtime internals.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn new(id: crate::WsSubscriptionId, kind: crate::WsSubscriptionKind, state: crate::WsSubscriptionState, remote_bound: bool) -> Self {
|
||||||
|
return Self { id, kind, state, remote_bound };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stable local subscription identity.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn id(&self) -> crate::WsSubscriptionId {
|
||||||
|
return self.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the standard subscription family.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn kind(&self) -> crate::WsSubscriptionKind {
|
||||||
|
return self.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current logical lifecycle state.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn state(&self) -> crate::WsSubscriptionState {
|
||||||
|
return self.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether a current remote subscription ID is bound internally.
|
||||||
|
///
|
||||||
|
/// The remote ID itself is deliberately absent because it is ephemeral across reconnects.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn remote_bound(&self) -> bool {
|
||||||
|
return self.remote_bound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe runtime snapshot for one physical WebSocket session.
|
||||||
|
///
|
||||||
|
/// The snapshot deliberately contains logical endpoint metadata and local identities only. It never stores the endpoint URL, credentials, request payloads or
|
||||||
|
/// raw notifications.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WsSessionSnapshot {
|
||||||
|
id: crate::WsSessionId,
|
||||||
|
endpoint_name: std::string::String,
|
||||||
|
provider: crate::WsProviderName,
|
||||||
|
cluster: crate::WsClusterName,
|
||||||
|
protocol: crate::WsProtocolKind,
|
||||||
|
state: crate::WsSessionState,
|
||||||
|
pending_request_count: usize,
|
||||||
|
continuity_gap_count: u64,
|
||||||
|
overflow_count: u64,
|
||||||
|
subscriptions: std::vec::Vec<crate::WsSubscriptionSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsSessionSnapshot {
|
||||||
|
/// Creates one safe session projection for Transport runtime internals.
|
||||||
|
#[must_use]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) fn new(
|
||||||
|
id: crate::WsSessionId,
|
||||||
|
endpoint_name: impl std::convert::Into<std::string::String>,
|
||||||
|
provider: crate::WsProviderName,
|
||||||
|
cluster: crate::WsClusterName,
|
||||||
|
protocol: crate::WsProtocolKind,
|
||||||
|
state: crate::WsSessionState,
|
||||||
|
pending_request_count: usize,
|
||||||
|
continuity_gap_count: u64,
|
||||||
|
overflow_count: u64,
|
||||||
|
subscriptions: std::vec::Vec<crate::WsSubscriptionSnapshot>,
|
||||||
|
) -> Self {
|
||||||
|
return Self {
|
||||||
|
id,
|
||||||
|
endpoint_name: endpoint_name.into(),
|
||||||
|
provider,
|
||||||
|
cluster,
|
||||||
|
protocol,
|
||||||
|
state,
|
||||||
|
pending_request_count,
|
||||||
|
continuity_gap_count,
|
||||||
|
overflow_count,
|
||||||
|
subscriptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the stable local session identity.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn id(&self) -> crate::WsSessionId {
|
||||||
|
return self.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the safe logical endpoint name.
|
||||||
|
#[must_use]
|
||||||
|
pub fn endpoint_name(&self) -> &str {
|
||||||
|
return self.endpoint_name.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the provider descriptor without endpoint credentials.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn provider(&self) -> &crate::WsProviderName {
|
||||||
|
return &self.provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the cluster descriptor.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn cluster(&self) -> &crate::WsClusterName {
|
||||||
|
return &self.cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the WebSocket protocol family.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn protocol(&self) -> crate::WsProtocolKind {
|
||||||
|
return self.protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current physical session lifecycle state.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn state(&self) -> crate::WsSessionState {
|
||||||
|
return self.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of JSON-RPC requests currently awaiting responses.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn pending_request_count(&self) -> usize {
|
||||||
|
return self.pending_request_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of observed physical continuity gaps for this session.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn continuity_gap_count(&self) -> u64 {
|
||||||
|
return self.continuity_gap_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the cumulative number of notification queue overflows observed by this session.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn overflow_count(&self) -> u64 {
|
||||||
|
return self.overflow_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns safe lifecycle projections for logical subscriptions owned by this session.
|
||||||
|
#[must_use]
|
||||||
|
pub fn subscriptions(&self) -> &[crate::WsSubscriptionSnapshot] {
|
||||||
|
return self.subscriptions.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of logical subscriptions currently projected by the session.
|
||||||
|
#[must_use]
|
||||||
|
pub fn subscription_count(&self) -> usize {
|
||||||
|
return self.subscriptions.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/ws_lifecycle.rs"]
|
||||||
|
mod tests;
|
||||||
544
crates/ksp-onchain-transport-lib/src/ws_settings.rs
Normal file
544
crates/ksp-onchain-transport-lib/src/ws_settings.rs
Normal file
@@ -0,0 +1,544 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/src/ws_settings.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
const DEFAULT_WS_CLOSE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
|
const DEFAULT_WS_COMMAND_QUEUE_CAPACITY: usize = 128;
|
||||||
|
const DEFAULT_WS_COMMAND_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||||
|
const DEFAULT_WS_MAX_ACTIVE_SUBSCRIPTIONS: usize = 1_024;
|
||||||
|
const DEFAULT_WS_MAX_FRAME_SIZE_BYTES: usize = 16 * 1024 * 1024;
|
||||||
|
const DEFAULT_WS_MAX_MESSAGE_SIZE_BYTES: usize = 64 * 1024 * 1024;
|
||||||
|
const DEFAULT_WS_MAX_PENDING_REQUESTS: usize = 128;
|
||||||
|
const DEFAULT_WS_MAX_WRITE_BUFFER_SIZE_BYTES: usize = 1024 * 1024;
|
||||||
|
const DEFAULT_WS_NOTIFICATION_QUEUE_CAPACITY: usize = 256;
|
||||||
|
const DEFAULT_WS_RECONNECT_INITIAL_BACKOFF: std::time::Duration = std::time::Duration::from_millis(250);
|
||||||
|
const DEFAULT_WS_RECONNECT_MAX_BACKOFF: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
|
const DEFAULT_WS_RECONNECT_MAX_RETRIES: u32 = 5;
|
||||||
|
|
||||||
|
/// Runtime WebSocket endpoint URL owned by Transport.
|
||||||
|
///
|
||||||
|
/// The actual URL can contain provider credentials. Its [`std::fmt::Debug`] implementation is intentionally redacted.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct WsEndpointUrl {
|
||||||
|
value: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsEndpointUrl {
|
||||||
|
/// Parses and validates one WebSocket endpoint URL.
|
||||||
|
pub fn parse(value: impl std::convert::Into<std::string::String>) -> ksp_core_lib::Result<Self> {
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating WebSocket endpoint URL");
|
||||||
|
let value = value.into();
|
||||||
|
let parsed = match reqwest::Url::parse(value.as_str()) {
|
||||||
|
std::result::Result::Ok(parsed) => parsed,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "ws_endpoints.url", "rejected invalid WebSocket endpoint URL");
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket endpoint URL is invalid")
|
||||||
|
.with_context("field", "ws_endpoints.url")
|
||||||
|
.with_source(error),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if parsed.scheme() != "ws" && parsed.scheme() != "wss" {
|
||||||
|
ksp_logging_lib::warn!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
field = "ws_endpoints.url",
|
||||||
|
scheme = parsed.scheme(),
|
||||||
|
"rejected WebSocket endpoint URL with unsupported scheme"
|
||||||
|
);
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket endpoint URL must use ws or wss")
|
||||||
|
.with_context("field", "ws_endpoints.url")
|
||||||
|
.with_context("scheme", parsed.scheme()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if parsed.host_str().is_none() {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = "ws_endpoints.url", "rejected WebSocket endpoint URL without host");
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket endpoint URL must contain a host")
|
||||||
|
.with_context("field", "ws_endpoints.url"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, scheme = parsed.scheme(), "validated WebSocket endpoint URL syntax");
|
||||||
|
return std::result::Result::Ok(Self { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the sensitive runtime URL text.
|
||||||
|
///
|
||||||
|
/// Callers must not write this value to logs, generic diagnostics or snapshots.
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
return self.value.as_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for WsEndpointUrl {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.write_str("WsEndpointUrl(<redacted>)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open provider descriptor used by WebSocket endpoint settings.
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct WsProviderName {
|
||||||
|
value: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsProviderName {
|
||||||
|
/// Creates an open provider descriptor. Validation is performed by [`WsTransportSettings::validate`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
|
||||||
|
return Self { value: value.into() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the provider descriptor text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
return self.value.as_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open cluster or network descriptor used by WebSocket endpoint settings.
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct WsClusterName {
|
||||||
|
value: std::string::String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsClusterName {
|
||||||
|
/// Creates an open cluster descriptor. Validation is performed by [`WsTransportSettings::validate`].
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(value: impl std::convert::Into<std::string::String>) -> Self {
|
||||||
|
return Self { value: value.into() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the cluster descriptor text.
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
return self.value.as_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebSocket protocol family understood by KSP Transport.
|
||||||
|
///
|
||||||
|
/// `0.2.7` exposes only standard Solana WebSocket. The non-exhaustive contract allows later provider-specific families without changing the common endpoint
|
||||||
|
/// container or injecting provider-only options into [`WsSessionSettings`].
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum WsProtocolKind {
|
||||||
|
/// Standard Solana JSON-RPC WebSocket PubSub.
|
||||||
|
SolanaStandard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsProtocolKind {
|
||||||
|
/// Returns the stable KSP descriptor for this protocol family.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::SolanaStandard => "solana_standard",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounded reconnect settings owned by the WebSocket transport runtime.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WsReconnectSettings {
|
||||||
|
max_retries: u32,
|
||||||
|
initial_backoff: std::time::Duration,
|
||||||
|
max_backoff: std::time::Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsReconnectSettings {
|
||||||
|
/// Creates bounded reconnect settings.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(max_retries: u32, initial_backoff: std::time::Duration, max_backoff: std::time::Duration) -> Self {
|
||||||
|
return Self { max_retries, initial_backoff, max_backoff };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of reconnect attempts allowed after the connection is lost.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_retries(&self) -> u32 {
|
||||||
|
return self.max_retries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the initial reconnect backoff.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn initial_backoff(&self) -> std::time::Duration {
|
||||||
|
return self.initial_backoff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum reconnect backoff.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_backoff(&self) -> std::time::Duration {
|
||||||
|
return self.max_backoff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for WsReconnectSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self::new(DEFAULT_WS_RECONNECT_MAX_RETRIES, DEFAULT_WS_RECONNECT_INITIAL_BACKOFF, DEFAULT_WS_RECONNECT_MAX_BACKOFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Policy controlling whether logical subscriptions are restored after a successful reconnect.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||||
|
pub enum WsResubscribePolicy {
|
||||||
|
/// Never restore subscriptions automatically after the physical connection is replaced.
|
||||||
|
Never,
|
||||||
|
/// Restore subscriptions that are still logically desired when reconnect completes.
|
||||||
|
#[default]
|
||||||
|
ActiveSubscriptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime limits and lifecycle settings for one physical WebSocket session.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WsSessionSettings {
|
||||||
|
command_timeout: std::time::Duration,
|
||||||
|
close_timeout: std::time::Duration,
|
||||||
|
reconnect: crate::WsReconnectSettings,
|
||||||
|
resubscribe: crate::WsResubscribePolicy,
|
||||||
|
command_queue_capacity: usize,
|
||||||
|
notification_queue_capacity: usize,
|
||||||
|
max_active_subscriptions: usize,
|
||||||
|
max_pending_requests: usize,
|
||||||
|
max_message_size_bytes: usize,
|
||||||
|
max_frame_size_bytes: usize,
|
||||||
|
max_write_buffer_size_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsSessionSettings {
|
||||||
|
/// Creates complete runtime settings for one physical WebSocket session.
|
||||||
|
#[must_use]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub const fn new(
|
||||||
|
command_timeout: std::time::Duration,
|
||||||
|
close_timeout: std::time::Duration,
|
||||||
|
reconnect: crate::WsReconnectSettings,
|
||||||
|
resubscribe: crate::WsResubscribePolicy,
|
||||||
|
command_queue_capacity: usize,
|
||||||
|
notification_queue_capacity: usize,
|
||||||
|
max_active_subscriptions: usize,
|
||||||
|
max_pending_requests: usize,
|
||||||
|
max_message_size_bytes: usize,
|
||||||
|
max_frame_size_bytes: usize,
|
||||||
|
max_write_buffer_size_bytes: usize,
|
||||||
|
) -> Self {
|
||||||
|
return Self {
|
||||||
|
command_timeout,
|
||||||
|
close_timeout,
|
||||||
|
reconnect,
|
||||||
|
resubscribe,
|
||||||
|
command_queue_capacity,
|
||||||
|
notification_queue_capacity,
|
||||||
|
max_active_subscriptions,
|
||||||
|
max_pending_requests,
|
||||||
|
max_message_size_bytes,
|
||||||
|
max_frame_size_bytes,
|
||||||
|
max_write_buffer_size_bytes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the deadline applied to bounded session commands and JSON-RPC control requests.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn command_timeout(&self) -> std::time::Duration {
|
||||||
|
return self.command_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total bounded close/shutdown deadline.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn close_timeout(&self) -> std::time::Duration {
|
||||||
|
return self.close_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the reconnect policy.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn reconnect(&self) -> &crate::WsReconnectSettings {
|
||||||
|
return &self.reconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the resubscribe policy.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn resubscribe(&self) -> crate::WsResubscribePolicy {
|
||||||
|
return self.resubscribe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bounded session-command queue capacity.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn command_queue_capacity(&self) -> usize {
|
||||||
|
return self.command_queue_capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bounded notification queue capacity allocated per logical subscription.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn notification_queue_capacity(&self) -> usize {
|
||||||
|
return self.notification_queue_capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum number of logical subscriptions allowed on one physical session.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_active_subscriptions(&self) -> usize {
|
||||||
|
return self.max_active_subscriptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum number of JSON-RPC requests allowed to await responses concurrently.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_pending_requests(&self) -> usize {
|
||||||
|
return self.max_pending_requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum accepted complete WebSocket message size in bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_message_size_bytes(&self) -> usize {
|
||||||
|
return self.max_message_size_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum accepted WebSocket frame size in bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_frame_size_bytes(&self) -> usize {
|
||||||
|
return self.max_frame_size_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum WebSocket write-buffer size in bytes.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn max_write_buffer_size_bytes(&self) -> usize {
|
||||||
|
return self.max_write_buffer_size_bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates runtime bounds without reading Config or environment state.
|
||||||
|
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, "validating WebSocket session settings");
|
||||||
|
if self.command_timeout.is_zero() {
|
||||||
|
return ws_invalid_settings("WebSocket command timeout must be greater than zero", "ws_session.command_timeout");
|
||||||
|
}
|
||||||
|
if self.close_timeout.is_zero() {
|
||||||
|
return ws_invalid_settings("WebSocket close timeout must be greater than zero", "ws_session.close_timeout");
|
||||||
|
}
|
||||||
|
if self.reconnect.initial_backoff().is_zero() {
|
||||||
|
return ws_invalid_settings("initial WebSocket reconnect backoff must be greater than zero", "ws_session.reconnect.initial_backoff");
|
||||||
|
}
|
||||||
|
if self.reconnect.max_backoff().is_zero() {
|
||||||
|
return ws_invalid_settings("maximum WebSocket reconnect backoff must be greater than zero", "ws_session.reconnect.max_backoff");
|
||||||
|
}
|
||||||
|
if self.reconnect.max_backoff() < self.reconnect.initial_backoff() {
|
||||||
|
return ws_invalid_settings(
|
||||||
|
"maximum WebSocket reconnect backoff must not be lower than initial reconnect backoff",
|
||||||
|
"ws_session.reconnect.max_backoff",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
validate_non_zero_bound(self.command_queue_capacity, "ws_session.command_queue_capacity")?;
|
||||||
|
validate_non_zero_bound(self.notification_queue_capacity, "ws_session.notification_queue_capacity")?;
|
||||||
|
validate_non_zero_bound(self.max_active_subscriptions, "ws_session.max_active_subscriptions")?;
|
||||||
|
validate_non_zero_bound(self.max_pending_requests, "ws_session.max_pending_requests")?;
|
||||||
|
validate_non_zero_bound(self.max_message_size_bytes, "ws_session.max_message_size_bytes")?;
|
||||||
|
validate_non_zero_bound(self.max_frame_size_bytes, "ws_session.max_frame_size_bytes")?;
|
||||||
|
validate_non_zero_bound(self.max_write_buffer_size_bytes, "ws_session.max_write_buffer_size_bytes")?;
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
command_queue_capacity = self.command_queue_capacity,
|
||||||
|
notification_queue_capacity = self.notification_queue_capacity,
|
||||||
|
max_active_subscriptions = self.max_active_subscriptions,
|
||||||
|
max_pending_requests = self.max_pending_requests,
|
||||||
|
max_message_size_bytes = self.max_message_size_bytes,
|
||||||
|
max_frame_size_bytes = self.max_frame_size_bytes,
|
||||||
|
max_write_buffer_size_bytes = self.max_write_buffer_size_bytes,
|
||||||
|
reconnect_max_retries = self.reconnect.max_retries(),
|
||||||
|
resubscribe = self.resubscribe.as_str(),
|
||||||
|
"validated WebSocket session settings"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::default::Default for WsSessionSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
return Self::new(
|
||||||
|
DEFAULT_WS_COMMAND_TIMEOUT,
|
||||||
|
DEFAULT_WS_CLOSE_TIMEOUT,
|
||||||
|
crate::WsReconnectSettings::default(),
|
||||||
|
crate::WsResubscribePolicy::default(),
|
||||||
|
DEFAULT_WS_COMMAND_QUEUE_CAPACITY,
|
||||||
|
DEFAULT_WS_NOTIFICATION_QUEUE_CAPACITY,
|
||||||
|
DEFAULT_WS_MAX_ACTIVE_SUBSCRIPTIONS,
|
||||||
|
DEFAULT_WS_MAX_PENDING_REQUESTS,
|
||||||
|
DEFAULT_WS_MAX_MESSAGE_SIZE_BYTES,
|
||||||
|
DEFAULT_WS_MAX_FRAME_SIZE_BYTES,
|
||||||
|
DEFAULT_WS_MAX_WRITE_BUFFER_SIZE_BYTES,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsResubscribePolicy {
|
||||||
|
/// Returns the stable KSP descriptor for this policy.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn as_str(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Never => "never",
|
||||||
|
Self::ActiveSubscriptions => "active_subscriptions",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime settings for one named WebSocket endpoint.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WsEndpointSettings {
|
||||||
|
name: std::string::String,
|
||||||
|
enabled: bool,
|
||||||
|
provider: crate::WsProviderName,
|
||||||
|
cluster: crate::WsClusterName,
|
||||||
|
protocol: crate::WsProtocolKind,
|
||||||
|
url: crate::WsEndpointUrl,
|
||||||
|
session: crate::WsSessionSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsEndpointSettings {
|
||||||
|
/// Creates explicit settings for one logical WebSocket endpoint.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
name: impl std::convert::Into<std::string::String>,
|
||||||
|
enabled: bool,
|
||||||
|
provider: crate::WsProviderName,
|
||||||
|
cluster: crate::WsClusterName,
|
||||||
|
protocol: crate::WsProtocolKind,
|
||||||
|
url: crate::WsEndpointUrl,
|
||||||
|
session: crate::WsSessionSettings,
|
||||||
|
) -> Self {
|
||||||
|
return Self { name: name.into(), enabled, provider, cluster, protocol, url, session };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the logical endpoint name.
|
||||||
|
#[must_use]
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
return self.name.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this endpoint can be used to create physical sessions.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn enabled(&self) -> bool {
|
||||||
|
return self.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the open provider descriptor.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn provider(&self) -> &crate::WsProviderName {
|
||||||
|
return &self.provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the open cluster descriptor.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn cluster(&self) -> &crate::WsClusterName {
|
||||||
|
return &self.cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the WebSocket protocol family.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn protocol(&self) -> crate::WsProtocolKind {
|
||||||
|
return self.protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the sensitive WebSocket endpoint URL wrapper.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn url(&self) -> &crate::WsEndpointUrl {
|
||||||
|
return &self.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the effective settings applied to every physical session explicitly created from this endpoint.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn session(&self) -> &crate::WsSessionSettings {
|
||||||
|
return &self.session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete runtime settings consumed by the KSP WebSocket transport foundation.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct WsTransportSettings {
|
||||||
|
endpoints: std::vec::Vec<crate::WsEndpointSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WsTransportSettings {
|
||||||
|
/// Creates complete WebSocket transport runtime settings.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(endpoints: std::vec::Vec<crate::WsEndpointSettings>) -> Self {
|
||||||
|
return Self { endpoints };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns configured WebSocket endpoints in declaration order.
|
||||||
|
#[must_use]
|
||||||
|
pub fn endpoints(&self) -> &[crate::WsEndpointSettings] {
|
||||||
|
return self.endpoints.as_slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates structural runtime invariants without reading Config or environment state.
|
||||||
|
pub fn validate(&self) -> ksp_core_lib::Result<()> {
|
||||||
|
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, endpoint_count = self.endpoints.len(), "validating WebSocket transport settings");
|
||||||
|
if self.endpoints.is_empty() {
|
||||||
|
return ws_invalid_settings("at least one WebSocket endpoint must be configured", "ws_endpoints");
|
||||||
|
}
|
||||||
|
let mut enabled_endpoint_count = 0_usize;
|
||||||
|
for (endpoint_index, endpoint) in self.endpoints.iter().enumerate() {
|
||||||
|
validate_ws_endpoint(endpoint, endpoint_index)?;
|
||||||
|
if endpoint.enabled() {
|
||||||
|
enabled_endpoint_count += 1;
|
||||||
|
}
|
||||||
|
for previous in &self.endpoints[..endpoint_index] {
|
||||||
|
if previous.name() == endpoint.name() {
|
||||||
|
return std::result::Result::Err(
|
||||||
|
ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, "WebSocket endpoint names must be unique")
|
||||||
|
.with_context("field", format!("ws_endpoints[{endpoint_index}].name"))
|
||||||
|
.with_context("endpoint_name", endpoint.name()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if enabled_endpoint_count == 0 {
|
||||||
|
return ws_invalid_settings("at least one WebSocket endpoint must be enabled", "ws_endpoints.enabled");
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
endpoint_count = self.endpoints.len(),
|
||||||
|
enabled_endpoint_count,
|
||||||
|
"validated WebSocket transport settings"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_ws_endpoint(endpoint: &crate::WsEndpointSettings, endpoint_index: usize) -> ksp_core_lib::Result<()> {
|
||||||
|
validate_ws_descriptor(endpoint.name(), format!("ws_endpoints[{endpoint_index}].name").as_str())?;
|
||||||
|
validate_ws_descriptor(endpoint.provider().as_str(), format!("ws_endpoints[{endpoint_index}].provider").as_str())?;
|
||||||
|
validate_ws_descriptor(endpoint.cluster().as_str(), format!("ws_endpoints[{endpoint_index}].cluster").as_str())?;
|
||||||
|
endpoint.session().validate()?;
|
||||||
|
ksp_logging_lib::trace!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
endpoint_name = endpoint.name(),
|
||||||
|
provider = endpoint.provider().as_str(),
|
||||||
|
cluster = endpoint.cluster().as_str(),
|
||||||
|
protocol = endpoint.protocol().as_str(),
|
||||||
|
enabled = endpoint.enabled(),
|
||||||
|
"validated WebSocket endpoint settings"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_ws_descriptor(value: &str, field: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return ws_invalid_settings("WebSocket transport descriptor must not be empty", field);
|
||||||
|
}
|
||||||
|
if value.trim() != value {
|
||||||
|
return ws_invalid_settings("WebSocket transport descriptor must not contain leading or trailing whitespace", field);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_non_zero_bound(value: usize, field: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
if value == 0 {
|
||||||
|
return ws_invalid_settings("WebSocket runtime bound must be greater than zero", field);
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ws_invalid_settings(message: &str, field: &str) -> ksp_core_lib::Result<()> {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, field = field, reason = message, "rejected WebSocket transport settings");
|
||||||
|
return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_SETTINGS, message).with_context("field", field));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/ws_settings.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
// file: crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
// version: 24
|
// version: 25
|
||||||
|
|
||||||
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
//! Integration tests for the public `ksp-onchain-transport-lib` consumer contract.
|
||||||
|
|
||||||
@@ -521,3 +521,31 @@ fn public_v0_2_4_pre_009_all_52_current_typed_wrappers_and_legacy_forms_are_avai
|
|||||||
let _get_block_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_block_legacy;
|
let _get_block_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_block_legacy;
|
||||||
let _get_transaction_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_legacy;
|
let _get_transaction_legacy = ksp_onchain_transport_lib::HttpTransportPool::get_transaction_legacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn public_v0_2_7_pre_002_websocket_settings_and_lifecycle_contracts_are_available_from_crate_root() {
|
||||||
|
let url = ksp_onchain_transport_lib::WsEndpointUrl::parse("wss://api.devnet.solana.com").expect("public WebSocket URL fixture must parse");
|
||||||
|
let session_settings = ksp_onchain_transport_lib::WsSessionSettings::default();
|
||||||
|
let endpoint = ksp_onchain_transport_lib::WsEndpointSettings::new(
|
||||||
|
"devnet_public",
|
||||||
|
true,
|
||||||
|
ksp_onchain_transport_lib::WsProviderName::new("solana-public"),
|
||||||
|
ksp_onchain_transport_lib::WsClusterName::new("devnet"),
|
||||||
|
ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard,
|
||||||
|
url,
|
||||||
|
session_settings,
|
||||||
|
);
|
||||||
|
let settings = ksp_onchain_transport_lib::WsTransportSettings::new(std::vec![endpoint]);
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
assert_eq!(settings.endpoints()[0].protocol(), ksp_onchain_transport_lib::WsProtocolKind::SolanaStandard);
|
||||||
|
let session_id = ksp_onchain_transport_lib::WsSessionId::new(std::num::NonZeroU64::new(1).expect("public test ID must be non-zero"));
|
||||||
|
let subscription_id = ksp_onchain_transport_lib::WsSubscriptionId::new(std::num::NonZeroU64::new(2).expect("public test ID must be non-zero"));
|
||||||
|
assert_eq!(session_id.get(), 1);
|
||||||
|
assert_eq!(subscription_id.get(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
ksp_onchain_transport_lib::WsSessionState::Reconnecting { attempt: 1 },
|
||||||
|
ksp_onchain_transport_lib::WsSessionState::Reconnecting { attempt: 1 }
|
||||||
|
);
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionKind::Slot.as_str(), "slot");
|
||||||
|
assert_eq!(ksp_onchain_transport_lib::WsSubscriptionState::Requested, ksp_onchain_transport_lib::WsSubscriptionState::Requested);
|
||||||
|
}
|
||||||
|
|||||||
70
crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
Normal file
70
crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
fn non_zero(value: u64) -> std::num::NonZeroU64 {
|
||||||
|
return std::num::NonZeroU64::new(value).expect("test ID must be non-zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_local_ids_preserve_ordering_and_numeric_identity() {
|
||||||
|
let first_session = crate::WsSessionId::new(non_zero(1));
|
||||||
|
let second_session = crate::WsSessionId::new(non_zero(2));
|
||||||
|
let subscription = crate::WsSubscriptionId::new(non_zero(7));
|
||||||
|
assert!(first_session < second_session);
|
||||||
|
assert_eq!(subscription.get(), 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_state_models_expose_concurrent_lifecycle_states() {
|
||||||
|
assert_eq!(crate::WsSessionState::Reconnecting { attempt: 3 }, crate::WsSessionState::Reconnecting { attempt: 3 });
|
||||||
|
assert_eq!(crate::WsSubscriptionState::Resubscribing, crate::WsSubscriptionState::Resubscribing);
|
||||||
|
assert_ne!(crate::WsSubscriptionState::Cancelling, crate::WsSubscriptionState::Closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_subscription_kinds_cover_all_nine_standard_families() {
|
||||||
|
let kinds = [
|
||||||
|
crate::WsSubscriptionKind::Account,
|
||||||
|
crate::WsSubscriptionKind::Block,
|
||||||
|
crate::WsSubscriptionKind::Logs,
|
||||||
|
crate::WsSubscriptionKind::Program,
|
||||||
|
crate::WsSubscriptionKind::Root,
|
||||||
|
crate::WsSubscriptionKind::Signature,
|
||||||
|
crate::WsSubscriptionKind::Slot,
|
||||||
|
crate::WsSubscriptionKind::SlotsUpdates,
|
||||||
|
crate::WsSubscriptionKind::Vote,
|
||||||
|
];
|
||||||
|
assert_eq!(kinds.len(), 9);
|
||||||
|
assert_eq!(kinds[7].as_str(), "slots_updates");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_snapshots_expose_safe_metadata_without_remote_ids_or_urls() {
|
||||||
|
let subscription = crate::WsSubscriptionSnapshot::new(
|
||||||
|
crate::WsSubscriptionId::new(non_zero(9)),
|
||||||
|
crate::WsSubscriptionKind::Slot,
|
||||||
|
crate::WsSubscriptionState::Active,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
let snapshot = crate::WsSessionSnapshot::new(
|
||||||
|
crate::WsSessionId::new(non_zero(3)),
|
||||||
|
"devnet_public",
|
||||||
|
crate::WsProviderName::new("solana-public"),
|
||||||
|
crate::WsClusterName::new("devnet"),
|
||||||
|
crate::WsProtocolKind::SolanaStandard,
|
||||||
|
crate::WsSessionState::Active,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
std::vec![subscription],
|
||||||
|
);
|
||||||
|
assert_eq!(snapshot.id().get(), 3);
|
||||||
|
assert_eq!(snapshot.endpoint_name(), "devnet_public");
|
||||||
|
assert_eq!(snapshot.pending_request_count(), 2);
|
||||||
|
assert_eq!(snapshot.continuity_gap_count(), 1);
|
||||||
|
assert_eq!(snapshot.subscription_count(), 1);
|
||||||
|
assert!(snapshot.subscriptions()[0].remote_bound());
|
||||||
|
let rendered = format!("{snapshot:?}");
|
||||||
|
assert!(!rendered.contains("wss://"));
|
||||||
|
assert!(!rendered.contains("remote_subscription_id"));
|
||||||
|
}
|
||||||
148
crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs
Normal file
148
crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
fn valid_endpoint(name: &str, url_text: &str) -> crate::WsEndpointSettings {
|
||||||
|
return crate::WsEndpointSettings::new(
|
||||||
|
name,
|
||||||
|
true,
|
||||||
|
crate::WsProviderName::new("solana-public"),
|
||||||
|
crate::WsClusterName::new("devnet"),
|
||||||
|
crate::WsProtocolKind::SolanaStandard,
|
||||||
|
crate::WsEndpointUrl::parse(url_text).expect("test WebSocket URL must parse"),
|
||||||
|
crate::WsSessionSettings::default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_endpoint_url_accepts_ws_and_wss() {
|
||||||
|
assert!(crate::WsEndpointUrl::parse("wss://api.devnet.solana.com").is_ok());
|
||||||
|
assert!(crate::WsEndpointUrl::parse("ws://127.0.0.1:8900").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_endpoint_url_rejects_http_schemes() {
|
||||||
|
let result = crate::WsEndpointUrl::parse("https://api.devnet.solana.com");
|
||||||
|
let error = result.expect_err("HTTP URL must not be accepted by WebSocket settings");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_endpoint_url_debug_redacts_secret_material() {
|
||||||
|
let url = crate::WsEndpointUrl::parse("wss://user:password@provider.invalid/path?api-key=SECRET-CANARY").expect("test URL must parse");
|
||||||
|
let rendered = format!("{url:?}");
|
||||||
|
assert_eq!(rendered, "WsEndpointUrl(<redacted>)");
|
||||||
|
assert!(!rendered.contains("SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("provider.invalid"));
|
||||||
|
assert!(!rendered.contains("password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_endpoint_url_errors_do_not_echo_sensitive_url() {
|
||||||
|
let result = crate::WsEndpointUrl::parse("http://user:password@provider.invalid/path?api-key=SECRET-CANARY");
|
||||||
|
let error = result.expect_err("unsupported scheme must fail");
|
||||||
|
let rendered = format!("{error:?}");
|
||||||
|
assert!(!rendered.contains("SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("provider.invalid"));
|
||||||
|
assert!(!rendered.contains("password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_protocol_kind_is_extensible_but_only_standard_is_available_now() {
|
||||||
|
assert_eq!(crate::WsProtocolKind::SolanaStandard.as_str(), "solana_standard");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_session_defaults_are_bounded_and_validate() {
|
||||||
|
let settings = crate::WsSessionSettings::default();
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
assert!(settings.command_queue_capacity() > 0);
|
||||||
|
assert!(settings.notification_queue_capacity() > 0);
|
||||||
|
assert!(settings.max_active_subscriptions() > 0);
|
||||||
|
assert!(settings.max_pending_requests() > 0);
|
||||||
|
assert!(settings.max_message_size_bytes() > 0);
|
||||||
|
assert!(settings.max_frame_size_bytes() > 0);
|
||||||
|
assert!(settings.max_write_buffer_size_bytes() > 0);
|
||||||
|
assert_eq!(settings.resubscribe(), crate::WsResubscribePolicy::ActiveSubscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_session_settings_reject_zero_runtime_bounds() {
|
||||||
|
let defaults = crate::WsSessionSettings::default();
|
||||||
|
let settings = crate::WsSessionSettings::new(
|
||||||
|
defaults.command_timeout(),
|
||||||
|
defaults.close_timeout(),
|
||||||
|
defaults.reconnect().clone(),
|
||||||
|
defaults.resubscribe(),
|
||||||
|
0,
|
||||||
|
defaults.notification_queue_capacity(),
|
||||||
|
defaults.max_active_subscriptions(),
|
||||||
|
defaults.max_pending_requests(),
|
||||||
|
defaults.max_message_size_bytes(),
|
||||||
|
defaults.max_frame_size_bytes(),
|
||||||
|
defaults.max_write_buffer_size_bytes(),
|
||||||
|
);
|
||||||
|
let error = settings.validate().expect_err("zero command queue capacity must fail");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_session_settings_reject_reversed_reconnect_backoff() {
|
||||||
|
let defaults = crate::WsSessionSettings::default();
|
||||||
|
let settings = crate::WsSessionSettings::new(
|
||||||
|
defaults.command_timeout(),
|
||||||
|
defaults.close_timeout(),
|
||||||
|
crate::WsReconnectSettings::new(2, std::time::Duration::from_secs(2), std::time::Duration::from_secs(1)),
|
||||||
|
defaults.resubscribe(),
|
||||||
|
defaults.command_queue_capacity(),
|
||||||
|
defaults.notification_queue_capacity(),
|
||||||
|
defaults.max_active_subscriptions(),
|
||||||
|
defaults.max_pending_requests(),
|
||||||
|
defaults.max_message_size_bytes(),
|
||||||
|
defaults.max_frame_size_bytes(),
|
||||||
|
defaults.max_write_buffer_size_bytes(),
|
||||||
|
);
|
||||||
|
assert!(settings.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_transport_settings_validate_unique_enabled_endpoints() {
|
||||||
|
let settings = crate::WsTransportSettings::new(std::vec![
|
||||||
|
valid_endpoint("devnet_primary", "wss://api.devnet.solana.com"),
|
||||||
|
valid_endpoint("devnet_secondary", "wss://example.invalid/ws"),
|
||||||
|
]);
|
||||||
|
assert!(settings.validate().is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_transport_settings_reject_duplicate_endpoint_names() {
|
||||||
|
let settings =
|
||||||
|
crate::WsTransportSettings::new(std::vec![valid_endpoint("duplicate", "wss://one.invalid/ws"), valid_endpoint("duplicate", "wss://two.invalid/ws"),]);
|
||||||
|
let error = settings.validate().expect_err("duplicate endpoint names must fail");
|
||||||
|
assert_eq!(error.code(), crate::ERROR_CODE_INVALID_SETTINGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_transport_settings_require_one_enabled_endpoint() {
|
||||||
|
let endpoint = crate::WsEndpointSettings::new(
|
||||||
|
"disabled",
|
||||||
|
false,
|
||||||
|
crate::WsProviderName::new("provider"),
|
||||||
|
crate::WsClusterName::new("devnet"),
|
||||||
|
crate::WsProtocolKind::SolanaStandard,
|
||||||
|
crate::WsEndpointUrl::parse("wss://provider.invalid/ws").expect("test URL must parse"),
|
||||||
|
crate::WsSessionSettings::default(),
|
||||||
|
);
|
||||||
|
let settings = crate::WsTransportSettings::new(std::vec![endpoint]);
|
||||||
|
assert!(settings.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn websocket_transport_settings_debug_never_exposes_endpoint_url() {
|
||||||
|
let settings =
|
||||||
|
crate::WsTransportSettings::new(std::vec![valid_endpoint("secret_endpoint", "wss://user:password@provider.invalid/path?api-key=SECRET-CANARY",)]);
|
||||||
|
let rendered = format!("{settings:?}");
|
||||||
|
assert!(rendered.contains("WsEndpointUrl(<redacted>)"));
|
||||||
|
assert!(!rendered.contains("SECRET-CANARY"));
|
||||||
|
assert!(!rendered.contains("provider.invalid"));
|
||||||
|
assert!(!rendered.contains("password"));
|
||||||
|
}
|
||||||
263
deltas/0.2.7/pre.002.md
Normal file
263
deltas/0.2.7/pre.002.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<!-- file: deltas/0.2.7/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.7-pre.002` — WebSocket settings + lifecycle contracts
|
||||||
|
|
||||||
|
## 1. Objet
|
||||||
|
|
||||||
|
Cette tranche matérialise la première surface Rust WebSocket de `ksp-onchain-transport-lib` sans ouvrir encore de socket physique et sans ajouter de dépendance WebSocket externe.
|
||||||
|
|
||||||
|
Version workspace :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.7-pre.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Livraison :
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.7-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Commit attendu :
|
||||||
|
|
||||||
|
```text
|
||||||
|
v0.2.7-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag prerelease.
|
||||||
|
|
||||||
|
## 2. Surface Transport ajoutée
|
||||||
|
|
||||||
|
Settings publics :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsEndpointUrl
|
||||||
|
WsProviderName
|
||||||
|
WsClusterName
|
||||||
|
WsProtocolKind
|
||||||
|
WsReconnectSettings
|
||||||
|
WsResubscribePolicy
|
||||||
|
WsSessionSettings
|
||||||
|
WsEndpointSettings
|
||||||
|
WsTransportSettings
|
||||||
|
```
|
||||||
|
|
||||||
|
Lifecycle public :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsSessionId
|
||||||
|
WsSubscriptionId
|
||||||
|
WsSessionState
|
||||||
|
WsSubscriptionState
|
||||||
|
WsSubscriptionKind
|
||||||
|
WsSessionSnapshot
|
||||||
|
WsSubscriptionSnapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
`WsProtocolKind` est `#[non_exhaustive]` et ne fournit pour `0.2.7` que `SolanaStandard`. Cette forme prépare l'ajout futur d'une famille provider-specific telle que Helius Enhanced WebSocket sans ajouter de paramètres Helius dans les settings Solana standard.
|
||||||
|
|
||||||
|
Les IDs locaux reposent sur `NonZeroU64`. Aucun ID serveur WebSocket n'entre dans le contrat public de contrôle.
|
||||||
|
|
||||||
|
## 3. URL et secrets
|
||||||
|
|
||||||
|
`WsEndpointUrl` :
|
||||||
|
|
||||||
|
- accepte uniquement `ws://` et `wss://` ;
|
||||||
|
- exige un host ;
|
||||||
|
- conserve la valeur sensible uniquement pour le futur code de connexion ;
|
||||||
|
- rend `WsEndpointUrl(<redacted>)` en `Debug` ;
|
||||||
|
- ne copie pas la valeur URL dans les erreurs de validation.
|
||||||
|
|
||||||
|
`WsEndpointSettings` et `WsTransportSettings` peuvent conserver `Debug` dérivé car le sous-type URL est lui-même redacted.
|
||||||
|
|
||||||
|
Les snapshots ne contiennent jamais :
|
||||||
|
|
||||||
|
```text
|
||||||
|
URL complète
|
||||||
|
credential/query token
|
||||||
|
request body
|
||||||
|
raw notification
|
||||||
|
remote subscription id
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Settings session bornés
|
||||||
|
|
||||||
|
Defaults initiaux Transport, explicitement policies KSP locales :
|
||||||
|
|
||||||
|
```text
|
||||||
|
command timeout 10 s
|
||||||
|
close timeout 5 s
|
||||||
|
reconnect retries 5
|
||||||
|
reconnect initial backoff 250 ms
|
||||||
|
reconnect maximum backoff 5 s
|
||||||
|
command queue 128
|
||||||
|
notification queue per sub 256
|
||||||
|
active subscriptions 1024
|
||||||
|
pending JSON-RPC requests 128
|
||||||
|
maximum message 64 MiB
|
||||||
|
maximum frame 16 MiB
|
||||||
|
maximum write buffer 1 MiB
|
||||||
|
resubscribe default ActiveSubscriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces valeurs ne sont pas présentées comme des limites Solana. `pre.004`/`pre.005` devront les appliquer réellement à l'actor/socket et pourront les recalibrer si les fixtures adversariales le justifient.
|
||||||
|
|
||||||
|
Validation structurelle :
|
||||||
|
|
||||||
|
- timeouts non nuls ;
|
||||||
|
- reconnect backoff non nul et ordonné ;
|
||||||
|
- capacités/limites strictement positives ;
|
||||||
|
- au moins un endpoint WS configuré et enabled ;
|
||||||
|
- noms endpoint uniques ;
|
||||||
|
- name/provider/cluster non vides et sans whitespace de bord.
|
||||||
|
|
||||||
|
## 5. Lifecycle et snapshots
|
||||||
|
|
||||||
|
États session matérialisés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Disconnected
|
||||||
|
Connecting
|
||||||
|
Active
|
||||||
|
Reconnecting { attempt }
|
||||||
|
Closing
|
||||||
|
Closed
|
||||||
|
Failed
|
||||||
|
```
|
||||||
|
|
||||||
|
États subscription matérialisés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Requested
|
||||||
|
Active
|
||||||
|
Resubscribing
|
||||||
|
Cancelling
|
||||||
|
Closed
|
||||||
|
Failed
|
||||||
|
```
|
||||||
|
|
||||||
|
`WsSubscriptionKind` couvre les neuf familles standard auditées : account, block, logs, program, root, signature, slot, slotsUpdates et vote.
|
||||||
|
|
||||||
|
`WsSessionSnapshot` expose uniquement des metadata sûres : local session ID, endpoint logical name, provider, cluster, protocol, state, pending request count, continuity gap count, overflow count et projections de subscriptions.
|
||||||
|
|
||||||
|
`WsSubscriptionSnapshot` expose local subscription ID, kind, state et `remote_bound`; l'ID distant reste interne et remappable.
|
||||||
|
|
||||||
|
Les constructeurs de snapshots sont crate-internal : les consumers ne peuvent pas fabriquer de faux états runtime.
|
||||||
|
|
||||||
|
## 6. Logging et tracing
|
||||||
|
|
||||||
|
La constante existante reste l'autorité crate-wide :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
TRACING_TARGET = "ksp-onchain-transport-lib"
|
||||||
|
```
|
||||||
|
|
||||||
|
Elle est définie dans `crates/ksp-onchain-transport-lib/src/constants.rs`.
|
||||||
|
|
||||||
|
Toute nouvelle émission passe par `ksp-logging-lib` :
|
||||||
|
|
||||||
|
- `trace` pour entrée/succès de validations et metadata endpoint sûres ;
|
||||||
|
- `debug` pour settings validés, compteurs et bornes ;
|
||||||
|
- `warn` pour rejets de settings/URL ;
|
||||||
|
- aucun `error` artificiel pour une erreur de validation caller.
|
||||||
|
|
||||||
|
Aucun appel direct à `tracing` n'est ajouté. Les logs n'incluent jamais la valeur de `WsEndpointUrl`.
|
||||||
|
|
||||||
|
## 7. Tests ajoutés
|
||||||
|
|
||||||
|
Tests unitaires settings :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ws/wss acceptés
|
||||||
|
HTTP rejeté
|
||||||
|
Debug URL redacted
|
||||||
|
erreur de scheme sans secret
|
||||||
|
protocol kind standard
|
||||||
|
settings defaults bornés
|
||||||
|
zero bound rejeté
|
||||||
|
reconnect backoff inversé rejeté
|
||||||
|
transport endpoints valides
|
||||||
|
endpoint names dupliqués rejetés
|
||||||
|
au moins un endpoint enabled
|
||||||
|
Debug transport sans URL/credential
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests unitaires lifecycle :
|
||||||
|
|
||||||
|
```text
|
||||||
|
IDs locaux non-zéro et ordonnables
|
||||||
|
états reconnect/resubscribe/cancelling distincts
|
||||||
|
9 familles standard couvertes
|
||||||
|
snapshot sans URL ni remote subscription id
|
||||||
|
```
|
||||||
|
|
||||||
|
Un canari `tests/public_api.rs` vérifie l'accès crate-root aux nouveaux contrats.
|
||||||
|
|
||||||
|
## 8. Fichiers principaux
|
||||||
|
|
||||||
|
Nouveaux :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/ws_lifecycle.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_settings.rs
|
||||||
|
crates/ksp-onchain-transport-lib/unit_tests/ws_lifecycle.rs
|
||||||
|
deltas/0.2.7/pre.002.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Modifiés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
crates/ksp-onchain-transport-lib/src/constants.rs
|
||||||
|
crates/ksp-onchain-transport-lib/src/lib.rs
|
||||||
|
crates/ksp-onchain-transport-lib/tests/public_api.rs
|
||||||
|
docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md
|
||||||
|
docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune dependency externe n'est ajoutée dans cette tranche.
|
||||||
|
|
||||||
|
## 9. Validation disponible dans le sandbox
|
||||||
|
|
||||||
|
Exécuté après modifications :
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Le sandbox ne fournit toujours pas `cargo`; les gates Rust compilés ne sont donc pas déclarés réussis ici.
|
||||||
|
|
||||||
|
Baseline opérateur reçue avant `pre.002` : `cargo fmt`, audit Python, `cargo check` et `cargo clippy` verts sur `0.2.7-pre.1`. `cargo test --workspace` n'échoue que sur le canari Config Desk qui compare encore la ressource packagée `0.2.6` à la version workspace `0.2.7-pre.1`; aucune régression WebSocket n'y est impliquée.
|
||||||
|
|
||||||
|
## 10. Gates opérateur avant commit
|
||||||
|
|
||||||
|
Après application du delta :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-onchain-transport-lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Le test workspace complet peut encore reproduire le canari de version packagée Config Desk tant que cette ressource n'est volontairement resynchronisée.
|
||||||
|
|
||||||
|
## 11. Suite
|
||||||
|
|
||||||
|
`0.2.7-pre.003` doit matérialiser :
|
||||||
|
|
||||||
|
```text
|
||||||
|
std.transport V2 HTTP + WS
|
||||||
|
backward read V1 HTTP-only
|
||||||
|
schema/fixtures V2
|
||||||
|
Config -> WsTransportSettings
|
||||||
|
ws_endpoints[].kind = solana_standard
|
||||||
|
```
|
||||||
|
|
||||||
|
La direction reste strictement `Config -> Transport`; aucun reverse dependency n'est autorisé.
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -->
|
<!-- file: docs/plans/014-V0_2_7_ONCHAIN_WEBSOCKET_PLAN.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Plan `0.2.7` — WebSocket Solana standard
|
# Plan `0.2.7` — WebSocket Solana standard
|
||||||
|
|
||||||
> **Statut : actif, gate `0.2.7-pre.001`.** Cette première tranche fixe l'inventaire normatif, les frontières, le threat model, le modèle session/subscription, la stratégie de dépendances et le sizing. Elle ne matérialise pas encore le client WebSocket runtime.
|
> **Statut : actif, `0.2.7-pre.002`.** Le gate `pre.001` est clos. `pre.002` matérialise les settings WebSocket Transport, la redaction URL, le discriminateur de protocole, les identités locales, les états lifecycle et les snapshots sûrs. Le socket physique reste différé à `pre.004`.
|
||||||
|
|
||||||
## 1. Objet et base vérifiée
|
## 1. Objet et base vérifiée
|
||||||
|
|
||||||
@@ -412,6 +412,40 @@ Les wrappers `WsProviderName`/`WsClusterName` sont parallèles aux `Http*` pour
|
|||||||
|
|
||||||
`WsSessionSettings` porte les bornes runtime nécessaires : command timeout, close timeout, reconnect, resubscribe, capacités de queues, maximum de subscriptions actives, maximum de requests JSON-RPC en vol, maximum de message/frame/write buffer. Pas de rôle HTTP ni de scheduler.
|
`WsSessionSettings` porte les bornes runtime nécessaires : command timeout, close timeout, reconnect, resubscribe, capacités de queues, maximum de subscriptions actives, maximum de requests JSON-RPC en vol, maximum de message/frame/write buffer. Pas de rôle HTTP ni de scheduler.
|
||||||
|
|
||||||
|
Checkpoint `pre.002` matérialisé :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsEndpointUrl
|
||||||
|
WsProviderName
|
||||||
|
WsClusterName
|
||||||
|
WsProtocolKind
|
||||||
|
WsReconnectSettings
|
||||||
|
WsResubscribePolicy
|
||||||
|
WsSessionSettings
|
||||||
|
WsEndpointSettings
|
||||||
|
WsTransportSettings
|
||||||
|
```
|
||||||
|
|
||||||
|
Defaults Transport initiaux, explicitement **policy KSP locale** et non limites Solana :
|
||||||
|
|
||||||
|
```text
|
||||||
|
command timeout 10 s
|
||||||
|
close timeout 5 s
|
||||||
|
reconnect retries 5
|
||||||
|
reconnect initial backoff 250 ms
|
||||||
|
reconnect maximum backoff 5 s
|
||||||
|
command queue 128
|
||||||
|
notification queue per sub 256
|
||||||
|
active subscriptions 1024
|
||||||
|
pending JSON-RPC requests 128
|
||||||
|
maximum message 64 MiB
|
||||||
|
maximum frame 16 MiB
|
||||||
|
maximum write buffer 1 MiB
|
||||||
|
resubscribe default ActiveSubscriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
Les bornes message/frame/write buffer sont seulement **contractuelles** dans `pre.002`; leur raccordement à la crate WebSocket et les tests oversized restent les gates `pre.004`/`pre.005`. Elles peuvent être recalibrées avant `rel.001` si les fixtures adversariales ou les formes `blockSubscribe` démontrent qu'une autre valeur KSP est préférable.
|
||||||
|
|
||||||
### 9.2 Cardinalité
|
### 9.2 Cardinalité
|
||||||
|
|
||||||
Contrat :
|
Contrat :
|
||||||
@@ -446,6 +480,8 @@ WsSubscriptionId # KSP local, stable malgré reconnect
|
|||||||
remote id u64 # strictement runtime/interne, remappable
|
remote id u64 # strictement runtime/interne, remappable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`pre.002` matérialise les deux IDs locaux comme wrappers de `NonZeroU64`. La génération est volontairement laissée au futur actor ; aucun compteur global ni singleton n'est introduit dans cette tranche.
|
||||||
|
|
||||||
Une `WsSubscription<T>` typée contient l'identité locale, la projection de lifecycle et un receiver bounded de notifications typées. L'ID remote ne devient pas un identifiant métier/public de contrôle.
|
Une `WsSubscription<T>` typée contient l'identité locale, la projection de lifecycle et un receiver bounded de notifications typées. L'ID remote ne devient pas un identifiant métier/public de contrôle.
|
||||||
|
|
||||||
## 10. State machines retenues
|
## 10. State machines retenues
|
||||||
@@ -680,7 +716,18 @@ remote request body complet
|
|||||||
|
|
||||||
Snapshot public cible : état session, endpoint name/provider/cluster, counts, reconnect state, continuity gaps, subscription IDs locaux + kinds + states. L'ID serveur peut rester interne ; un booléen `remote_bound` suffit pour diagnostiquer le binding sans faire croire à sa stabilité.
|
Snapshot public cible : état session, endpoint name/provider/cluster, counts, reconnect state, continuity gaps, subscription IDs locaux + kinds + states. L'ID serveur peut rester interne ; un booléen `remote_bound` suffit pour diagnostiquer le binding sans faire croire à sa stabilité.
|
||||||
|
|
||||||
Tests dédiés : URL `wss://user:pass@host/path?api-key=secret` ne doit apparaître ni dans `Debug`, ni erreurs, ni logs capturés, ni snapshots.
|
`pre.002` matérialise `WsSessionSnapshot` et `WsSubscriptionSnapshot`. Leurs constructeurs restent crate-internal : les consumers ne peuvent pas fabriquer une fausse projection runtime. Aucun des deux types ne contient URL, credential, request body, raw notification ou remote subscription id.
|
||||||
|
|
||||||
|
Observabilité `pre.002` :
|
||||||
|
|
||||||
|
- `constants.rs` conserve le `TRACING_TARGET = "ksp-onchain-transport-lib"` crate-wide ;
|
||||||
|
- toute émission ajoutée passe par les macros `ksp-logging-lib`, jamais par `tracing` directement ;
|
||||||
|
- `trace` couvre les validations de settings et metadata sûres ;
|
||||||
|
- `debug` confirme les settings validés avec uniquement des compteurs/bornes et descriptors sûrs ;
|
||||||
|
- `warn` qualifie les rejets de settings/URL sans inclure la valeur URL ;
|
||||||
|
- aucun `error` n'est ajouté artificiellement pour une simple erreur de validation caller. Les erreurs runtime terminales seront instrumentées quand l'actor/socket existera.
|
||||||
|
|
||||||
|
Tests dédiés : URL `wss://user:pass@host/path?api-key=secret` ne doit apparaître ni dans `Debug`, ni erreurs, ni logs capturés, ni snapshots. `pre.002` couvre déjà `Debug`, erreur de validation et forme des snapshots ; la capture de logs runtime reste à compléter avec l'actor.
|
||||||
|
|
||||||
## 16. DTOs et losslessness
|
## 16. DTOs et losslessness
|
||||||
|
|
||||||
@@ -805,7 +852,7 @@ L'inventaire officiel n'impose que 9 familles de subscriptions, mais le lifecycl
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
pre.001 audit interne/externe + matrice 18 méthodes + bot3 + dependencies + threat model + plan/sizing
|
pre.001 audit interne/externe + matrice 18 méthodes + bot3 + dependencies + threat model + plan/sizing
|
||||||
pre.002 settings WS Transport + URL redaction + IDs/states/snapshots + tests de settings
|
pre.002 DONE — settings WS Transport + URL redaction + IDs/states/snapshots + tests de settings
|
||||||
pre.003 std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings
|
pre.003 std.transport V2 HTTP+WS + backward V1 + discriminateur WS + schema/fixtures + Config -> WsTransportSettings
|
||||||
pre.004 deps tokio-tungstenite/futures-util + actor physique + handshake/read/write + pending JSON-RPC + serveur local
|
pre.004 deps tokio-tungstenite/futures-util + actor physique + handshake/read/write + pending JSON-RPC + serveur local
|
||||||
pre.005 limites frame/message/request + control frames + cancellation/close/shutdown + adversarial socket tests
|
pre.005 limites frame/message/request + control frames + cancellation/close/shutdown + adversarial socket tests
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<!-- file: docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md -->
|
<!-- file: docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Validation `0.2.7` — WebSocket Solana standard
|
# Validation `0.2.7` — WebSocket Solana standard
|
||||||
|
|
||||||
> **Statut : matrice initiale ouverte par `0.2.7-pre.001`.** Les colonnes de preuve seront consolidées au fil des prereleases puis fermées avant `0.2.7-rel.001`.
|
> **Statut : matrice active, `0.2.7-pre.002`.** Le gate normatif `pre.001` est clos ; les settings, identités, états lifecycle et snapshots sûrs sont maintenant matérialisés. Les preuves runtime socket/subscription restent ouvertes.
|
||||||
|
|
||||||
## 1. Baseline normative
|
## 1. Baseline normative
|
||||||
|
|
||||||
@@ -195,8 +195,8 @@ Les votes observés sont gossip/pre-consensus ; aucune garantie d'entrée dans l
|
|||||||
| plusieurs subs / session | registry actor par session | `pre.006` |
|
| plusieurs subs / session | registry actor par session | `pre.006` |
|
||||||
| ID public subscription | local KSP stable | `pre.006` |
|
| ID public subscription | local KSP stable | `pre.006` |
|
||||||
| ID serveur | éphémère interne et remappé | `pre.006`/`pre.007` |
|
| ID serveur | éphémère interne et remappé | `pre.006`/`pre.007` |
|
||||||
| session states | Disconnected/Connecting/Active/Reconnecting/Closing/Closed/Failed | `pre.002` |
|
| session states | Disconnected/Connecting/Active/Reconnecting/Closing/Closed/Failed | **Done `pre.002`** |
|
||||||
| subscription states | Requested/Active/Resubscribing/Cancelling/Closed/Failed | `pre.002` |
|
| subscription states | Requested/Active/Resubscribing/Cancelling/Closed/Failed | **Done `pre.002`** |
|
||||||
| reconnect | physique uniquement, budget/backoff finis | `pre.007` |
|
| reconnect | physique uniquement, budget/backoff finis | `pre.007` |
|
||||||
| resubscribe | policy `Never` ou `ActiveSubscriptions`, ordre local déterministe | `pre.007` |
|
| resubscribe | policy `Never` ou `ActiveSubscriptions`, ordre local déterministe | `pre.007` |
|
||||||
| continuity | gap observable, aucune promesse lossless | `pre.007` |
|
| continuity | gap observable, aucune promesse lossless | `pre.007` |
|
||||||
@@ -207,21 +207,21 @@ Les votes observés sont gossip/pre-consensus ; aucune garantie d'entrée dans l
|
|||||||
## 7. Threat/security compliance initiale
|
## 7. Threat/security compliance initiale
|
||||||
|
|
||||||
| Invariant | Preuve attendue | Statut |
|
| Invariant | Preuve attendue | Statut |
|
||||||
|---------------------------------------------------|-----------------------------------|---------|
|
|---------------------------------------------------|---------------------------------------------|--------------------|
|
||||||
| URL/credentials absents de `Debug` | unit tests URL wrapper | Planned |
|
| URL/credentials absents de `Debug` | unit tests URL wrapper | **Done `pre.002`** |
|
||||||
| URL/credentials absents des erreurs | adversarial connection errors | Planned |
|
| URL/credentials absents des erreurs | validation URL + future connection errors | Partial `pre.002` |
|
||||||
| URL/credentials absents des logs | writer/capture KSP logging | Planned |
|
| URL/credentials absents des logs | safe fields `pre.002`, capture actor future | Partial `pre.002` |
|
||||||
| snapshots sans URL/raw payload | public API canary | Planned |
|
| snapshots sans URL/raw payload | unit shape + public contract | **Done `pre.002`** |
|
||||||
| frame/message finis | oversized server fixture | Planned |
|
| frame/message finis | settings bornés, enforcement socket futur | Partial `pre.002` |
|
||||||
| JSON borné indirectement par message | oversized + malformed fixture | Planned |
|
| JSON borné indirectement par message | oversized + malformed fixture | Planned |
|
||||||
| queues notifications bornées | slow consumer fixture | Planned |
|
| queues notifications bornées | capacité settings, channel futur | Partial `pre.002` |
|
||||||
| pending RPC borné + timeout | no-response fixture | Planned |
|
| pending RPC borné + timeout | settings bornés, runtime futur | Partial `pre.002` |
|
||||||
| reconnect loop bornée | repeated disconnect fixture | Planned |
|
| reconnect loop bornée | repeated disconnect fixture | Planned |
|
||||||
| unsubscribe pendant reconnect ne resubscribe pas | race fixture | Planned |
|
| unsubscribe pendant reconnect ne resubscribe pas | race fixture | Planned |
|
||||||
| signature terminale ne resubscribe pas | terminal fixture | Planned |
|
| signature terminale ne resubscribe pas | terminal fixture | Planned |
|
||||||
| shutdown ne bloque pas | peer hostile/no close ack fixture | Planned |
|
| shutdown ne bloque pas | peer hostile/no close ack fixture | Planned |
|
||||||
| no Store/Program/Wallet/Config dep dans Transport | cargo tree + source canary | Planned |
|
| no Store/Program/Wallet/Config dep dans Transport | cargo tree + source canary | Planned |
|
||||||
| no direct `tracing` dans Transport | workspace audit/canary | Planned |
|
| no direct `tracing` dans Transport | workspace audit + source audit | **Done `pre.002`** |
|
||||||
|
|
||||||
## 8. Dependency compliance initiale
|
## 8. Dependency compliance initiale
|
||||||
|
|
||||||
@@ -268,6 +268,51 @@ Le type Transport correspondant au discriminateur est prévu `#[non_exhaustive]`
|
|||||||
|
|
||||||
La schema V1 n'est pas assouplie. Une schema V2 explicite remplace la fixture standard au moment où l'adapter est matérialisé.
|
La schema V1 n'est pas assouplie. Une schema V2 explicite remplace la fixture standard au moment où l'adapter est matérialisé.
|
||||||
|
|
||||||
|
## 9.1 Checkpoint settings/lifecycle `pre.002`
|
||||||
|
|
||||||
|
Surface publique matérialisée :
|
||||||
|
|
||||||
|
```text
|
||||||
|
WsEndpointUrl
|
||||||
|
WsProviderName
|
||||||
|
WsClusterName
|
||||||
|
WsProtocolKind::SolanaStandard
|
||||||
|
WsReconnectSettings
|
||||||
|
WsResubscribePolicy::{Never, ActiveSubscriptions}
|
||||||
|
WsSessionSettings
|
||||||
|
WsEndpointSettings
|
||||||
|
WsTransportSettings
|
||||||
|
WsSessionId
|
||||||
|
WsSubscriptionId
|
||||||
|
WsSessionState
|
||||||
|
WsSubscriptionState
|
||||||
|
WsSubscriptionKind
|
||||||
|
WsSessionSnapshot
|
||||||
|
WsSubscriptionSnapshot
|
||||||
|
```
|
||||||
|
|
||||||
|
Gates déterministes ajoutés :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ws:// et wss:// acceptés
|
||||||
|
HTTP rejeté par WsEndpointUrl
|
||||||
|
Debug URL redacted
|
||||||
|
erreur de scheme sans URL/credential
|
||||||
|
settings session default bornés
|
||||||
|
zero runtime bound rejeté
|
||||||
|
reconnect backoff inversé rejeté
|
||||||
|
endpoint names uniques
|
||||||
|
au moins un endpoint enabled
|
||||||
|
Debug WsTransportSettings sans URL/credential
|
||||||
|
9 familles standard WsSubscriptionKind
|
||||||
|
snapshots sans URL ni remote subscription id
|
||||||
|
public API canary crate-root
|
||||||
|
```
|
||||||
|
|
||||||
|
Logging : `TRACING_TARGET` reste défini dans `constants.rs`; les nouveaux événements utilisent exclusivement `ksp_logging_lib::trace!`, `debug!` et `warn!` avec des fields sûrs. Aucun `tracing` direct n'est ajouté.
|
||||||
|
|
||||||
|
Les defaults de taille/queue sont des **policies KSP locales**, pas des limites Solana. Leur enforcement réel et leurs tests oversized/slow-consumer restent attendus dans les tranches socket/backpressure.
|
||||||
|
|
||||||
## 10. Validation du gate `pre.001`
|
## 10. Validation du gate `pre.001`
|
||||||
|
|
||||||
Exécuté dans le sandbox :
|
Exécuté dans le sandbox :
|
||||||
|
|||||||
Reference in New Issue
Block a user