wsio_server/namespace/operators/
broadcast.rs1use 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#[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 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 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}