Skip to main content

myko/entities/
server.rs

1use std::sync::Arc;
2
3use hyphae::{LeftJoinExt, MapExt};
4
5use crate::{
6    entities::client::{ClientServerIdRelation, GetAllClients},
7    prelude::*,
8    report::{ReportContext, ReportHandler},
9};
10
11#[myko_item]
12#[derive(Eq)]
13pub struct Server {
14    pub version: String,
15    #[searchable]
16    pub address: String,
17    pub port: u16,
18    pub started_at: String, // ISO DateTime
19}
20
21#[myko_query(Server)]
22pub struct GetConnectedServer {}
23
24impl QueryHandler for GetConnectedServer {
25    fn test_entity(ctx: QueryTestContext<Self>) -> bool {
26        let item_id = ctx.item.id.to_string();
27        let host_id = ctx.query_context.req.host_id.to_string();
28        item_id == host_id
29    }
30
31    #[cfg(not(target_arch = "wasm32"))]
32    fn build_view(
33        ctx: QueryBuildArgs<Self>,
34    ) -> Option<impl MapQuery<Key = Arc<str>, Value = Arc<dyn AnyItem>>>
35    where
36        Self: Send + Sync + 'static,
37    {
38        let host_id: Arc<str> = ctx
39            .query_context
40            .query_context
41            .req
42            .host_id
43            .to_string()
44            .into();
45        let store = ctx
46            .query_context
47            .registry()
48            .get_or_create(Server::ENTITY_NAME_STATIC);
49        Some(crate::query::build_ids_source_map(&store, &[host_id]))
50    }
51}
52
53#[myko_query(Server)]
54pub struct GetPeerServers {}
55
56impl QueryHandler for GetPeerServers {
57    fn test_entity(ctx: QueryTestContext<Self>) -> bool {
58        let item_id = ctx.item.id.to_string();
59        let host_id = ctx.query_context.req.host_id.to_string();
60        item_id != host_id
61    }
62
63    #[cfg(not(target_arch = "wasm32"))]
64    fn build_view(
65        ctx: QueryBuildArgs<Self>,
66    ) -> Option<impl MapQuery<Key = Arc<str>, Value = Arc<dyn AnyItem>>>
67    where
68        Self: Send + Sync + 'static,
69    {
70        let host_id = ctx.query_context.query_context.req.host_id.to_string();
71        let store = ctx
72            .query_context
73            .registry()
74            .get_or_create(Server::ENTITY_NAME_STATIC)
75            .as_ref()
76            .clone()
77            .lock();
78        Some(store.select_by(move |id, _server| id.as_ref() != host_id))
79    }
80}
81
82// ─────────────────────────────────────────────────────────────────────────────
83// Manual Report Example: ServerStats
84// ─────────────────────────────────────────────────────────────────────────────
85
86/// Server statistics including connected client count.
87/// This is a manually implemented report demonstrating reactive query subscriptions.
88#[myko_macros::myko_report_output]
89#[derive(Eq)]
90pub struct ServerStatsOutput {
91    /// The server entity (if found)
92    pub server: Option<Arc<Server>>,
93    /// Number of clients connected to this server
94    pub client_count: usize,
95    /// Server uptime in seconds (computed from `started_at`)
96    pub uptime_seconds: Option<i64>,
97}
98
99/// Report that returns current server statistics.
100///
101/// This report demonstrates:
102/// - Subscribing to multiple queries (`GetConnectedServer`, `GetAllClients`)
103/// - Combining query results reactively
104/// - Computing derived values (uptime)
105///
106/// # Example
107///
108/// ```text
109/// // Client-side usage:
110/// let cell = client.watch_report::<ServerStats, ServerStatsOutput>(ServerStats {});
111///
112/// // `cell` updates whenever server/client state changes.
113/// // Read current value:
114/// let latest = cell.get();
115///
116/// // Or subscribe reactively:
117/// let _guard = cell.subscribe(|signal| {
118///   // handle Signal::Value(Some(ServerStatsOutput { ... }))
119/// });
120/// ```
121#[myko_macros::myko_report(ServerStatsOutput)]
122pub struct ServerStats {}
123
124impl ReportHandler for ServerStats {
125    type Output = ServerStatsOutput;
126
127    fn compute(&self, ctx: ReportContext) -> impl Materialize<Arc<Self::Output>, Definite> {
128        let host_id: Arc<str> = ctx.host_id().to_string().into();
129        // Canonical string keys match `IdFor<Server>::MapKey`; the direct join
130        // projection reads the shared relationship index without cloning clients
131        // into an intermediate joined value.
132        let stats_by_server = ctx
133            .query_map_by_str(GetConnectedServer {})
134            .left_join_fk::<ClientServerIdRelation, _>(ctx.query_map_by_str(GetAllClients {}))
135            .map_joined_values(|_server_id, server, clients| (server.clone(), clients.len()))
136            .materialize();
137
138        stats_by_server.get(&host_id).map(|stats| {
139            let Some((server, client_count)) = stats else {
140                return Arc::new(ServerStatsOutput {
141                    server: None,
142                    client_count: 0,
143                    uptime_seconds: None,
144                });
145            };
146            let uptime_seconds = chrono::DateTime::parse_from_rfc3339(&server.started_at)
147                .ok()
148                .map(|started| {
149                    let now = chrono::Utc::now();
150                    now.signed_duration_since(started.with_timezone(&chrono::Utc))
151                        .num_seconds()
152                });
153            Arc::new(ServerStatsOutput {
154                server: Some(server.clone()),
155                client_count: *client_count,
156                uptime_seconds,
157            })
158        })
159    }
160}