1use std::{
4 collections::{BTreeMap, BTreeSet, VecDeque},
5 future::Future,
6 num::NonZeroUsize,
7 pin::Pin,
8 sync::{Mutex, RwLock},
9 task::{Context, Poll, Waker},
10};
11
12use soaprs_auth::{Principal, PrincipalId};
13use soaprs_core::{BoxFuture, SoapError, SoapResult};
14use soaprs_realtime::{
15 BackpressurePolicy, ChannelId, ChannelMembership, ConnectionContext, ConnectionId,
16 ConnectionPresence, DeliveryOutcome, JoinOutcome, LeaveOutcome, OutboundDelivery,
17 RealtimeDelivery,
18};
19
20#[derive(Debug)]
22pub struct MemoryConnectionPresence<P> {
23 connections: RwLock<BTreeMap<ConnectionId, ConnectionContext<P>>>,
24}
25
26impl<P> MemoryConnectionPresence<P> {
27 pub const fn new() -> Self {
29 Self {
30 connections: RwLock::new(BTreeMap::new()),
31 }
32 }
33
34 fn read(
35 &self,
36 ) -> SoapResult<std::sync::RwLockReadGuard<'_, BTreeMap<ConnectionId, ConnectionContext<P>>>>
37 {
38 self.connections
39 .read()
40 .map_err(|_| SoapError::infrastructure("in-memory presence read lock poisoned"))
41 }
42
43 fn write(
44 &self,
45 ) -> SoapResult<std::sync::RwLockWriteGuard<'_, BTreeMap<ConnectionId, ConnectionContext<P>>>>
46 {
47 self.connections
48 .write()
49 .map_err(|_| SoapError::infrastructure("in-memory presence write lock poisoned"))
50 }
51}
52
53impl<P> Default for MemoryConnectionPresence<P> {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl<P> ConnectionPresence<P> for MemoryConnectionPresence<P>
60where
61 P: Principal + Clone + Send + Sync,
62{
63 fn register(&self, context: ConnectionContext<P>) -> BoxFuture<'_, SoapResult<()>> {
64 Box::pin(async move {
65 let mut connections = self.write()?;
66 if connections.contains_key(context.connection_id()) {
67 return Err(SoapError::conflict(format!(
68 "realtime connection `{}` is already registered",
69 context.connection_id()
70 )));
71 }
72 connections.insert(context.connection_id().clone(), context);
73 Ok(())
74 })
75 }
76
77 fn remove<'a>(
78 &'a self,
79 connection_id: &'a ConnectionId,
80 ) -> BoxFuture<'a, SoapResult<Option<ConnectionContext<P>>>> {
81 Box::pin(async move { Ok(self.write()?.remove(connection_id)) })
82 }
83
84 fn connection<'a>(
85 &'a self,
86 connection_id: &'a ConnectionId,
87 ) -> BoxFuture<'a, SoapResult<Option<ConnectionContext<P>>>> {
88 Box::pin(async move { Ok(self.read()?.get(connection_id).cloned()) })
89 }
90
91 fn connections(&self) -> BoxFuture<'_, SoapResult<Vec<ConnectionContext<P>>>> {
92 Box::pin(async move { Ok(self.read()?.values().cloned().collect()) })
93 }
94
95 fn principal_connections<'a>(
96 &'a self,
97 principal_id: &'a PrincipalId,
98 ) -> BoxFuture<'a, SoapResult<Vec<ConnectionContext<P>>>> {
99 Box::pin(async move {
100 Ok(self
101 .read()?
102 .values()
103 .filter(|context| {
104 context.authentication().is_some_and(|authentication| {
105 authentication.principal().principal_id() == principal_id
106 })
107 })
108 .cloned()
109 .collect())
110 })
111 }
112}
113
114#[derive(Debug, Default)]
115struct MembershipState {
116 channels: BTreeMap<ChannelId, BTreeSet<ConnectionId>>,
117 connections: BTreeMap<ConnectionId, BTreeSet<ChannelId>>,
118}
119
120#[derive(Debug, Default)]
122pub struct MemoryChannelMembership {
123 state: RwLock<MembershipState>,
124}
125
126impl MemoryChannelMembership {
127 pub const fn new() -> Self {
129 Self {
130 state: RwLock::new(MembershipState {
131 channels: BTreeMap::new(),
132 connections: BTreeMap::new(),
133 }),
134 }
135 }
136
137 fn read(&self) -> SoapResult<std::sync::RwLockReadGuard<'_, MembershipState>> {
138 self.state
139 .read()
140 .map_err(|_| SoapError::infrastructure("in-memory membership read lock poisoned"))
141 }
142
143 fn write(&self) -> SoapResult<std::sync::RwLockWriteGuard<'_, MembershipState>> {
144 self.state
145 .write()
146 .map_err(|_| SoapError::infrastructure("in-memory membership write lock poisoned"))
147 }
148}
149
150impl ChannelMembership for MemoryChannelMembership {
151 fn join<'a>(
152 &'a self,
153 connection_id: &'a ConnectionId,
154 channel_id: &'a ChannelId,
155 ) -> BoxFuture<'a, SoapResult<JoinOutcome>> {
156 Box::pin(async move {
157 let mut state = self.write()?;
158 let joined = state
159 .channels
160 .entry(channel_id.clone())
161 .or_default()
162 .insert(connection_id.clone());
163 state
164 .connections
165 .entry(connection_id.clone())
166 .or_default()
167 .insert(channel_id.clone());
168 Ok(if joined {
169 JoinOutcome::Joined
170 } else {
171 JoinOutcome::AlreadyMember
172 })
173 })
174 }
175
176 fn leave<'a>(
177 &'a self,
178 connection_id: &'a ConnectionId,
179 channel_id: &'a ChannelId,
180 ) -> BoxFuture<'a, SoapResult<LeaveOutcome>> {
181 Box::pin(async move {
182 let mut state = self.write()?;
183 let removed = state
184 .channels
185 .get_mut(channel_id)
186 .is_some_and(|members| members.remove(connection_id));
187 if state
188 .channels
189 .get(channel_id)
190 .is_some_and(BTreeSet::is_empty)
191 {
192 state.channels.remove(channel_id);
193 }
194 if let Some(channels) = state.connections.get_mut(connection_id) {
195 channels.remove(channel_id);
196 if channels.is_empty() {
197 state.connections.remove(connection_id);
198 }
199 }
200 Ok(if removed {
201 LeaveOutcome::Left
202 } else {
203 LeaveOutcome::NotMember
204 })
205 })
206 }
207
208 fn members<'a>(
209 &'a self,
210 channel_id: &'a ChannelId,
211 ) -> BoxFuture<'a, SoapResult<Vec<ConnectionId>>> {
212 Box::pin(async move {
213 Ok(self
214 .read()?
215 .channels
216 .get(channel_id)
217 .map(|members| members.iter().cloned().collect())
218 .unwrap_or_default())
219 })
220 }
221
222 fn channels<'a>(
223 &'a self,
224 connection_id: &'a ConnectionId,
225 ) -> BoxFuture<'a, SoapResult<Vec<ChannelId>>> {
226 Box::pin(async move {
227 Ok(self
228 .read()?
229 .connections
230 .get(connection_id)
231 .map(|channels| channels.iter().cloned().collect())
232 .unwrap_or_default())
233 })
234 }
235
236 fn remove_connection<'a>(
237 &'a self,
238 connection_id: &'a ConnectionId,
239 ) -> BoxFuture<'a, SoapResult<Vec<ChannelId>>> {
240 Box::pin(async move {
241 let mut state = self.write()?;
242 let channels = state
243 .connections
244 .remove(connection_id)
245 .unwrap_or_default()
246 .into_iter()
247 .collect::<Vec<_>>();
248 for channel_id in &channels {
249 if let Some(members) = state.channels.get_mut(channel_id) {
250 members.remove(connection_id);
251 if members.is_empty() {
252 state.channels.remove(channel_id);
253 }
254 }
255 }
256 Ok(channels)
257 })
258 }
259}
260
261struct DeliveryState<M> {
262 queue: VecDeque<OutboundDelivery<M>>,
263 waiters: Vec<Waker>,
264}
265
266pub struct MemoryRealtimeDelivery<M> {
271 capacity: Option<NonZeroUsize>,
272 state: Mutex<DeliveryState<M>>,
273}
274
275impl<M> MemoryRealtimeDelivery<M> {
276 pub const fn new() -> Self {
278 Self {
279 capacity: None,
280 state: Mutex::new(DeliveryState {
281 queue: VecDeque::new(),
282 waiters: Vec::new(),
283 }),
284 }
285 }
286
287 pub const fn bounded(capacity: NonZeroUsize) -> Self {
289 Self {
290 capacity: Some(capacity),
291 state: Mutex::new(DeliveryState {
292 queue: VecDeque::new(),
293 waiters: Vec::new(),
294 }),
295 }
296 }
297
298 pub fn len(&self) -> SoapResult<usize> {
300 Ok(self.lock()?.queue.len())
301 }
302
303 pub fn is_empty(&self) -> SoapResult<bool> {
305 self.len().map(|length| length == 0)
306 }
307
308 pub fn drain(&self) -> SoapResult<Vec<OutboundDelivery<M>>> {
310 let (deliveries, waiters) = {
311 let mut state = self.lock()?;
312 let deliveries = state.queue.drain(..).collect();
313 let waiters = std::mem::take(&mut state.waiters);
314 (deliveries, waiters)
315 };
316 for waiter in waiters {
317 waiter.wake();
318 }
319 Ok(deliveries)
320 }
321
322 fn lock(&self) -> SoapResult<std::sync::MutexGuard<'_, DeliveryState<M>>> {
323 self.state
324 .lock()
325 .map_err(|_| SoapError::infrastructure("in-memory delivery lock poisoned"))
326 }
327
328 fn has_capacity(&self, length: usize) -> bool {
329 self.capacity.is_none_or(|capacity| length < capacity.get())
330 }
331}
332
333impl<M> Default for MemoryRealtimeDelivery<M> {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339impl<M> RealtimeDelivery<M> for MemoryRealtimeDelivery<M>
340where
341 M: Send,
342{
343 fn deliver(&self, delivery: OutboundDelivery<M>) -> BoxFuture<'_, SoapResult<DeliveryOutcome>> {
344 Box::pin(MemoryDeliveryFuture {
345 delivery: Mutex::new(Some(delivery)),
346 sink: self,
347 })
348 }
349}
350
351struct MemoryDeliveryFuture<'a, M> {
352 delivery: Mutex<Option<OutboundDelivery<M>>>,
353 sink: &'a MemoryRealtimeDelivery<M>,
354}
355
356impl<M> Future for MemoryDeliveryFuture<'_, M>
357where
358 M: Send,
359{
360 type Output = SoapResult<DeliveryOutcome>;
361
362 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
363 let future = self.as_ref().get_ref();
364 let mut state = match self.sink.lock() {
365 Ok(state) => state,
366 Err(error) => return Poll::Ready(Err(error)),
367 };
368 if self.sink.has_capacity(state.queue.len()) {
369 let mut delivery = match future.delivery.lock() {
370 Ok(delivery) => delivery,
371 Err(_) => {
372 return Poll::Ready(Err(SoapError::infrastructure(
373 "in-memory delivery future lock poisoned",
374 )));
375 }
376 };
377 let Some(delivery) = delivery.take() else {
378 return Poll::Ready(Err(SoapError::infrastructure(
379 "in-memory delivery future was polled after completion",
380 )));
381 };
382 state.queue.push_back(delivery);
383 return Poll::Ready(Ok(DeliveryOutcome::Accepted));
384 }
385
386 let backpressure = match future.delivery.lock() {
387 Ok(delivery) => delivery.as_ref().map(|delivery| delivery.backpressure),
388 Err(_) => {
389 return Poll::Ready(Err(SoapError::infrastructure(
390 "in-memory delivery future lock poisoned",
391 )));
392 }
393 };
394 let Some(backpressure) = backpressure else {
395 return Poll::Ready(Err(SoapError::infrastructure(
396 "in-memory delivery future lost its message",
397 )));
398 };
399 match backpressure {
400 BackpressurePolicy::Wait => {
401 if !state
402 .waiters
403 .iter()
404 .any(|waiter| waiter.will_wake(context.waker()))
405 {
406 state.waiters.push(context.waker().clone());
407 }
408 Poll::Pending
409 }
410 BackpressurePolicy::Reject => Poll::Ready(Err(SoapError::unavailable(
411 "realtime outbound delivery queue is full",
412 ))),
413 BackpressurePolicy::DropNewest => match future.delivery.lock() {
414 Ok(mut delivery) => {
415 *delivery = None;
416 Poll::Ready(Ok(DeliveryOutcome::Dropped))
417 }
418 Err(_) => Poll::Ready(Err(SoapError::infrastructure(
419 "in-memory delivery future lock poisoned",
420 ))),
421 },
422 }
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use std::{
429 num::NonZeroUsize,
430 pin::pin,
431 task::{Context, Poll, Waker},
432 time::SystemTime,
433 };
434
435 use soaprs_auth::{Authentication, Principal, StandardPrincipal};
436 use soaprs_contract_tests::{
437 block_on, verify_channel_membership_contract, verify_connection_presence_contract,
438 };
439 use soaprs_core::{MessageEnvelope, MessageMetadata, SoapErrorKind};
440 use soaprs_realtime::{
441 BackpressurePolicy, ChannelId, ConnectionContext, ConnectionId, DeliveryOutcome,
442 OutboundDelivery, RealtimeDelivery,
443 };
444
445 use super::{MemoryChannelMembership, MemoryConnectionPresence, MemoryRealtimeDelivery};
446
447 #[test]
448 fn presence_and_membership_pass_shared_contracts() {
449 let Some(first_id) = ConnectionId::new("connection-b").ok() else {
450 panic!("valid connection id");
451 };
452 let Some(second_id) = ConnectionId::new("connection-a").ok() else {
453 panic!("valid connection id");
454 };
455 let principal = || {
456 StandardPrincipal::new("player-1")
457 .and_then(|principal| principal.role("player"))
458 .and_then(|principal| Authentication::new("session", principal))
459 };
460 let (Some(first_auth), Some(second_auth)) = (principal().ok(), principal().ok()) else {
461 panic!("valid authentication");
462 };
463 let first = ConnectionContext::new(first_id.clone(), SystemTime::UNIX_EPOCH)
464 .authenticated(first_auth);
465 let second = ConnectionContext::new(second_id.clone(), SystemTime::UNIX_EPOCH)
466 .authenticated(second_auth);
467 let principal_id = first
468 .authentication()
469 .map(|authentication| authentication.principal().principal_id().clone());
470 let Some(principal_id) = principal_id else {
471 panic!("authenticated fixture");
472 };
473
474 let presence = MemoryConnectionPresence::new();
475 assert!(
476 block_on(verify_connection_presence_contract(
477 &presence,
478 first,
479 second,
480 &principal_id,
481 ))
482 .is_ok()
483 );
484
485 let Some(first_channel) = ChannelId::new("channel-b").ok() else {
486 panic!("valid channel id");
487 };
488 let Some(second_channel) = ChannelId::new("channel-a").ok() else {
489 panic!("valid channel id");
490 };
491 let membership = MemoryChannelMembership::new();
492 assert!(
493 block_on(verify_channel_membership_contract(
494 &membership,
495 &first_id,
496 &second_id,
497 &first_channel,
498 &second_channel,
499 ))
500 .is_ok()
501 );
502 }
503
504 #[test]
505 fn bounded_delivery_exposes_wait_reject_and_drop_backpressure() {
506 let capacity = NonZeroUsize::new(1).unwrap_or(NonZeroUsize::MIN);
507 let delivery = MemoryRealtimeDelivery::bounded(capacity);
508 let Some(connection_id) = ConnectionId::new("connection-1").ok() else {
509 panic!("valid connection id");
510 };
511 let message = |id: &'static str| {
512 MessageEnvelope::new(
513 id.to_owned(),
514 MessageMetadata::new(id, SystemTime::UNIX_EPOCH),
515 )
516 };
517
518 let first = OutboundDelivery::direct(connection_id.clone(), message("first"));
519 assert_eq!(
520 block_on(delivery.deliver(first)).ok(),
521 Some(DeliveryOutcome::Accepted)
522 );
523
524 let dropped = OutboundDelivery::direct(connection_id.clone(), message("dropped"))
525 .backpressure(BackpressurePolicy::DropNewest);
526 assert_eq!(
527 block_on(delivery.deliver(dropped)).ok(),
528 Some(DeliveryOutcome::Dropped)
529 );
530
531 let rejected = OutboundDelivery::direct(connection_id.clone(), message("rejected"))
532 .backpressure(BackpressurePolicy::Reject);
533 assert_eq!(
534 block_on(delivery.deliver(rejected))
535 .as_ref()
536 .map_err(soaprs_core::SoapError::kind),
537 Err(SoapErrorKind::Unavailable)
538 );
539
540 let waiting = OutboundDelivery::direct(connection_id, message("waiting"));
541 let mut waiting = pin!(delivery.deliver(waiting));
542 let mut context = Context::from_waker(Waker::noop());
543 assert!(matches!(waiting.as_mut().poll(&mut context), Poll::Pending));
544
545 let first_batch = delivery.drain();
546 assert_eq!(
547 first_batch
548 .ok()
549 .and_then(|batch| batch.first().map(|item| item.envelope.message.clone())),
550 Some("first".to_owned())
551 );
552 assert!(matches!(
553 waiting.as_mut().poll(&mut context),
554 Poll::Ready(Ok(DeliveryOutcome::Accepted))
555 ));
556 assert_eq!(delivery.len().ok(), Some(1));
557 }
558}