use std::{
fmt,
sync::{Arc, OnceLock},
};
use hyphae::{Cell, CellImmutable, CellMap, MapExt, Materialize};
#[cfg(not(target_arch = "wasm32"))]
use hyphae::{MapEntriesExt, MapQuery};
use serde::Serialize;
use super::WsWriter;
use crate::{
command::{CommandId, CommandRequest},
wire::{MykoMessage, encode_command_message},
};
pub struct ClientRegistry {
writers: CellMap<Arc<str>, RegisteredWriter>,
}
#[derive(Clone)]
struct RegisteredWriter(Arc<dyn WsWriter>);
impl fmt::Debug for RegisteredWriter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("RegisteredWriter").finish()
}
}
impl PartialEq for RegisteredWriter {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl ClientRegistry {
fn new() -> Self {
Self {
writers: CellMap::new().with_name("client_registry"),
}
}
pub fn register(&self, client_id: Arc<str>, writer: Arc<dyn WsWriter>) {
self.writers.insert(client_id, RegisteredWriter(writer));
}
pub fn unregister(&self, client_id: &str) {
self.writers.remove(&Arc::<str>::from(client_id));
}
#[must_use]
pub(crate) fn watch_connected(&self, client_id: &Arc<str>) -> Cell<bool, CellImmutable> {
self.writers
.get(client_id)
.map(Option::is_some)
.materialize()
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn connected_ids(&self) -> impl MapQuery<Key = Arc<str>, Value = ()> + use<> {
self.writers
.clone()
.map_entries(|client_id, _| (client_id.clone(), ()))
}
#[must_use]
pub fn send_to(&self, client_id: &str, msg: MykoMessage) -> bool {
self.writers
.get_value(&Arc::<str>::from(client_id))
.is_some_and(|writer| {
writer.0.send(msg);
true
})
}
pub fn send_command_request_to<C>(&self, client_id: &str, request: &CommandRequest<C>) -> bool
where
C: CommandId + Serialize,
{
let Some(writer) = self.writers.get_value(&Arc::<str>::from(client_id)) else {
return false;
};
let command_id = request.command_id().to_string();
let protocol = writer.0.protocol();
match encode_command_message(protocol, request) {
Ok(payload) => {
writer
.0
.send_serialized_command(request.tx.clone(), command_id, payload);
true
}
Err(err) => {
tracing::error!(
"Failed to serialize command {} for client {}: {}",
request.command_id(),
client_id,
err
);
false
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.writers.keys_snapshot().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.writers.is_empty()
}
}
static CLIENT_REGISTRY: OnceLock<Arc<ClientRegistry>> = OnceLock::new();
pub fn init_client_registry() {
let _ = CLIENT_REGISTRY.set(Arc::new(ClientRegistry::new()));
}
pub fn client_registry() -> Arc<ClientRegistry> {
CLIENT_REGISTRY
.get_or_init(|| Arc::new(ClientRegistry::new()))
.clone()
}
pub fn try_client_registry() -> Option<Arc<ClientRegistry>> {
CLIENT_REGISTRY.get().cloned()
}
#[cfg(test)]
mod liveness_tests {
use super::*;
use hyphae::Gettable;
use crate::server::client_session::WsWriter;
use crate::wire::message::MykoMessage;
struct NullWriter;
impl WsWriter for NullWriter {
fn send(&self, _msg: MykoMessage) {}
fn send_serialized_command(
&self,
_tx: Arc<str>,
_command_id: String,
_payload: crate::wire::command::EncodedCommandMessage,
) {
}
}
#[test]
fn cellmap_liveness_reflects_registered_writers() {
let registry = ClientRegistry::new();
let client_id = Arc::<str>::from("a");
let connected = registry.watch_connected(&client_id);
assert!(!connected.get());
registry.register("a".into(), Arc::new(NullWriter));
assert!(connected.get());
registry.unregister("a");
assert!(!connected.get());
}
}