v0.2.1-pre.005

This commit is contained in:
2026-08-17 20:27:44 +02:00
parent bc71fba289
commit ac1b1033c4
18 changed files with 1284 additions and 49 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-onchain-transport-lib/src/client.rs
// version: 2
// version: 3
/// Passive runtime availability reported for one logical HTTP endpoint or role.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -163,6 +163,26 @@ impl HttpEndpointSnapshot {
}
}
pub(crate) struct HttpEndpointHttpResponse {
status: u16,
retry_after: std::option::Option<std::time::Duration>,
body: std::vec::Vec<u8>,
}
impl HttpEndpointHttpResponse {
pub(crate) const fn status(&self) -> u16 {
return self.status;
}
pub(crate) const fn retry_after(&self) -> std::option::Option<std::time::Duration> {
return self.retry_after;
}
pub(crate) fn body(&self) -> &[u8] {
return self.body.as_slice();
}
}
/// Shareable logical HTTP endpoint client owned by KSP Transport.
///
/// The underlying `reqwest::Client` owns socket pooling. KSP keeps the configured URL private from diagnostics and exposes only safe routing metadata.
@@ -173,7 +193,7 @@ pub struct HttpEndpointClient {
struct HttpEndpointClientInner {
settings: crate::HttpEndpointSettings,
_client: reqwest::Client,
client: reqwest::Client,
role_runtimes: std::vec::Vec<std::sync::Arc<crate::resilience::HttpRoleRuntime>>,
}
@@ -212,7 +232,7 @@ impl HttpEndpointClient {
role_count = settings.roles().len(),
"created logical HTTP endpoint client"
);
return std::result::Result::Ok(Self { inner: std::sync::Arc::new(HttpEndpointClientInner { settings, _client: client, role_runtimes }) });
return std::result::Result::Ok(Self { inner: std::sync::Arc::new(HttpEndpointClientInner { settings, client, role_runtimes }) });
}
/// Returns the endpoint identity used for safe diagnostics and routing.
@@ -245,6 +265,36 @@ impl HttpEndpointClient {
return self.inner.settings.request_timeout();
}
pub(crate) async fn post_json_rpc(&self, payload: &str, timeout: std::time::Duration) -> ksp_core_lib::Result<HttpEndpointHttpResponse> {
if timeout.is_zero() {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TIMEOUT, "HTTP JSON-RPC request budget expired before dispatch")
.with_context("endpoint_name", self.name()),
);
}
let send_result = self
.inner
.client
.post(self.inner.settings.url().as_str())
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(payload.to_owned())
.timeout(timeout)
.send()
.await;
let response = match send_result {
std::result::Result::Ok(response) => response,
std::result::Result::Err(error) => return std::result::Result::Err(map_reqwest_error(self.name(), error)),
};
let status = response.status().as_u16();
let retry_after = parse_retry_after(response.headers());
let body_result = response.bytes().await;
let body = match body_result {
std::result::Result::Ok(body) => body.to_vec(),
std::result::Result::Err(error) => return std::result::Result::Err(map_reqwest_error(self.name(), error)),
};
return std::result::Result::Ok(HttpEndpointHttpResponse { status, retry_after, body });
}
/// Returns whether one enabled role can serve the requested capability structurally.
#[must_use]
pub fn supports(&self, role: &crate::HttpRoleName, request_kind: &crate::HttpRequestKind) -> bool {
@@ -410,6 +460,33 @@ fn fallback_role_snapshot(role: &crate::HttpEndpointRoleSettings, request_kinds:
};
}
fn map_reqwest_error(endpoint_name: &str, error: reqwest::Error) -> ksp_core_lib::Error {
let code = if error.is_timeout() {
crate::ERROR_CODE_TIMEOUT
} else if error.is_connect() {
crate::ERROR_CODE_HTTP_CONNECTION_FAILED
} else {
crate::ERROR_CODE_HTTP_REQUEST_FAILED
};
return ksp_core_lib::Error::new(code, "HTTP JSON-RPC request failed").with_context("endpoint_name", endpoint_name).with_source(error);
}
fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> std::option::Option<std::time::Duration> {
let value = match headers.get(reqwest::header::RETRY_AFTER) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::option::Option::None,
};
let text = match value.to_str() {
std::result::Result::Ok(text) => text.trim(),
std::result::Result::Err(_) => return std::option::Option::None,
};
let seconds = match text.parse::<u64>() {
std::result::Result::Ok(seconds) => seconds,
std::result::Result::Err(_) => return std::option::Option::None,
};
return std::option::Option::Some(std::time::Duration::from_secs(seconds));
}
fn build_reqwest_client(settings: &crate::HttpEndpointSettings) -> std::result::Result<reqwest::Client, reqwest::Error> {
let mut builder = reqwest::Client::builder()
.connect_timeout(settings.connect_timeout())