ironflow_engine/notify/
publisher.rs1use std::sync::Arc;
4
5use tokio::spawn;
6
7use super::{Event, EventSubscriber};
8
9struct Subscription {
11 subscriber: Arc<dyn EventSubscriber>,
12 event_types: Vec<&'static str>,
13}
14
15impl Subscription {
16 fn accepts(&self, event: &Event) -> bool {
18 self.event_types.contains(&event.event_type())
19 }
20}
21
22pub struct EventPublisher {
40 subscriptions: Vec<Subscription>,
41}
42
43impl EventPublisher {
44 pub fn new() -> Self {
55 Self {
56 subscriptions: Vec::new(),
57 }
58 }
59
60 pub fn subscribe(
88 &mut self,
89 subscriber: impl EventSubscriber + 'static,
90 event_types: &[&'static str],
91 ) {
92 self.subscriptions.push(Subscription {
93 subscriber: Arc::new(subscriber),
94 event_types: event_types.to_vec(),
95 });
96 }
97
98 pub fn subscriber_count(&self) -> usize {
100 self.subscriptions.len()
101 }
102
103 pub fn publish(&self, event: Event) {
108 for subscription in &self.subscriptions {
109 if !subscription.accepts(&event) {
110 continue;
111 }
112 let subscriber = subscription.subscriber.clone();
113 let event = event.clone();
114 spawn(async move {
115 subscriber.handle(&event).await;
116 });
117 }
118 }
119}
120
121impl Default for EventPublisher {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use std::collections::HashMap;
130 use std::sync::atomic::{AtomicU32, Ordering};
131 use std::time::Duration;
132
133 use super::*;
134 use crate::notify::{SubscriberFuture, WebhookSubscriber};
135 use rust_decimal::Decimal;
136 use tokio::time::sleep;
137
138 use chrono::Utc;
139 use ironflow_store::models::RunStatus;
140 use uuid::Uuid;
141
142 fn sample_run_status_changed() -> Event {
143 Event::RunStatusChanged {
144 run_id: Uuid::now_v7(),
145 workflow_name: "deploy".to_string(),
146 from: RunStatus::Running,
147 to: RunStatus::Completed,
148 error: None,
149 cost_usd: Decimal::new(42, 2),
150 duration_ms: 5000,
151 labels: HashMap::new(),
152 at: Utc::now(),
153 }
154 }
155
156 fn sample_user_signed_in() -> Event {
157 Event::UserSignedIn {
158 user_id: Uuid::now_v7(),
159 username: "alice".to_string(),
160 at: Utc::now(),
161 }
162 }
163
164 #[test]
165 fn starts_empty() {
166 let publisher = EventPublisher::new();
167 assert_eq!(publisher.subscriber_count(), 0);
168 }
169
170 #[test]
171 fn subscribe_increments_count() {
172 let mut publisher = EventPublisher::new();
173 publisher.subscribe(
174 WebhookSubscriber::new("https://example.com"),
175 &[Event::RUN_STATUS_CHANGED],
176 );
177 assert_eq!(publisher.subscriber_count(), 1);
178 }
179
180 #[test]
181 fn publish_with_no_subscribers_is_noop() {
182 let publisher = EventPublisher::new();
183 publisher.publish(sample_run_status_changed());
184 }
185
186 #[test]
187 fn default_is_empty() {
188 let publisher = EventPublisher::default();
189 assert_eq!(publisher.subscriber_count(), 0);
190 }
191
192 struct CountingSubscriber {
193 count: AtomicU32,
194 }
195
196 impl CountingSubscriber {
197 fn new() -> Self {
198 Self {
199 count: AtomicU32::new(0),
200 }
201 }
202
203 fn count(&self) -> u32 {
204 self.count.load(Ordering::SeqCst)
205 }
206 }
207
208 impl EventSubscriber for CountingSubscriber {
209 fn name(&self) -> &str {
210 "counting"
211 }
212
213 fn handle<'a>(&'a self, _event: &'a Event) -> SubscriberFuture<'a> {
214 Box::pin(async move {
215 self.count.fetch_add(1, Ordering::SeqCst);
216 })
217 }
218 }
219
220 #[tokio::test]
221 async fn subscriber_receives_matching_events() {
222 let subscriber = Arc::new(CountingSubscriber::new());
223 let mut publisher = EventPublisher::new();
224
225 struct ArcSub(Arc<CountingSubscriber>);
226 impl EventSubscriber for ArcSub {
227 fn name(&self) -> &str {
228 self.0.name()
229 }
230 fn handle<'a>(&'a self, event: &'a Event) -> SubscriberFuture<'a> {
231 self.0.handle(event)
232 }
233 }
234
235 publisher.subscribe(ArcSub(subscriber.clone()), &[Event::RUN_STATUS_CHANGED]);
236
237 publisher.publish(sample_run_status_changed()); publisher.publish(sample_user_signed_in()); sleep(Duration::from_millis(50)).await;
241
242 assert_eq!(subscriber.count(), 1);
243 }
244
245 #[tokio::test]
246 async fn all_filter_matches_everything() {
247 let subscriber = Arc::new(CountingSubscriber::new());
248 let mut publisher = EventPublisher::new();
249
250 struct ArcSub(Arc<CountingSubscriber>);
251 impl EventSubscriber for ArcSub {
252 fn name(&self) -> &str {
253 self.0.name()
254 }
255 fn handle<'a>(&'a self, event: &'a Event) -> SubscriberFuture<'a> {
256 self.0.handle(event)
257 }
258 }
259
260 publisher.subscribe(ArcSub(subscriber.clone()), Event::ALL);
261
262 publisher.publish(sample_run_status_changed());
263 publisher.publish(sample_user_signed_in());
264
265 sleep(Duration::from_millis(50)).await;
266
267 assert_eq!(subscriber.count(), 2);
268 }
269
270 #[tokio::test]
271 async fn empty_filter_matches_nothing() {
272 let subscriber = Arc::new(CountingSubscriber::new());
273 let mut publisher = EventPublisher::new();
274
275 struct ArcSub(Arc<CountingSubscriber>);
276 impl EventSubscriber for ArcSub {
277 fn name(&self) -> &str {
278 self.0.name()
279 }
280 fn handle<'a>(&'a self, event: &'a Event) -> SubscriberFuture<'a> {
281 self.0.handle(event)
282 }
283 }
284
285 publisher.subscribe(ArcSub(subscriber.clone()), &[]);
286
287 publisher.publish(sample_run_status_changed());
288 publisher.publish(sample_user_signed_in());
289
290 sleep(Duration::from_millis(50)).await;
291
292 assert_eq!(subscriber.count(), 0);
293 }
294}