1use std::collections::HashSet;
5use std::sync::{Arc, Mutex};
6
7use manabrew_relay_protocol::{SeatTransportReport, TRANSPORT_WEBRTC};
8use serde_json::Value;
9use tokio::sync::mpsc;
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum ShellEvent {
14 Envelope { target: String, envelope: Value },
16 Signal { from: String, payload: Value },
18}
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum ShellCommand {
23 Serving { seats: Vec<String> },
25 Signal { to: String, payload: Value },
27 SeatEnvelope { from: String, envelope: Value },
29}
30
31type Reprime = Box<dyn Fn(&str) + Send + Sync>;
32
33pub struct ShellBridge {
35 serving: Mutex<HashSet<String>>,
36 frozen: Mutex<HashSet<String>>,
38 owed_a_board: Mutex<HashSet<String>>,
40 attested: Mutex<HashSet<String>>,
42 emit: Arc<dyn Fn(ShellEvent) + Send + Sync>,
43 on_fallback: Mutex<Option<Reprime>>,
44}
45
46impl ShellBridge {
47 pub fn new(
48 emit: impl Fn(ShellEvent) + Send + Sync + 'static,
49 ) -> (
50 Arc<Self>,
51 mpsc::UnboundedSender<ShellCommand>,
52 mpsc::UnboundedReceiver<ShellCommand>,
53 ) {
54 let (tx, rx) = mpsc::unbounded_channel();
55 let bridge = Arc::new(Self {
56 serving: Mutex::new(HashSet::new()),
57 frozen: Mutex::new(HashSet::new()),
58 owed_a_board: Mutex::new(HashSet::new()),
59 attested: Mutex::new(HashSet::new()),
60 emit: Arc::new(emit),
61 on_fallback: Mutex::new(None),
62 });
63 (bridge, tx, rx)
64 }
65
66 pub fn set_roster(&self, seats: impl IntoIterator<Item = String>) {
67 if let Ok(mut attested) = self.attested.lock() {
68 *attested = seats.into_iter().collect();
69 }
70 }
71
72 pub fn set_on_fallback(&self, reprime: impl Fn(&str) + Send + Sync + 'static) {
73 if let Ok(mut slot) = self.on_fallback.lock() {
74 *slot = Some(Box::new(reprime));
75 }
76 }
77
78 pub fn set_serving(&self, seats: Vec<String>) {
80 let incoming: HashSet<String> = seats.into_iter().collect();
81 let lost: Vec<String> = {
82 let Ok(mut frozen) = self.frozen.lock() else {
83 return;
84 };
85 let lost: Vec<String> = frozen.difference(&incoming).cloned().collect();
86 for seat in &lost {
87 frozen.remove(seat);
88 }
89 lost
90 };
91 if let Ok(mut serving) = self.serving.lock() {
92 *serving = incoming;
93 }
94 for seat in lost {
95 let newly_owed = self
96 .owed_a_board
97 .lock()
98 .map(|mut set| set.insert(seat.clone()))
99 .unwrap_or(false);
100 if !newly_owed {
101 continue;
102 }
103 if let Ok(slot) = self.on_fallback.lock() {
104 if let Some(reprime) = slot.as_ref() {
105 reprime(&seat);
106 }
107 }
108 }
109 }
110
111 pub fn freeze_for_game(&self, seats: &[String], taken: &[String]) -> Vec<String> {
113 let Ok(serving) = self.serving.lock() else {
114 return Vec::new();
115 };
116 let Ok(attested) = self.attested.lock() else {
117 return Vec::new();
118 };
119 let mine: HashSet<String> = seats
120 .iter()
121 .filter(|seat| {
122 serving.contains(*seat) && attested.contains(*seat) && !taken.contains(*seat)
123 })
124 .cloned()
125 .collect();
126 let mut listed: Vec<String> = mine.iter().cloned().collect();
127 listed.sort();
128 if let Ok(mut frozen) = self.frozen.lock() {
129 *frozen = mine;
130 }
131 if let Ok(mut owed) = self.owed_a_board.lock() {
132 owed.clear();
133 }
134 listed
135 }
136
137 pub fn clear_game(&self) {
138 if let Ok(mut frozen) = self.frozen.lock() {
139 frozen.clear();
140 }
141 if let Ok(mut owed) = self.owed_a_board.lock() {
142 owed.clear();
143 }
144 }
145
146 pub fn note_relay_message(&self, username: &str) {
148 if let Ok(mut owed) = self.owed_a_board.lock() {
149 owed.remove(username);
150 }
151 }
152
153 pub fn try_send(&self, target: &str, envelope: &Value) -> bool {
155 let claimed = self
156 .frozen
157 .lock()
158 .map(|frozen| frozen.contains(target))
159 .unwrap_or(false);
160 if !claimed {
161 return false;
162 }
163 if envelope.get("kind").and_then(Value::as_str) == Some("prompt") {
164 tracing::info!(target, "shell bridge: handed a prompt to the webview");
165 }
166 (self.emit)(ShellEvent::Envelope {
167 target: target.to_string(),
168 envelope: envelope.clone(),
169 });
170 true
171 }
172
173 pub fn forward_signal(&self, from: &str, payload: Value) {
174 (self.emit)(ShellEvent::Signal {
175 from: from.to_string(),
176 payload,
177 });
178 }
179
180 pub fn transport_report(&self, seats: &[String]) -> Vec<SeatTransportReport> {
181 seats
182 .iter()
183 .map(|username| SeatTransportReport {
184 username: username.clone(),
185 transport: TRANSPORT_WEBRTC.to_string(),
186 })
187 .collect()
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use serde_json::json;
195 use std::sync::mpsc as std_mpsc;
196
197 fn bridge() -> (Arc<ShellBridge>, std_mpsc::Receiver<ShellEvent>) {
198 let (tx, rx) = std_mpsc::channel();
199 let (bridge, _cmd_tx, _cmd_rx) = ShellBridge::new(move |event| {
200 let _ = tx.send(event);
201 });
202 (bridge, rx)
203 }
204
205 #[test]
206 fn nothing_leaves_this_plane_before_the_game_freezes_it() {
207 let (bridge, events) = bridge();
208 bridge.set_roster(["bob".to_string()]);
209 bridge.set_serving(vec!["bob".into()]);
210 assert!(!bridge.try_send("bob", &json!({"kind": "prompt"})));
211 assert!(events.try_recv().is_err());
212
213 bridge.freeze_for_game(&["bob".into()], &[]);
214 assert!(bridge.try_send("bob", &json!({"kind": "prompt"})));
215 }
216
217 #[test]
218 fn a_seat_the_direct_plane_took_is_not_claimed_twice() {
219 let (bridge, _events) = bridge();
220 bridge.set_roster(["bob".to_string(), "carol".to_string()]);
221 bridge.set_serving(vec!["bob".into(), "carol".into()]);
222 let mine = bridge.freeze_for_game(&["bob".into(), "carol".into()], &["bob".into()]);
223 assert_eq!(mine, vec!["carol".to_string()]);
224 assert!(!bridge.try_send("bob", &json!({})));
225 assert!(bridge.try_send("carol", &json!({})));
226 }
227
228 #[test]
229 fn a_seat_the_roster_does_not_name_stays_on_the_relay() {
230 let (bridge, _events) = bridge();
231 bridge.set_serving(vec!["bob".into()]);
232 assert!(
233 bridge.freeze_for_game(&["bob".into()], &[]).is_empty(),
234 "no roster, no plane"
235 );
236 bridge.set_roster(["bob".to_string()]);
237 assert_eq!(
238 bridge.freeze_for_game(&["bob".into()], &[]),
239 vec!["bob".to_string()]
240 );
241 }
242
243 #[test]
244 fn a_seat_that_loses_its_channel_falls_back_owing_a_board() {
245 let (bridge, _events) = bridge();
246 let repriced: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
247 let seen = repriced.clone();
248 bridge.set_on_fallback(move |seat| seen.lock().unwrap().push(seat.to_string()));
249
250 bridge.set_roster(["bob".to_string()]);
251 bridge.set_serving(vec!["bob".into()]);
252 bridge.freeze_for_game(&["bob".into()], &[]);
253 assert!(bridge.try_send("bob", &json!({})));
254
255 bridge.set_serving(vec![]);
256 assert_eq!(repriced.lock().unwrap().as_slice(), ["bob"]);
257 assert!(!bridge.try_send("bob", &json!({})));
258 }
259
260 #[test]
261 fn the_debt_is_owed_once_and_settled_by_the_seat_answering_on_the_relay() {
262 let (bridge, _events) = bridge();
263 let count = Arc::new(Mutex::new(0usize));
264 let seen = count.clone();
265 bridge.set_on_fallback(move |_| *seen.lock().unwrap() += 1);
266
267 bridge.set_roster(["bob".to_string()]);
268 bridge.set_serving(vec!["bob".into()]);
269 bridge.freeze_for_game(&["bob".into()], &[]);
270 bridge.set_serving(vec![]);
271 bridge.set_serving(vec![]);
272 assert_eq!(*count.lock().unwrap(), 1);
273
274 bridge.note_relay_message("bob");
275 assert!(!bridge.try_send("bob", &json!({})));
276
277 bridge.set_serving(vec!["bob".into()]);
278 assert!(!bridge.try_send("bob", &json!({})));
279 }
280
281 #[test]
282 fn a_seat_that_was_never_frozen_here_owes_nothing_when_it_stops_being_served() {
283 let (bridge, _events) = bridge();
284 let count = Arc::new(Mutex::new(0usize));
285 let seen = count.clone();
286 bridge.set_on_fallback(move |_| *seen.lock().unwrap() += 1);
287
288 bridge.set_serving(vec!["bob".into()]);
289 bridge.set_serving(vec![]);
290 assert_eq!(*count.lock().unwrap(), 0);
291 }
292
293 #[test]
294 fn the_capture_is_told_which_seats_it_stopped_seeing() {
295 let (bridge, _events) = bridge();
296 let report = bridge.transport_report(&["bob".into()]);
297 assert_eq!(report.len(), 1);
298 assert_eq!(report[0].username, "bob");
299 assert_eq!(report[0].transport, TRANSPORT_WEBRTC);
300 }
301
302 #[test]
303 fn signalling_for_this_host_reaches_the_webview_that_holds_the_connections() {
304 let (bridge, events) = bridge();
305 bridge.forward_signal("bob", json!({"sdp": {"type": "offer"}}));
306 assert_eq!(
307 events.try_recv().unwrap(),
308 ShellEvent::Signal {
309 from: "bob".into(),
310 payload: json!({"sdp": {"type": "offer"}}),
311 }
312 );
313 }
314}