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#[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 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 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}