Skip to main content

myko/entities/
client.rs

1use std::sync::Arc;
2
3use hyphae::{Cell, CellImmutable, MapExt};
4use myko_macros::{myko_command, myko_report, myko_report_output};
5
6use crate::{
7    entities::server::{Server, ServerId},
8    prelude::*,
9    report::{ReportContext, ReportHandler},
10};
11
12#[myko_item]
13pub struct Client {
14    #[belongs_to(Server)]
15    pub server_id: ServerId,
16
17    /// ISO timestamp for windback mode. When set, the client sees historical state
18    /// as of this timestamp instead of live state.
19    pub windback: Option<Arc<str>>,
20}
21
22// ─────────────────────────────────────────────────────────────────────────────
23// Custom Reports
24// ─────────────────────────────────────────────────────────────────────────────
25
26#[myko_report_output]
27pub struct ClientStatusOutput {
28    pub online: bool,
29}
30
31/// Report that returns whether a client is currently connected
32#[myko_report(ClientStatusOutput)]
33pub struct ClientStatus {
34    pub client_id: ClientId,
35}
36
37impl ReportHandler for ClientStatus {
38    type Output = ClientStatusOutput;
39
40    fn compute(&self, ctx: ReportContext) -> Cell<Arc<Self::Output>, CellImmutable> {
41        let client_id = self.client_id.clone();
42
43        // Query all clients and check if one with our id exists
44        ctx.query_map(GetAllClients {})
45            .entries()
46            .map(move |clients| {
47                let online = clients
48                    .iter()
49                    .any(|(_, c)| c.id.as_ref() == client_id.as_ref());
50                Arc::new(ClientStatusOutput { online })
51            })
52    }
53}
54
55// ─────────────────────────────────────────────────────────────────────────────
56// Windback Support
57// ─────────────────────────────────────────────────────────────────────────────
58
59/// Report that returns the current windback time for the requesting client.
60/// Returns None if the client is not in windback mode.
61#[myko_report_output]
62pub struct WindbackStatusOutput {
63    /// ISO timestamp if in windback mode, None otherwise
64    pub windback: Option<Arc<str>>,
65}
66
67#[myko_report(WindbackStatusOutput)]
68pub struct WindbackStatus {}
69
70impl ReportHandler for WindbackStatus {
71    type Output = WindbackStatusOutput;
72
73    fn compute(&self, ctx: ReportContext) -> Cell<Arc<Self::Output>, CellImmutable> {
74        let client_id = ctx
75            .client_id()
76            .map(|id| ClientId::from(Arc::<str>::from(id)));
77
78        // Query all clients and find the requesting client's windback status
79        ctx.query_map(GetAllClients {})
80            .entries()
81            .map(move |clients| {
82                let windback = client_id
83                    .as_ref()
84                    .and_then(|cid| clients.iter().find(|(_, c)| c.id.as_ref() == cid.as_ref()))
85                    .and_then(|(_, c)| c.windback.clone());
86                Arc::new(WindbackStatusOutput { windback })
87            })
88    }
89}
90
91/// Command to set the windback time for the current client.
92/// When set, queries return historical state as of the specified timestamp.
93#[myko_command(bool)]
94pub struct SetClientWindbackTime {
95    /// ISO timestamp to wind back to
96    pub windback: Arc<str>,
97}
98
99impl crate::command::CommandHandler for SetClientWindbackTime {
100    fn execute(
101        self,
102        ctx: crate::command::CommandContext,
103    ) -> Result<bool, crate::command::CommandError> {
104        let client_id = ctx
105            .client_id()
106            .ok_or_else(|| crate::command::CommandError {
107                tx: ctx.tx().to_string(),
108                command_id: "SetClientWindbackTime".to_string(),
109                message: "No client_id in context - windback requires a WebSocket connection"
110                    .to_string(),
111            })?;
112
113        // Find the client entity
114        let client = ctx
115            .exec_report(GetClientById {
116                id: ClientId::from(Arc::<str>::from(client_id.clone())),
117            })?
118            .ok_or_else(|| CommandError {
119                tx: ctx.tx().to_string(),
120                command_id: "SetClientWindbackTime".to_string(),
121                message: format!("Client {} not found", client_id),
122            })?;
123
124        // Update client with new windback time
125        let updated_client = Client {
126            id: client.id.clone(),
127            server_id: client.server_id.clone(),
128            windback: Some(self.windback.clone()),
129        };
130
131        ctx.emit_set(&updated_client)?;
132
133        Ok(true)
134    }
135}
136
137/// Command to clear the windback time for the current client.
138/// Returns the client to viewing live state.
139#[myko_command(bool)]
140pub struct ClearClientWindbackTime {}
141
142impl crate::command::CommandHandler for ClearClientWindbackTime {
143    fn execute(
144        self,
145        ctx: crate::command::CommandContext,
146    ) -> Result<bool, crate::command::CommandError> {
147        let client_id = ctx
148            .client_id()
149            .ok_or_else(|| crate::command::CommandError {
150                tx: ctx.tx().to_string(),
151                command_id: "ClearClientWindbackTime".to_string(),
152                message: "No client_id in context - windback requires a WebSocket connection"
153                    .to_string(),
154            })?;
155
156        // Find the client entity
157        let client = ctx
158            .exec_report(GetClientById {
159                id: ClientId::from(Arc::<str>::from(client_id.clone())),
160            })?
161            .ok_or_else(|| CommandError {
162                tx: ctx.tx().to_string(),
163                command_id: "SetClientWindbackTime".to_string(),
164                message: format!("Client {} not found", client_id),
165            })?;
166        // Update client to clear windback
167        let updated_client = Client {
168            id: client.id.clone(),
169            server_id: client.server_id.clone(),
170            windback: None,
171        };
172
173        ctx.emit_set(&updated_client)?;
174
175        Ok(true)
176    }
177}