Skip to main content

wsio_server/namespace/operators/
broadcast.rs

1use std::{
2    collections::HashSet,
3    sync::Arc,
4};
5
6use anyhow::Result;
7use futures_util::{
8    StreamExt,
9    future::ready,
10    stream::iter,
11};
12use roaring::RoaringTreemap;
13use serde::Serialize;
14
15use super::super::{
16    NamespaceStatus,
17    WsIoServerNamespace,
18};
19use crate::{
20    connection::WsIoServerConnection,
21    core::packet::WsIoPacket,
22};
23
24// Structs
25#[derive(Clone, Debug)]
26#[must_use]
27pub struct WsIoServerNamespaceBroadcastOperator {
28    exclude_connection_ids: HashSet<u64>,
29    exclude_rooms: HashSet<String>,
30    include_rooms: HashSet<String>,
31    namespace: Arc<WsIoServerNamespace>,
32}
33
34impl WsIoServerNamespaceBroadcastOperator {
35    #[inline]
36    pub(in super::super) fn new(namespace: Arc<WsIoServerNamespace>) -> Self {
37        Self {
38            exclude_connection_ids: HashSet::new(),
39            exclude_rooms: HashSet::new(),
40            include_rooms: HashSet::new(),
41            namespace,
42        }
43    }
44
45    // Private methods
46    async fn for_each_target_connections<F, Fut>(&self, f: F)
47    where
48        F: Fn(Arc<WsIoServerConnection>) -> Fut + Send + Sync + 'static,
49        Fut: Future<Output = Result<()>> + Send + 'static,
50    {
51        let target_connection_ids = self.target_connection_ids();
52        if target_connection_ids.is_empty() {
53            return;
54        }
55
56        iter(target_connection_ids)
57            .filter_map(|target_connection_id| {
58                ready(
59                    self.namespace
60                        .connections
61                        .get(&target_connection_id)
62                        .map(|entry| entry.value().clone()),
63                )
64            })
65            .for_each_concurrent(self.namespace.config.broadcast_concurrency_limit, |connection| async {
66                let _ = f(connection).await;
67            })
68            .await;
69    }
70
71    fn target_connection_ids(&self) -> RoaringTreemap {
72        let mut target_connection_ids = if self.include_rooms.is_empty() {
73            (**self.namespace.connection_ids.load()).clone()
74        } else {
75            let mut connection_ids = RoaringTreemap::new();
76            for room_name in &self.include_rooms {
77                if let Some(room) = self.namespace.rooms.get(room_name) {
78                    connection_ids |= room.value();
79                }
80            }
81
82            connection_ids
83        };
84
85        for room_name in &self.exclude_rooms {
86            if let Some(room) = self.namespace.rooms.get(room_name) {
87                target_connection_ids -= room.value();
88                if target_connection_ids.is_empty() {
89                    break;
90                }
91            }
92        }
93
94        for exclude_connection_id in &self.exclude_connection_ids {
95            target_connection_ids.remove(*exclude_connection_id);
96        }
97
98        #[cfg(feature = "tracing")]
99        tracing::trace!(
100            namespace = self.namespace.path(),
101            target_count = target_connection_ids.len(),
102            include_room_count = self.include_rooms.len(),
103            exclude_room_count = self.exclude_rooms.len(),
104            exclude_connection_count = self.exclude_connection_ids.len(),
105            "resolved broadcast targets"
106        );
107
108        target_connection_ids
109    }
110
111    // Public methods
112    pub async fn close(self) {
113        #[cfg(feature = "tracing")]
114        tracing::debug!(
115            namespace = self.namespace.path(),
116            "broadcasting close to namespace targets"
117        );
118
119        self.for_each_target_connections(|connection| async move {
120            connection.close();
121            Ok(())
122        })
123        .await;
124    }
125
126    pub async fn disconnect(self) -> Result<()> {
127        #[cfg(feature = "tracing")]
128        tracing::debug!(
129            namespace = self.namespace.path(),
130            "broadcasting disconnect to namespace targets"
131        );
132
133        let message = self.namespace.encode_packet_to_message(&WsIoPacket::new_disconnect())?;
134        self.for_each_target_connections(move |connection| {
135            let message = message.clone();
136            async move { connection.send_message(message).await }
137        })
138        .await;
139
140        Ok(())
141    }
142
143    pub async fn emit<D: Serialize>(self, event: impl AsRef<str>, data: Option<&D>) -> Result<()> {
144        self.namespace.status.ensure(NamespaceStatus::Running, |status| {
145            format!("Cannot emit in invalid status: {status:?}")
146        })?;
147        let event = event.as_ref();
148
149        #[cfg(feature = "tracing")]
150        tracing::trace!(
151            namespace = self.namespace.path(),
152            event,
153            has_data = data.is_some(),
154            "broadcasting event to namespace targets"
155        );
156
157        let message = self.namespace.encode_packet_to_message(&WsIoPacket::new_event(
158            event,
159            data.map(|data| self.namespace.config.packet_codec.encode_data(data))
160                .transpose()?,
161        ))?;
162
163        self.for_each_target_connections(move |connection| {
164            let message = message.clone();
165            async move { connection.emit_event_message(message).await }
166        })
167        .await;
168
169        Ok(())
170    }
171
172    #[inline]
173    pub fn except(mut self, room_names: impl IntoIterator<Item = impl Into<String>>) -> Self {
174        self.exclude_rooms.extend(room_names.into_iter().map(Into::into));
175        self
176    }
177
178    pub fn except_connection_ids(mut self, connection_ids: impl IntoIterator<Item = u64>) -> Self {
179        self.exclude_connection_ids.extend(connection_ids);
180        self
181    }
182
183    #[inline]
184    pub fn to(mut self, room_names: impl IntoIterator<Item = impl Into<String>>) -> Self {
185        self.include_rooms.extend(room_names.into_iter().map(Into::into));
186        self
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::WsIoServer;
194
195    fn namespace_with_rooms() -> Arc<WsIoServerNamespace> {
196        let namespace = WsIoServer::builder()
197            .build()
198            .new_namespace_builder("/test")
199            .register()
200            .unwrap();
201
202        namespace
203            .connection_ids
204            .store(Arc::new(RoaringTreemap::from_iter([1, 2, 3])));
205
206        namespace.add_connection_id_to_room("included", 1);
207        namespace.add_connection_id_to_room("included", 2);
208        namespace.add_connection_id_to_room("excluded", 2);
209        namespace.add_connection_id_to_room("excluded", 3);
210        namespace
211    }
212
213    #[test]
214    fn resolves_existing_include_room_members() {
215        let targets = namespace_with_rooms().to(["included"]).target_connection_ids();
216
217        assert_eq!(targets, RoaringTreemap::from_iter([1, 2]));
218    }
219
220    #[test]
221    fn resolves_included_and_excluded_room_intersection() {
222        let targets = namespace_with_rooms()
223            .to(["included"])
224            .except(["excluded"])
225            .target_connection_ids();
226
227        assert_eq!(targets, RoaringTreemap::from_iter([1]));
228    }
229
230    #[test]
231    fn resolves_empty_include_room_to_no_targets() {
232        let targets = namespace_with_rooms().to(["missing"]).target_connection_ids();
233
234        assert!(targets.is_empty());
235    }
236}