use super::{AttachId, Protocol, Registry};
use fnv::FnvHashMap;
use rustrade_integration::protocol::websocket::WsMessage;
use serde_json::value::RawValue;
use tracing::{debug, warn};
#[derive(Debug)]
pub(crate) struct Element<'a, Slot> {
pub(crate) slot: Slot,
pub(crate) raw: &'a RawValue,
}
pub(crate) enum Elements<'a> {
Array(Vec<&'a RawValue>),
Closed(String),
Ignored,
}
pub(crate) fn elements<P: Protocol>(message: &WsMessage) -> Elements<'_> {
let text = match message {
WsMessage::Text(text) => text.as_str(),
WsMessage::Binary(bytes) => match std::str::from_utf8(bytes) {
Ok(text) => text,
Err(_) => {
warn!(
len = bytes.len(),
"{} sent a binary frame that is not UTF-8; ignored",
P::NAME,
);
return Elements::Ignored;
}
},
WsMessage::Close(frame) => {
return Elements::Closed(format!("closed by {}: {frame:?}", P::NAME));
}
_ => return Elements::Ignored,
};
match serde_json::from_str::<Vec<&RawValue>>(text) {
Ok(elements) => Elements::Array(elements),
Err(error) => {
warn!(
%error,
frame = %excerpt(text),
"{} sent a frame that is not a JSON array; ignored",
P::NAME,
);
Elements::Ignored
}
}
}
pub(crate) fn excerpt(text: &str) -> String {
text.chars().take(200).collect()
}
pub(crate) fn route<P: Protocol>(
registry: &mut Registry<P>,
frame: &WsMessage,
total: usize,
elements: &[Element<'_, P::Slot>],
) {
if elements.is_empty() {
return;
}
let mut counts = FnvHashMap::<AttachId, usize>::default();
for element in elements {
let Some(holders) = registry.holders(&element.slot) else {
debug!(
slot = ?element.slot,
"{} message for a subscription no stream holds",
P::NAME,
);
continue;
};
for id in holders {
*counts.entry(*id).or_default() += 1;
}
}
for (id, count) in counts {
let frame = if count == total {
frame.clone()
} else {
WsMessage::text(share(registry, id, elements))
};
registry.deliver(id, frame);
}
}
fn share<P: Protocol>(
registry: &Registry<P>,
id: AttachId,
elements: &[Element<'_, P::Slot>],
) -> String {
let capacity = elements
.iter()
.map(|element| element.raw.get().len() + 1)
.sum::<usize>()
+ 1;
let mut share = String::with_capacity(capacity);
share.push('[');
for element in elements.iter().filter(|element| {
registry
.holders(&element.slot)
.is_some_and(|holders| holders.contains(&id))
}) {
if share.len() > 1 {
share.push(',');
}
share.push_str(element.raw.get());
}
share.push(']');
share
}