use std::collections::HashMap;
use chia_traits::Streamable;
use crate::envelope::{InteractionShape, MessageType};
use crate::error::{MessageError, Result};
pub const BAND_CORE: u32 = 0x0000_0000;
pub const BAND_PEER_RPC: u32 = 0x0000_0100;
pub const BAND_DIG_CHAT: u32 = 0x0000_0200;
pub const BAND_DIG_EMAIL: u32 = 0x0000_0300;
pub const BAND_DIG_VIDEO: u32 = 0x0000_0400;
pub const BAND_PRESENCE: u32 = 0x0000_0500;
pub const BAND_IPC: u32 = 0x0000_0600;
pub const BAND_SOCIAL_GRAPH: u32 = 0x0000_0700;
pub const BAND_RELAY_CONTROL: u32 = 0x0000_0800;
pub const BAND_RELAY_MESH: u32 = 0x0000_0900;
pub const BAND_EXPERIMENTAL: u32 = 0x1000_0000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MessageBand {
Core,
PeerRpc,
DigChat,
DigEmail,
DigVideo,
Presence,
Ipc,
SocialGraph,
RelayControl,
RelayMesh,
Experimental,
Reserved,
}
impl MessageType {
pub const CORE_HANDSHAKE: MessageType = MessageType(BAND_CORE);
pub const CORE_ACK: MessageType = MessageType(BAND_CORE + 1);
pub const CORE_ERROR: MessageType = MessageType(BAND_CORE + 2);
pub const CORE_KEEPALIVE: MessageType = MessageType(BAND_CORE + 3);
#[must_use]
pub fn band(self) -> MessageBand {
match self.0 {
0x0000_0000..=0x0000_00FF => MessageBand::Core,
0x0000_0100..=0x0000_01FF => MessageBand::PeerRpc,
0x0000_0200..=0x0000_02FF => MessageBand::DigChat,
0x0000_0300..=0x0000_03FF => MessageBand::DigEmail,
0x0000_0400..=0x0000_04FF => MessageBand::DigVideo,
0x0000_0500..=0x0000_05FF => MessageBand::Presence,
0x0000_0600..=0x0000_06FF => MessageBand::Ipc,
0x0000_0700..=0x0000_07FF => MessageBand::SocialGraph,
0x0000_0800..=0x0000_08FF => MessageBand::RelayControl,
0x0000_0900..=0x0000_09FF => MessageBand::RelayMesh,
0x1000_0000..=0xFFFF_FFFF => MessageBand::Experimental,
_ => MessageBand::Reserved,
}
}
}
pub trait MessageKind {
const TYPE_ID: MessageType;
type Payload: Streamable;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dispatch {
Handled,
Dropped,
}
type Handler = Box<dyn Fn(&[u8]) -> Result<()> + Send + Sync>;
#[derive(Default)]
pub struct MessageRegistry {
handlers: HashMap<MessageType, Handler>,
}
impl MessageRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register<K, F>(&mut self, handler: F) -> Result<()>
where
K: MessageKind,
F: Fn(K::Payload) -> Result<()> + Send + Sync + 'static,
{
let type_id = K::TYPE_ID;
if self.handlers.contains_key(&type_id) {
return Err(MessageError::DuplicateType(type_id.0));
}
let handler: Handler = Box::new(move |bytes: &[u8]| {
let payload =
K::Payload::from_bytes(bytes).map_err(|e| MessageError::Codec(e.to_string()))?;
handler(payload)
});
self.handlers.insert(type_id, handler);
Ok(())
}
#[must_use]
pub fn contains(&self, message_type: MessageType) -> bool {
self.handlers.contains_key(&message_type)
}
#[must_use]
pub fn len(&self) -> usize {
self.handlers.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.handlers.is_empty()
}
pub fn dispatch(
&self,
message_type: MessageType,
shape: InteractionShape,
payload: &[u8],
) -> Result<Dispatch> {
match self.handlers.get(&message_type) {
Some(handler) => handler(payload).map(|()| Dispatch::Handled),
None => match shape {
InteractionShape::Request | InteractionShape::StreamFrame => {
Err(MessageError::UnsupportedType(message_type.0))
}
InteractionShape::OneShot | InteractionShape::Response => Ok(Dispatch::Dropped),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chia_streamable_macro::Streamable;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Streamable)]
struct ChatText {
body: Vec<u8>,
}
struct ChatTextKind;
impl MessageKind for ChatTextKind {
const TYPE_ID: MessageType = MessageType(BAND_DIG_CHAT);
type Payload = ChatText;
}
#[derive(Debug, Clone, PartialEq, Eq, Streamable)]
struct Ping {
nonce: u64,
}
struct PingKind;
impl MessageKind for PingKind {
const TYPE_ID: MessageType = MessageType(BAND_PEER_RPC);
type Payload = Ping;
}
#[test]
fn register_then_dispatch_decodes_and_routes_to_the_handler() {
let seen = Arc::new(AtomicU32::new(0));
let sink = Arc::clone(&seen);
let mut registry = MessageRegistry::new();
registry
.register::<ChatTextKind, _>(move |msg: ChatText| {
sink.store(msg.body.len() as u32, Ordering::SeqCst);
Ok(())
})
.unwrap();
let payload = ChatText {
body: vec![1, 2, 3, 4, 5],
}
.to_bytes()
.unwrap();
let outcome = registry
.dispatch(ChatTextKind::TYPE_ID, InteractionShape::Request, &payload)
.unwrap();
assert_eq!(outcome, Dispatch::Handled);
assert_eq!(
seen.load(Ordering::SeqCst),
5,
"handler saw the decoded payload"
);
}
#[test]
fn unknown_type_request_returns_unsupported_type() {
let registry = MessageRegistry::new();
let unknown = MessageType(BAND_DIG_EMAIL + 0x42);
assert_eq!(
registry
.dispatch(unknown, InteractionShape::Request, &[])
.unwrap_err(),
MessageError::UnsupportedType(unknown.0)
);
}
#[test]
fn unknown_type_stream_frame_returns_unsupported_type() {
let registry = MessageRegistry::new();
let unknown = MessageType(BAND_DIG_VIDEO + 7);
assert_eq!(
registry
.dispatch(unknown, InteractionShape::StreamFrame, &[])
.unwrap_err(),
MessageError::UnsupportedType(unknown.0)
);
}
#[test]
fn unknown_type_one_shot_is_silently_dropped_not_an_error() {
let registry = MessageRegistry::new();
let unknown = MessageType(BAND_EXPERIMENTAL);
assert_eq!(
registry
.dispatch(unknown, InteractionShape::OneShot, &[])
.unwrap(),
Dispatch::Dropped
);
}
#[test]
fn unknown_type_response_is_silently_dropped() {
let registry = MessageRegistry::new();
let unknown = MessageType(BAND_PRESENCE + 1);
assert_eq!(
registry
.dispatch(unknown, InteractionShape::Response, &[])
.unwrap(),
Dispatch::Dropped
);
}
#[test]
fn registering_a_new_type_never_disturbs_existing_registrations() {
let mut registry = MessageRegistry::new();
registry
.register::<ChatTextKind, _>(|_: ChatText| Ok(()))
.unwrap();
assert!(registry.contains(ChatTextKind::TYPE_ID));
registry.register::<PingKind, _>(|_: Ping| Ok(())).unwrap();
assert_eq!(registry.len(), 2);
assert!(
registry.contains(ChatTextKind::TYPE_ID),
"the first type is undisturbed"
);
assert!(registry.contains(PingKind::TYPE_ID));
}
#[test]
fn re_registering_the_same_id_is_refused_additive_only() {
let mut registry = MessageRegistry::new();
registry
.register::<ChatTextKind, _>(|_: ChatText| Ok(()))
.unwrap();
assert_eq!(
registry
.register::<ChatTextKind, _>(|_: ChatText| Ok(()))
.unwrap_err(),
MessageError::DuplicateType(ChatTextKind::TYPE_ID.0)
);
assert_eq!(
registry.len(),
1,
"the failed re-registration left the table unchanged"
);
}
#[test]
fn a_handler_decode_failure_propagates_and_never_panics() {
let mut registry = MessageRegistry::new();
registry.register::<PingKind, _>(|_: Ping| Ok(())).unwrap();
let err = registry
.dispatch(PingKind::TYPE_ID, InteractionShape::Request, &[1, 2, 3])
.unwrap_err();
assert!(matches!(err, MessageError::Codec(_)));
}
#[test]
fn a_handler_reported_error_propagates() {
let mut registry = MessageRegistry::new();
registry
.register::<PingKind, _>(|_: Ping| Err(MessageError::UnsupportedType(0)))
.unwrap();
let payload = Ping { nonce: 9 }.to_bytes().unwrap();
assert!(registry
.dispatch(PingKind::TYPE_ID, InteractionShape::OneShot, &payload)
.is_err());
}
#[test]
fn every_reserved_band_classifies_at_its_boundaries() {
let cases = [
(BAND_CORE, MessageBand::Core),
(BAND_CORE + 0xFF, MessageBand::Core),
(BAND_PEER_RPC, MessageBand::PeerRpc),
(BAND_PEER_RPC + 0xFF, MessageBand::PeerRpc),
(BAND_DIG_CHAT, MessageBand::DigChat),
(BAND_DIG_CHAT + 0xFF, MessageBand::DigChat),
(BAND_DIG_EMAIL, MessageBand::DigEmail),
(BAND_DIG_VIDEO, MessageBand::DigVideo),
(BAND_PRESENCE, MessageBand::Presence),
(BAND_IPC, MessageBand::Ipc),
(BAND_IPC + 0xFF, MessageBand::Ipc),
(BAND_SOCIAL_GRAPH, MessageBand::SocialGraph),
(BAND_SOCIAL_GRAPH + 0xFF, MessageBand::SocialGraph),
(BAND_RELAY_CONTROL, MessageBand::RelayControl),
(BAND_RELAY_CONTROL + 0xFF, MessageBand::RelayControl),
(BAND_RELAY_MESH, MessageBand::RelayMesh),
(BAND_RELAY_MESH + 0xFF, MessageBand::RelayMesh),
(BAND_EXPERIMENTAL, MessageBand::Experimental),
(0xFFFF_FFFF, MessageBand::Experimental),
];
for (id, expected) in cases {
assert_eq!(MessageType(id).band(), expected, "id {id:#010x}");
}
}
#[test]
fn unallocated_ids_classify_as_reserved() {
for id in [0x0000_0A00, 0x0000_1000, 0x00FF_FFFF, 0x0FFF_FFFF] {
assert_eq!(
MessageType(id).band(),
MessageBand::Reserved,
"id {id:#010x}"
);
}
}
#[test]
fn named_core_type_constants_land_in_the_core_band() {
for mt in [
MessageType::CORE_HANDSHAKE,
MessageType::CORE_ACK,
MessageType::CORE_ERROR,
MessageType::CORE_KEEPALIVE,
] {
assert_eq!(mt.band(), MessageBand::Core);
}
}
}