use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use hyphae::LeftSemiJoinExt;
use hyphae::MapExt;
use myko_macros::{myko_command, myko_report, myko_report_output, myko_view};
use crate::{
entities::server::{Server, ServerId},
prelude::*,
report::{ReportContext, ReportHandler},
server::client_registry,
};
#[myko_item]
#[derive(Eq)]
pub struct Client {
#[belongs_to(Server)]
pub server_id: ServerId,
#[serde(default)]
pub address: Option<Arc<str>>,
pub windback: Option<Arc<str>>,
}
crate::mark_framework_typegen_type!(ClientId);
#[myko_view(Client)]
pub struct ConnectedClients {}
impl ViewHandler for ConnectedClients {
#[cfg(not(target_arch = "wasm32"))]
fn build_cell(
ctx: ViewBuildArgs<Self>,
) -> impl MapQuery<Key = Arc<str>, Value = Arc<Self::Item>>
where
Self: Send + Sync + 'static,
{
ctx.view_context
.query_map_by_str(GetAllClients {})
.left_semi_join_by(
client_registry().connected_ids(),
|client_id, _| client_id.clone(),
|client_id, ()| client_id.clone(),
)
}
}
#[myko_report_output]
#[derive(Eq)]
pub struct ClientStatusOutput {
pub online: bool,
}
#[myko_report(ClientStatusOutput)]
pub struct ClientStatus {
pub client_id: ClientId,
}
impl ReportHandler for ClientStatus {
type Output = ClientStatusOutput;
fn compute(&self, _ctx: ReportContext) -> impl Materialize<Arc<Self::Output>, Definite> {
let client_id: Arc<str> = self.client_id.clone().into();
client_registry()
.watch_connected(&client_id)
.map(|online| Arc::new(ClientStatusOutput { online: *online }))
.materialize()
}
}
#[myko_report_output]
#[derive(Eq)]
pub struct WindbackStatusOutput {
pub windback: Option<Arc<str>>,
}
#[myko_report(WindbackStatusOutput)]
pub struct WindbackStatus {}
impl ReportHandler for WindbackStatus {
type Output = WindbackStatusOutput;
fn compute(&self, ctx: ReportContext) -> impl Materialize<Arc<Self::Output>, Definite> {
let client_id = ctx.client_id().map_or_else(Arc::<str>::default, Arc::from);
let store = ctx.registry().get_or_create(Client::ENTITY_NAME_STATIC);
store.get(&client_id).map(|client| {
let windback = client
.as_ref()
.and_then(|client| client.as_any().downcast_ref::<Client>())
.and_then(|client| client.windback.clone());
Arc::new(WindbackStatusOutput { windback })
})
}
}
#[myko_command(bool)]
pub struct SetClientWindbackTime {
pub windback: Arc<str>,
}
impl crate::command::CommandHandler for SetClientWindbackTime {
fn execute(
self,
ctx: crate::command::CommandContext,
) -> Result<bool, crate::command::CommandError> {
let client_id = ctx.client_id().ok_or_else(|| {
crate::command::CommandError::new(
ctx.tx(),
"SetClientWindbackTime",
"No client_id in context - windback requires a WebSocket connection",
)
})?;
let client = ctx
.exec_report(GetClientById {
id: ClientId::from(Arc::<str>::from(client_id)),
})?
.ok_or_else(|| {
CommandError::new(
ctx.tx(),
"SetClientWindbackTime",
format!("Client {client_id} not found"),
)
})?;
let updated_client = Client {
id: client.id.clone(),
server_id: client.server_id.clone(),
address: client.address.clone(),
windback: Some(self.windback),
};
ctx.emit_set(&updated_client)?;
Ok(true)
}
}
#[myko_command(bool)]
pub struct ClearClientWindbackTime {}
impl crate::command::CommandHandler for ClearClientWindbackTime {
fn execute(
self,
ctx: crate::command::CommandContext,
) -> Result<bool, crate::command::CommandError> {
let client_id = ctx.client_id().ok_or_else(|| {
crate::command::CommandError::new(
ctx.tx(),
"ClearClientWindbackTime",
"No client_id in context - windback requires a WebSocket connection",
)
})?;
let client = ctx
.exec_report(GetClientById {
id: ClientId::from(Arc::<str>::from(client_id)),
})?
.ok_or_else(|| {
CommandError::new(
ctx.tx(),
"ClearClientWindbackTime",
format!("Client {client_id} not found"),
)
})?;
let updated_client = Client {
id: client.id.clone(),
server_id: client.server_id.clone(),
address: client.address.clone(),
windback: None,
};
ctx.emit_set(&updated_client)?;
Ok(true)
}
}
#[cfg(test)]
mod indexed_join_tests {
use hyphae::{CellMap, LeftJoinExt, MapQuery};
use super::*;
#[test]
#[allow(clippy::similar_names)]
fn generated_relation_indexes_arc_backed_query_rows() {
let servers = CellMap::<Arc<str>, Arc<Server>>::new();
let clients = CellMap::<Arc<str>, Arc<Client>>::new();
let joined = servers
.clone()
.left_join_fk::<ClientServerIdRelation, _>(clients.clone())
.materialize();
let server_a_id: Arc<str> = "server-a".into();
let server_b_id: Arc<str> = "server-b".into();
servers.insert(
server_a_id.clone(),
Arc::new(Server {
id: ServerId::from(server_a_id.clone()),
version: "test".to_string(),
address: "127.0.0.1".to_string(),
port: 1,
started_at: "1970-01-01T00:00:00Z".to_string(),
}),
);
servers.insert(
server_b_id.clone(),
Arc::new(Server {
id: ServerId::from(server_b_id.clone()),
version: "test".to_string(),
address: "127.0.0.1".to_string(),
port: 2,
started_at: "1970-01-01T00:00:00Z".to_string(),
}),
);
let client_id: Arc<str> = "client".into();
let client = |server_id: Arc<str>| {
Arc::new(Client {
id: ClientId::from(client_id.clone()),
server_id: ServerId::from(server_id),
address: None,
windback: None,
})
};
clients.insert(client_id.clone(), client(server_a_id.clone()));
assert!(matches!(
joined.get_value(&server_a_id),
Some((_, rows)) if rows.len() == 1
));
assert!(matches!(
joined.get_value(&server_b_id),
Some((_, rows)) if rows.is_empty()
));
clients.insert(client_id.clone(), client(server_b_id.clone()));
assert!(matches!(
joined.get_value(&server_a_id),
Some((_, rows)) if rows.is_empty()
));
assert!(matches!(
joined.get_value(&server_b_id),
Some((_, rows)) if rows.len() == 1
));
}
}