Skip to main content

dartboard_editor/
session_mirror.rs

1use dartboard_core::{Canvas, CanvasOp, Peer, RgbColor, ServerMsg, UserId};
2
3#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
4pub enum ConnectState {
5    #[default]
6    Pending,
7    Welcomed,
8    Rejected,
9}
10
11/// Host-facing view of a remote dartboard session. Tracks identity and peer
12/// list as `ServerMsg`s arrive; emits typed `MirrorEvent`s so hosts can react
13/// without re-matching the raw wire enum.
14#[derive(Debug, Default, Clone)]
15pub struct SessionMirror {
16    pub peers: Vec<Peer>,
17    pub my_user_id: Option<UserId>,
18    pub my_color: Option<RgbColor>,
19    pub connect_state: ConnectState,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum MirrorEvent {
24    Welcomed {
25        my_user_id: UserId,
26        my_color: RgbColor,
27        peers: Vec<Peer>,
28        snapshot: Canvas,
29    },
30    RemoteOp {
31        op: CanvasOp,
32        from: UserId,
33    },
34    PeerJoined(Peer),
35    PeerLeft {
36        user_id: UserId,
37        index: usize,
38    },
39    ConnectRejected {
40        reason: String,
41    },
42}
43
44impl SessionMirror {
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Apply a server message and return the corresponding event for the host
50    /// to react to. Self-echoed `OpBroadcast`s and unknown `PeerLeft`s return
51    /// `None`; `Ack`/`Reject` are not surfaced.
52    pub fn apply(&mut self, msg: ServerMsg) -> Option<MirrorEvent> {
53        match msg {
54            ServerMsg::Welcome {
55                your_user_id,
56                your_color,
57                peers,
58                snapshot,
59            } => {
60                self.my_user_id = Some(your_user_id);
61                self.my_color = Some(your_color);
62                self.peers = peers.clone();
63                self.connect_state = ConnectState::Welcomed;
64                Some(MirrorEvent::Welcomed {
65                    my_user_id: your_user_id,
66                    my_color: your_color,
67                    peers,
68                    snapshot,
69                })
70            }
71            ServerMsg::OpBroadcast { op, from, .. } => {
72                if Some(from) == self.my_user_id {
73                    None
74                } else {
75                    Some(MirrorEvent::RemoteOp { op, from })
76                }
77            }
78            ServerMsg::PeerJoined { peer } => {
79                self.peers.push(peer.clone());
80                Some(MirrorEvent::PeerJoined(peer))
81            }
82            ServerMsg::PeerLeft { user_id } => {
83                let idx = self.peers.iter().position(|p| p.user_id == user_id)?;
84                self.peers.remove(idx);
85                Some(MirrorEvent::PeerLeft {
86                    user_id,
87                    index: idx,
88                })
89            }
90            ServerMsg::ConnectRejected { reason } => {
91                self.connect_state = ConnectState::Rejected;
92                Some(MirrorEvent::ConnectRejected { reason })
93            }
94            ServerMsg::Ack { .. } | ServerMsg::Reject { .. } => None,
95        }
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use dartboard_core::{CanvasOp, Pos};
103
104    fn make_peer(user_id: UserId, name: &str) -> Peer {
105        Peer {
106            user_id,
107            name: name.to_string(),
108            color: RgbColor::new(1, 2, 3),
109        }
110    }
111
112    #[test]
113    fn welcome_records_identity_and_peers() {
114        let mut mirror = SessionMirror::new();
115        assert_eq!(mirror.connect_state, ConnectState::Pending);
116
117        let welcome_peers = vec![make_peer(2, "Bob")];
118        let event = mirror.apply(ServerMsg::Welcome {
119            your_user_id: 1,
120            your_color: RgbColor::new(9, 9, 9),
121            peers: welcome_peers.clone(),
122            snapshot: Canvas::with_size(4, 2),
123        });
124
125        assert_eq!(mirror.my_user_id, Some(1));
126        assert_eq!(mirror.my_color, Some(RgbColor::new(9, 9, 9)));
127        assert_eq!(mirror.peers, welcome_peers);
128        assert_eq!(mirror.connect_state, ConnectState::Welcomed);
129        assert!(matches!(event, Some(MirrorEvent::Welcomed { .. })));
130    }
131
132    #[test]
133    fn op_broadcast_from_self_is_swallowed() {
134        let mut mirror = SessionMirror::new();
135        mirror.my_user_id = Some(1);
136
137        let op = CanvasOp::PaintCell {
138            pos: Pos { x: 0, y: 0 },
139            ch: 'x',
140            fg: RgbColor::new(0, 0, 0),
141        };
142
143        assert!(mirror
144            .apply(ServerMsg::OpBroadcast {
145                from: 1,
146                op: op.clone(),
147                seq: 1,
148            })
149            .is_none());
150
151        assert_eq!(
152            mirror.apply(ServerMsg::OpBroadcast {
153                from: 2,
154                op: op.clone(),
155                seq: 2,
156            }),
157            Some(MirrorEvent::RemoteOp { op, from: 2 })
158        );
159    }
160
161    #[test]
162    fn peer_join_and_leave_track_index() {
163        let mut mirror = SessionMirror::new();
164        mirror.apply(ServerMsg::PeerJoined {
165            peer: make_peer(10, "a"),
166        });
167        mirror.apply(ServerMsg::PeerJoined {
168            peer: make_peer(20, "b"),
169        });
170
171        assert_eq!(
172            mirror.apply(ServerMsg::PeerLeft { user_id: 10 }),
173            Some(MirrorEvent::PeerLeft {
174                user_id: 10,
175                index: 0,
176            })
177        );
178        assert_eq!(mirror.peers.len(), 1);
179        assert_eq!(mirror.peers[0].user_id, 20);
180
181        // Unknown peer leave is a no-op.
182        assert!(mirror.apply(ServerMsg::PeerLeft { user_id: 999 }).is_none());
183    }
184
185    #[test]
186    fn connect_rejected_marks_state() {
187        let mut mirror = SessionMirror::new();
188        let event = mirror.apply(ServerMsg::ConnectRejected {
189            reason: "server full".into(),
190        });
191        assert_eq!(mirror.connect_state, ConnectState::Rejected);
192        assert!(matches!(event, Some(MirrorEvent::ConnectRejected { .. })));
193    }
194}