cratefield_runtime_native/ports/
realtime.rs1#![allow(clippy::disallowed_types)]
15
16use std::collections::HashMap;
17use std::sync::atomic::{AtomicU64, Ordering};
18use std::sync::{Arc, Mutex};
19use std::time::Duration;
20
21use async_trait::async_trait;
22use cratefield_core::{Member, Realtime, RealtimeError, RoomContext, RoomHandler};
23use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
24
25type MemberSender = (String, UnboundedSender<Vec<u8>>);
27
28#[derive(Clone)]
30pub struct InProcessRealtime {
31 handler: Arc<dyn RoomHandler>,
32 rooms: Arc<Mutex<HashMap<String, Room>>>,
33 next_session: Arc<AtomicU64>,
40}
41
42struct Registration {
45 session: u64,
46 tx: UnboundedSender<Vec<u8>>,
47}
48
49#[derive(Default)]
50struct Room {
51 members: HashMap<String, Registration>,
58 alarm: Option<tokio::task::JoinHandle<()>>,
60}
61
62impl InProcessRealtime {
63 #[must_use]
64 pub fn new(handler: Arc<dyn RoomHandler>) -> Self {
65 Self {
66 handler,
67 rooms: Arc::new(Mutex::new(HashMap::new())),
68 next_session: Arc::new(AtomicU64::new(1)),
69 }
70 }
71
72 fn context(&self, room_id: &str) -> NativeContext {
73 NativeContext {
74 rooms: Arc::clone(&self.rooms),
75 handler: Arc::clone(&self.handler),
76 room_id: room_id.to_owned(),
77 }
78 }
79
80 pub async fn connect(
93 &self,
94 room_id: &str,
95 member: Member,
96 ) -> Result<Connection, RealtimeError> {
97 let (tx, rx) = unbounded_channel();
98 let session = self.next_session.fetch_add(1, Ordering::Relaxed);
99 {
100 let mut rooms = self.rooms.lock().expect("rooms lock");
101 rooms
102 .entry(room_id.to_owned())
103 .or_default()
104 .members
105 .insert(member.id.clone(), Registration { session, tx });
106 }
107 let ctx = self.context(room_id);
108 self.handler.on_join(&ctx, &member).await?;
109 Ok(Connection {
110 rx,
111 member,
112 room_id: room_id.to_owned(),
113 session,
114 realtime: self.clone(),
115 })
116 }
117}
118
119pub struct Connection {
122 rx: UnboundedReceiver<Vec<u8>>,
123 member: Member,
124 room_id: String,
125 session: u64,
129 realtime: InProcessRealtime,
130}
131
132impl Connection {
133 pub async fn next_outgoing(&mut self) -> Option<Vec<u8>> {
136 self.rx.recv().await
137 }
138
139 pub async fn deliver(&self, message: &[u8]) -> Result<(), RealtimeError> {
151 let present = {
152 let rooms = self.realtime.rooms.lock().expect("rooms lock");
153 rooms.get(&self.room_id).is_some_and(|room| {
154 room.members
155 .get(&self.member.id)
156 .is_some_and(|registration| registration.session == self.session)
157 })
158 };
159 if !present {
160 return Err(RealtimeError::NotAMember);
161 }
162 let ctx = self.realtime.context(&self.room_id);
163 self.realtime
164 .handler
165 .on_message(&ctx, &self.member, message)
166 .await
167 }
168
169 pub async fn close(self) -> Result<(), RealtimeError> {
185 {
186 let mut rooms = self.realtime.rooms.lock().expect("rooms lock");
187 let owns_entry = rooms.get(&self.room_id).is_some_and(|room| {
188 room.members
189 .get(&self.member.id)
190 .is_some_and(|registration| registration.session == self.session)
191 });
192 if owns_entry && let Some(room) = rooms.get_mut(&self.room_id) {
193 room.members.remove(&self.member.id);
194 if room.members.is_empty() {
195 if let Some(alarm) = room.alarm.take() {
196 alarm.abort();
197 }
198 rooms.remove(&self.room_id);
199 }
200 }
201 }
202 let ctx = self.realtime.context(&self.room_id);
203 self.realtime.handler.on_leave(&ctx, &self.member).await
204 }
205}
206
207struct NativeContext {
209 rooms: Arc<Mutex<HashMap<String, Room>>>,
210 handler: Arc<dyn RoomHandler>,
211 room_id: String,
212}
213
214impl NativeContext {
215 fn senders(&self) -> Vec<MemberSender> {
219 let rooms = self.rooms.lock().expect("rooms lock");
220 rooms
221 .get(&self.room_id)
222 .map(|room| {
223 room.members
224 .iter()
225 .map(|(id, registration)| (id.clone(), registration.tx.clone()))
226 .collect()
227 })
228 .unwrap_or_default()
229 }
230}
231
232#[async_trait]
233impl RoomContext for NativeContext {
234 fn room_id(&self) -> &str {
235 &self.room_id
236 }
237
238 async fn members(&self) -> Vec<Member> {
239 self.senders()
240 .into_iter()
241 .map(|(id, _)| Member::new(id))
242 .collect()
243 }
244
245 async fn broadcast(&self, message: &[u8]) -> Result<(), RealtimeError> {
246 for (_, tx) in self.senders() {
247 let _ = tx.send(message.to_vec());
249 }
250 Ok(())
251 }
252
253 async fn send(&self, member_id: &str, message: &[u8]) -> Result<(), RealtimeError> {
254 let sender = self
255 .senders()
256 .into_iter()
257 .find(|(id, _)| id == member_id)
258 .map(|(_, tx)| tx);
259 if let Some(tx) = sender {
260 let _ = tx.send(message.to_vec());
261 }
262 Ok(())
263 }
264
265 async fn set_alarm(&self, delay: Duration) -> Result<(), RealtimeError> {
266 let rooms = Arc::clone(&self.rooms);
267 let handler = Arc::clone(&self.handler);
268 let room_id = self.room_id.clone();
269 let handle = tokio::spawn(async move {
270 tokio::time::sleep(delay).await;
271 let ctx = NativeContext {
272 rooms,
273 handler: Arc::clone(&handler),
274 room_id,
275 };
276 let _ = handler.on_alarm(&ctx).await;
277 });
278 let mut rooms = self.rooms.lock().expect("rooms lock");
279 if let Some(room) = rooms.get_mut(&self.room_id) {
280 if let Some(previous) = room.alarm.replace(handle) {
281 previous.abort();
282 }
283 } else {
284 handle.abort();
286 }
287 Ok(())
288 }
289}
290
291#[async_trait]
292impl Realtime for InProcessRealtime {
293 async fn broadcast(&self, room_id: &str, message: &[u8]) -> Result<(), RealtimeError> {
294 self.context(room_id).broadcast(message).await
295 }
296
297 async fn members(&self, room_id: &str) -> Result<Vec<Member>, RealtimeError> {
298 Ok(self.context(room_id).members().await)
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use std::sync::atomic::{AtomicUsize, Ordering};
306
307 #[derive(Default)]
310 struct Recorder {
311 joins: AtomicUsize,
312 leaves: AtomicUsize,
313 alarms: AtomicUsize,
314 }
315
316 #[async_trait]
317 impl RoomHandler for Recorder {
318 async fn on_join(
319 &self,
320 ctx: &dyn RoomContext,
321 member: &Member,
322 ) -> Result<(), RealtimeError> {
323 self.joins.fetch_add(1, Ordering::SeqCst);
324 ctx.broadcast(format!("join:{}", member.id).as_bytes())
325 .await
326 }
327
328 async fn on_message(
329 &self,
330 ctx: &dyn RoomContext,
331 member: &Member,
332 message: &[u8],
333 ) -> Result<(), RealtimeError> {
334 let echo = format!("{}:{}", member.id, String::from_utf8_lossy(message));
335 ctx.broadcast(echo.as_bytes()).await
336 }
337
338 async fn on_leave(
339 &self,
340 _ctx: &dyn RoomContext,
341 _member: &Member,
342 ) -> Result<(), RealtimeError> {
343 self.leaves.fetch_add(1, Ordering::SeqCst);
344 Ok(())
345 }
346
347 async fn on_alarm(&self, ctx: &dyn RoomContext) -> Result<(), RealtimeError> {
348 self.alarms.fetch_add(1, Ordering::SeqCst);
349 ctx.broadcast(b"tick").await
350 }
351 }
352
353 fn recorder() -> (InProcessRealtime, Arc<Recorder>) {
354 let recorder = Arc::new(Recorder::default());
355 (InProcessRealtime::new(recorder.clone()), recorder)
356 }
357
358 #[tokio::test]
359 async fn join_broadcasts_to_every_member() {
360 let (rt, rec) = recorder();
361 let mut a = rt.connect("room", Member::new("a")).await.unwrap();
362 let mut b = rt.connect("room", Member::new("b")).await.unwrap();
363
364 rt.broadcast("room", b"hello").await.unwrap();
366 assert_eq!(rt.members("room").await.unwrap().len(), 2);
367 assert_eq!(rec.joins.load(Ordering::SeqCst), 2);
368
369 assert_eq!(a.next_outgoing().await.unwrap(), b"join:a");
371 assert_eq!(a.next_outgoing().await.unwrap(), b"join:b");
372 assert_eq!(a.next_outgoing().await.unwrap(), b"hello");
373 assert_eq!(b.next_outgoing().await.unwrap(), b"join:b");
375 assert_eq!(b.next_outgoing().await.unwrap(), b"hello");
376 }
377
378 #[tokio::test]
379 async fn a_message_reaches_the_other_members() {
380 let (rt, _rec) = recorder();
381 let a = rt.connect("room", Member::new("a")).await.unwrap();
382 let mut b = rt.connect("room", Member::new("b")).await.unwrap();
383 assert_eq!(b.next_outgoing().await.unwrap(), b"join:b");
386
387 a.deliver(b"hi").await.unwrap();
388 assert_eq!(b.next_outgoing().await.unwrap(), b"a:hi");
389 }
390
391 #[tokio::test]
392 async fn leaving_removes_the_member_and_runs_on_leave() {
393 let (rt, rec) = recorder();
394 let a = rt.connect("room", Member::new("a")).await.unwrap();
395 rt.connect("room", Member::new("b")).await.unwrap();
396 a.close().await.unwrap();
397 assert_eq!(rec.leaves.load(Ordering::SeqCst), 1);
398 let ids: Vec<String> = rt
399 .members("room")
400 .await
401 .unwrap()
402 .into_iter()
403 .map(|m| m.id)
404 .collect();
405 assert_eq!(ids, ["b"]);
406 }
407
408 #[tokio::test]
409 async fn a_message_from_a_departed_member_is_refused() {
410 let (rt, _rec) = recorder();
411 rt.connect("room", Member::new("keep")).await.unwrap();
412 let gone = rt.connect("room", Member::new("gone")).await.unwrap();
413 let realtime = rt.clone();
415 let member = gone.member.clone();
416 gone.close().await.unwrap();
417 let orphan = Connection {
418 rx: unbounded_channel().1,
419 member,
420 room_id: "room".to_owned(),
421 session: 0,
422 realtime,
423 };
424 assert!(matches!(
425 orphan.deliver(b"late").await,
426 Err(RealtimeError::NotAMember)
427 ));
428 }
429
430 #[tokio::test(start_paused = true)]
431 async fn an_alarm_fires_exactly_once() {
432 let (rt, rec) = recorder();
433 let mut a = rt.connect("room", Member::new("a")).await.unwrap();
434 let _ = a.next_outgoing().await; rt.context("room")
438 .set_alarm(Duration::from_millis(50))
439 .await
440 .unwrap();
441 tokio::time::advance(Duration::from_millis(60)).await;
442 tokio::task::yield_now().await;
444
445 assert_eq!(a.next_outgoing().await.unwrap(), b"tick");
446 assert_eq!(rec.alarms.load(Ordering::SeqCst), 1);
447 }
448
449 #[tokio::test]
457 async fn a_stale_connection_closing_leaves_the_reconnected_member_wired() {
458 let (rt, _rec) = recorder();
459 let old = rt.connect("room", Member::new("a")).await.unwrap();
460 let mut new = rt.connect("room", Member::new("a")).await.unwrap();
461 assert_eq!(new.next_outgoing().await.unwrap(), b"join:a");
464
465 old.close().await.unwrap();
466
467 assert_eq!(rt.members("room").await.unwrap().len(), 1);
469 rt.broadcast("room", b"hello").await.unwrap();
470 assert_eq!(new.next_outgoing().await.unwrap(), b"hello");
471 new.deliver(b"hi").await.unwrap();
472 }
473
474 #[tokio::test(start_paused = true)]
478 async fn a_stale_connection_closing_does_not_cancel_the_live_members_alarm() {
479 let (rt, rec) = recorder();
480 let old = rt.connect("room", Member::new("a")).await.unwrap();
481 let mut new = rt.connect("room", Member::new("a")).await.unwrap();
482 let _ = new.next_outgoing().await; rt.context("room")
485 .set_alarm(Duration::from_millis(50))
486 .await
487 .unwrap();
488 tokio::task::yield_now().await;
490
491 old.close().await.unwrap();
492
493 tokio::time::advance(Duration::from_millis(60)).await;
494 tokio::task::yield_now().await;
495 assert_eq!(new.next_outgoing().await.unwrap(), b"tick");
496 assert_eq!(rec.alarms.load(Ordering::SeqCst), 1);
497 }
498}