use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use holo_hash::DnaHash;
use holochain_timestamp::Timestamp;
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
pub fn kitsune_id_to_base64url(id: &kitsune2_api::Id) -> String {
URL_SAFE_NO_PAD.encode(&id.0)
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Kitsune2NetworkMetricsRequest {
pub dna_hash: Option<DnaHash>,
pub include_dht_summary: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct Kitsune2NetworkMetrics {
pub fetch_state_summary: FetchStateSummary,
pub gossip_state_summary: GossipStateSummary,
pub local_agents: Vec<LocalAgentSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct LocalAgentSummary {
pub agent: holo_hash::AgentPubKey,
pub storage_arc: DhtArc,
pub target_arc: DhtArc,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct HolochainTransportStats {
pub transport_stats: TransportStats,
#[cfg_attr(feature = "ts_rs", ts(as = "BlockedMessageCountsMapTs"))]
pub blocked_message_counts: HashMap<String, HashMap<DnaHash, MessageBlockCount>>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(untagged)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub enum DhtArc {
#[default]
Empty,
Arc(u32, u32),
}
impl From<kitsune2_api::DhtArc> for DhtArc {
fn from(a: kitsune2_api::DhtArc) -> Self {
match a {
kitsune2_api::DhtArc::Empty => Self::Empty,
kitsune2_api::DhtArc::Arc(lo, hi) => Self::Arc(lo, hi),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct FetchStateSummary {
pub pending_requests: HashMap<String, Vec<String>>,
}
impl From<kitsune2_api::FetchStateSummary> for FetchStateSummary {
fn from(s: kitsune2_api::FetchStateSummary) -> Self {
Self {
pending_requests: s
.pending_requests
.into_iter()
.map(|(op_id, urls)| {
let urls = urls.into_iter().map(|u| u.as_str().to_string()).collect();
(kitsune_id_to_base64url(&op_id), urls)
})
.collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct GossipRoundStateSummary {
pub session_with_peer: String,
}
impl From<kitsune2_api::GossipRoundStateSummary> for GossipRoundStateSummary {
fn from(s: kitsune2_api::GossipRoundStateSummary) -> Self {
Self {
session_with_peer: s.session_with_peer.as_str().to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct DhtSegmentState {
#[serde(with = "serde_bytes")]
#[cfg_attr(feature = "ts_rs", ts(type = "Uint8Array"))]
pub disc_top_hash: Vec<u8>,
pub disc_boundary: Timestamp,
#[cfg_attr(feature = "ts_rs", ts(type = "Uint8Array[]"))]
pub ring_top_hashes: Vec<serde_bytes::ByteBuf>,
}
impl From<kitsune2_api::DhtSegmentState> for DhtSegmentState {
fn from(s: kitsune2_api::DhtSegmentState) -> Self {
Self {
disc_top_hash: s.disc_top_hash.to_vec(),
disc_boundary: Timestamp::from_micros(s.disc_boundary.as_micros()),
ring_top_hashes: s
.ring_top_hashes
.into_iter()
.map(|h| serde_bytes::ByteBuf::from(h.to_vec()))
.collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct PeerMeta {
pub last_gossip_timestamp: Option<Timestamp>,
pub new_ops_bookmark: Option<Timestamp>,
pub peer_behavior_errors: Option<u32>,
pub local_errors: Option<u32>,
pub peer_busy: Option<u32>,
pub peer_terminated: Option<u32>,
pub completed_rounds: Option<u32>,
pub peer_timeouts: Option<u32>,
pub dht_op_count: Option<u64>,
pub is_tombstone: bool,
pub storage_arc: DhtArc,
}
impl From<kitsune2_api::PeerMeta> for PeerMeta {
fn from(p: kitsune2_api::PeerMeta) -> Self {
Self {
last_gossip_timestamp: p
.last_gossip_timestamp
.map(|t| Timestamp::from_micros(t.as_micros())),
new_ops_bookmark: p
.new_ops_bookmark
.map(|t| Timestamp::from_micros(t.as_micros())),
peer_behavior_errors: p.peer_behavior_errors,
local_errors: p.local_errors,
peer_busy: p.peer_busy,
peer_terminated: p.peer_terminated,
completed_rounds: p.completed_rounds,
peer_timeouts: p.peer_timeouts,
dht_op_count: p.dht_op_count,
is_tombstone: p.is_tombstone,
storage_arc: p.storage_arc.into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct GossipStateSummary {
pub initiated_round: Option<GossipRoundStateSummary>,
pub accepted_rounds: Vec<GossipRoundStateSummary>,
pub dht_summary: HashMap<String, DhtSegmentState>,
pub peer_meta: HashMap<String, PeerMeta>,
pub local_op_count: u64,
}
impl From<kitsune2_api::GossipStateSummary> for GossipStateSummary {
fn from(s: kitsune2_api::GossipStateSummary) -> Self {
Self {
initiated_round: s.initiated_round.map(Into::into),
accepted_rounds: s.accepted_rounds.into_iter().map(Into::into).collect(),
dht_summary: s
.dht_summary
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
peer_meta: s
.peer_meta
.into_iter()
.map(|(url, meta)| (url.as_str().to_string(), meta.into()))
.collect(),
local_op_count: s.local_op_count,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct TransportConnectionStats {
pub pub_key: String,
pub send_message_count: u64,
pub send_bytes: u64,
pub recv_message_count: u64,
pub recv_bytes: u64,
pub opened_at_s: u64,
pub is_direct: bool,
}
impl From<kitsune2_api::TransportConnectionStats> for TransportConnectionStats {
fn from(s: kitsune2_api::TransportConnectionStats) -> Self {
Self {
pub_key: s.pub_key,
send_message_count: s.send_message_count,
send_bytes: s.send_bytes,
recv_message_count: s.recv_message_count,
recv_bytes: s.recv_bytes,
opened_at_s: s.opened_at_s,
is_direct: s.is_direct,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct TransportStats {
pub backend: String,
pub peer_urls: Vec<String>,
pub connections: Vec<TransportConnectionStats>,
}
impl From<kitsune2_api::TransportStats> for TransportStats {
fn from(s: kitsune2_api::TransportStats) -> Self {
Self {
backend: s.backend,
peer_urls: s
.peer_urls
.into_iter()
.map(|u| u.as_str().to_string())
.collect(),
connections: s.connections.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "ts_rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts_rs", ts(export_to = "api/admin/types.ts"))]
pub struct MessageBlockCount {
pub incoming: u32,
pub outgoing: u32,
}
impl From<kitsune2_api::MessageBlockCount> for MessageBlockCount {
fn from(c: kitsune2_api::MessageBlockCount) -> Self {
Self {
incoming: c.incoming,
outgoing: c.outgoing,
}
}
}
#[cfg(feature = "ts_rs")]
holo_hash::ts_alias!(
BlockedMessageCountsMapTs,
"BlockedMessageCountsMap",
"Record<string, Record<string, MessageBlockCount>>",
"api/admin/types.ts",
deps: [MessageBlockCount]
);
#[cfg(test)]
mod wire_compat {
use super::*;
fn round_trip<K: serde::Serialize, M: serde::de::DeserializeOwned + serde::Serialize>(k: &K) {
let wire = rmp_serde::to_vec_named(k).unwrap();
let mirror: M = rmp_serde::from_slice(&wire).unwrap();
assert_eq!(rmp_serde::to_vec_named(&mirror).unwrap(), wire);
}
#[test]
fn dht_arc_wire_compat() {
round_trip::<_, DhtArc>(&kitsune2_api::DhtArc::Empty);
round_trip::<_, DhtArc>(&kitsune2_api::DhtArc::Arc(0, u32::MAX));
}
#[test]
fn fetch_state_summary_wire_compat() {
let mut pending_requests = HashMap::new();
pending_requests.insert(
kitsune2_api::OpId::from(bytes::Bytes::from_static(b"op-1")),
vec![kitsune2_api::Url::from_str("wss://test.com:443").unwrap()],
);
round_trip::<_, FetchStateSummary>(&kitsune2_api::FetchStateSummary { pending_requests });
}
#[test]
fn gossip_state_summary_wire_compat() {
let peer_url = kitsune2_api::Url::from_str("wss://test.com:443").unwrap();
let mut dht_summary = HashMap::new();
dht_summary.insert(
"segment-0".to_string(),
kitsune2_api::DhtSegmentState {
disc_top_hash: bytes::Bytes::from_static(b"top-hash"),
disc_boundary: kitsune2_api::Timestamp::from_micros(42),
ring_top_hashes: vec![bytes::Bytes::from_static(b"ring-hash")],
},
);
let mut peer_meta = HashMap::new();
peer_meta.insert(
peer_url.clone(),
kitsune2_api::PeerMeta {
last_gossip_timestamp: Some(kitsune2_api::Timestamp::from_micros(1)),
new_ops_bookmark: Some(kitsune2_api::Timestamp::from_micros(2)),
peer_behavior_errors: Some(1),
local_errors: Some(2),
peer_busy: Some(3),
peer_terminated: Some(4),
completed_rounds: Some(5),
peer_timeouts: Some(6),
dht_op_count: Some(7),
is_tombstone: false,
storage_arc: kitsune2_api::DhtArc::Arc(0, u32::MAX),
},
);
let summary = kitsune2_api::GossipStateSummary {
initiated_round: Some(kitsune2_api::GossipRoundStateSummary {
session_with_peer: peer_url.clone(),
}),
accepted_rounds: vec![kitsune2_api::GossipRoundStateSummary {
session_with_peer: peer_url,
}],
dht_summary,
peer_meta,
local_op_count: 99,
};
round_trip::<_, GossipStateSummary>(&summary);
}
#[test]
fn transport_stats_wire_compat() {
let stats = kitsune2_api::TransportStats {
backend: "test-backend".to_string(),
peer_urls: vec![kitsune2_api::Url::from_str("wss://test.com:443").unwrap()],
connections: vec![kitsune2_api::TransportConnectionStats {
pub_key: "pub-key".to_string(),
send_message_count: 1,
send_bytes: 2,
recv_message_count: 3,
recv_bytes: 4,
opened_at_s: 5,
is_direct: true,
}],
};
round_trip::<_, TransportStats>(&stats);
}
#[test]
fn message_block_count_wire_compat() {
round_trip::<_, MessageBlockCount>(&kitsune2_api::MessageBlockCount {
incoming: 1,
outgoing: 2,
});
}
#[test]
fn kitsune_id_to_base64url_matches_kitsune2_serialize() {
let raw = bytes::Bytes::from_static(b"kitsune-id-raw-bytes-fixture");
let op_id = kitsune2_api::OpId::from(raw.clone());
let agent_id = kitsune2_api::AgentId::from(raw.clone());
let space_id = kitsune2_api::SpaceId::from(raw.clone());
let expected = URL_SAFE_NO_PAD.encode(&raw);
assert_eq!(kitsune_id_to_base64url(&op_id), expected);
assert_eq!(kitsune_id_to_base64url(&agent_id), expected);
assert_eq!(kitsune_id_to_base64url(&space_id), expected);
let op_id_wire: String =
rmp_serde::from_slice(&rmp_serde::to_vec_named(&op_id).unwrap()).unwrap();
let agent_id_wire: String =
rmp_serde::from_slice(&rmp_serde::to_vec_named(&agent_id).unwrap()).unwrap();
let space_id_wire: String =
rmp_serde::from_slice(&rmp_serde::to_vec_named(&space_id).unwrap()).unwrap();
assert_eq!(op_id_wire, expected);
assert_eq!(agent_id_wire, expected);
assert_eq!(space_id_wire, expected);
}
#[test]
fn kitsune_id_to_base64url_ignores_display_override() {
let override_installed = kitsune2_api::OpId::set_global_display_callback(|_bytes, f| {
f.write_str("not-the-wire-value")
});
let raw = bytes::Bytes::from_static(b"op-id-under-display-override");
let op_id = kitsune2_api::OpId::from(raw.clone());
if override_installed {
assert_eq!(op_id.to_string(), "not-the-wire-value");
}
assert_eq!(
kitsune_id_to_base64url(&op_id),
URL_SAFE_NO_PAD.encode(&raw)
);
}
#[test]
fn fetch_state_summary_from_uses_wire_bytes_for_op_id_key() {
let raw = bytes::Bytes::from_static(b"fetch-state-summary-op-id");
let op_id = kitsune2_api::OpId::from(raw.clone());
let url = kitsune2_api::Url::from_str("wss://test.com:443").unwrap();
let mut pending_requests = HashMap::new();
pending_requests.insert(op_id, vec![url.clone()]);
let mirror: FetchStateSummary = kitsune2_api::FetchStateSummary { pending_requests }.into();
let expected_key = URL_SAFE_NO_PAD.encode(&raw);
assert_eq!(
mirror.pending_requests.get(&expected_key),
Some(&vec![url.as_str().to_string()])
);
}
}
#[cfg(all(test, feature = "ts_rs"))]
mod ts_export {
use super::*;
use ts_rs::TS;
#[test]
fn dht_arc_untagged_shape_is_null_or_pair() {
let cfg = ts_rs::Config::default();
assert_eq!(DhtArc::inline(&cfg), "null | [number, number]");
}
}