1use std::sync::Arc;
13
14use axum::extract::{Query, State};
15use axum::response::{IntoResponse, Response};
16use axum::routing::{get, post};
17use axum::{Json, Router, http::StatusCode};
18use dashmap::DashMap;
19use serde::{Deserialize, Serialize};
20use serde_json::{Value, json};
21use tokio::sync::Notify;
22use tokio::time::{Duration, timeout};
23
24use crate::route::Route;
25use crate::store::Store;
26
27pub const DEFAULT_RECV_TIMEOUT_MS: u64 = 25_000;
28
29pub const AWAY_RETAIN_MS: u64 = 3 * 24 * 60 * 60 * 1_000; const ROSTER_CAP: usize = 4096;
39
40const NOTIFY_CAP: usize = 4096;
43
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46pub struct Envelope {
47 pub payload: Value,
48 pub ts: u64,
50}
51
52#[derive(Clone)]
53pub struct Broker {
54 store: Store,
55 notifies: Arc<DashMap<String, Arc<Notify>>>,
58 roster: Arc<DashMap<String, (Value, u64)>>,
64 cap: usize,
65}
66
67impl Broker {
68 pub fn new(store: Store, cap: usize) -> Self {
69 Self {
70 store,
71 notifies: Arc::new(DashMap::new()),
72 roster: Arc::new(DashMap::new()),
73 cap: cap.max(1),
74 }
75 }
76
77 pub fn announce(&self, key: String, announcement: Value, now: u64) {
82 self.roster
83 .retain(|_, (_, at)| now.saturating_sub(*at) < AWAY_RETAIN_MS);
84 if self.roster.len() >= ROSTER_CAP && !self.roster.contains_key(&key) {
85 return; }
87 self.roster.insert(key, (announcement, now));
88 }
89
90 pub fn unregister(&self, key: &str) {
95 self.roster.remove(key);
96 }
97
98 pub fn roster(&self, now: u64) -> Vec<Value> {
103 self.roster
104 .iter()
105 .filter_map(|e| {
106 let (ann, at) = e.value();
107 let age = now.saturating_sub(*at);
108 if age >= AWAY_RETAIN_MS {
109 return None;
110 }
111 let mut v = ann.clone();
112 if let Some(obj) = v.as_object_mut() {
113 obj.insert("age_ms".into(), json!(age));
114 }
115 Some(v)
116 })
117 .collect()
118 }
119
120 fn notify_handle(&self, id: &str) -> Arc<Notify> {
121 if self.notifies.len() > NOTIFY_CAP && !self.notifies.contains_key(id) {
127 self.notifies.retain(|_, v| Arc::strong_count(v) > 1);
128 }
129 self.notifies
130 .entry(id.to_string())
131 .or_insert_with(|| Arc::new(Notify::new()))
132 .clone()
133 }
134
135 pub async fn enqueue(&self, to: &str, payload: Value, ts: u64) -> anyhow::Result<()> {
137 let bytes = serde_json::to_vec(&Envelope { payload, ts })?;
138 self.store.enqueue(to.to_string(), bytes).await?;
139 while self.store.depth(to.to_string()).await? > self.cap {
142 match self.store.peek_oldest(to.to_string()).await? {
143 Some((old_key, _)) => {
144 self.store.ack(old_key).await?;
145 tracing::warn!(to, cap = self.cap, "queue full; dropped oldest");
146 }
147 None => break,
148 }
149 }
150 self.notify_handle(to).notify_one();
151 Ok(())
152 }
153
154 pub async fn recv(
157 &self,
158 id: &str,
159 wait: Duration,
160 ) -> anyhow::Result<Option<(Envelope, String)>> {
161 let deadline = tokio::time::Instant::now() + wait;
162 let notify = self.notify_handle(id);
163 loop {
164 if let Some((key, bytes)) = self.store.peek_oldest(id.to_string()).await? {
165 return Ok(Some((serde_json::from_slice(&bytes)?, key)));
166 }
167 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
168 if remaining.is_zero() {
169 return Ok(None);
170 }
171 let notified = notify.notified();
174 if timeout(remaining, notified).await.is_err() {
175 return Ok(None);
176 }
177 }
178 }
179
180 pub async fn ack(&self, key: &str) -> anyhow::Result<()> {
181 self.store.ack(key.to_string()).await
182 }
183
184 pub async fn depth(&self, id: &str) -> anyhow::Result<usize> {
185 self.store.depth(id.to_string()).await
186 }
187
188 pub fn router(self) -> Router {
189 Router::new()
190 .route("/send", post(send))
191 .route("/recv", get(recv_handler))
192 .route("/ack", post(ack_handler))
193 .route("/announce", post(announce_handler))
194 .route("/unregister", post(unregister_handler))
195 .route("/roster", get(roster_handler))
196 .with_state(self)
197 }
198}
199
200#[derive(Deserialize)]
201struct SendBody {
202 to: String,
203 payload: Value,
204}
205
206async fn send(State(broker): State<Broker>, Json(body): Json<SendBody>) -> StatusCode {
207 match broker
208 .enqueue(&body.to, body.payload, crate::now_ms())
209 .await
210 {
211 Ok(()) => StatusCode::ACCEPTED,
212 Err(e) => {
213 tracing::error!("enqueue failed: {e}");
214 StatusCode::INTERNAL_SERVER_ERROR
215 }
216 }
217}
218
219#[derive(Deserialize)]
220struct RecvQuery {
221 me: String,
222 #[serde(default = "default_timeout")]
223 timeout_ms: u64,
224}
225
226fn default_timeout() -> u64 {
227 DEFAULT_RECV_TIMEOUT_MS
228}
229
230async fn recv_handler(State(broker): State<Broker>, Query(q): Query<RecvQuery>) -> Response {
231 match broker
232 .recv(&q.me, Duration::from_millis(q.timeout_ms))
233 .await
234 {
235 Ok(Some((env, ack))) => {
236 Json(json!({ "status": "message", "envelope": env, "ack": ack })).into_response()
237 }
238 Ok(None) => Json(json!({ "status": "timeout" })).into_response(),
239 Err(e) => {
240 tracing::error!("recv failed: {e}");
241 (
244 StatusCode::INTERNAL_SERVER_ERROR,
245 Json(json!({ "status": "error" })),
246 )
247 .into_response()
248 }
249 }
250}
251
252#[derive(Deserialize)]
253struct AckBody {
254 me: String,
255 ack: String,
256}
257
258async fn ack_handler(State(broker): State<Broker>, Json(body): Json<AckBody>) -> StatusCode {
259 if !body.ack.starts_with(&format!("{}\u{0}", body.me)) {
263 return StatusCode::FORBIDDEN;
264 }
265 match broker.ack(&body.ack).await {
266 Ok(()) => StatusCode::OK,
267 Err(e) => {
268 tracing::error!("ack failed: {e}");
269 StatusCode::INTERNAL_SERVER_ERROR
270 }
271 }
272}
273
274fn roster_key(body: &Value) -> Option<String> {
278 let pubkey = body.get("pubkey").and_then(|v| v.as_str())?;
279 let session_id = body
280 .get("session")
281 .and_then(|s| s.get("session_id"))
282 .and_then(|v| v.as_str())
283 .unwrap_or_default();
284 Some(Route::new(pubkey, session_id).to_string())
285}
286
287async fn announce_handler(State(broker): State<Broker>, Json(body): Json<Value>) -> StatusCode {
289 let Some(key) = roster_key(&body) else {
290 return StatusCode::BAD_REQUEST;
291 };
292 broker.announce(key, body, crate::now_ms());
293 StatusCode::ACCEPTED
294}
295
296async fn unregister_handler(State(broker): State<Broker>, Json(body): Json<Value>) -> StatusCode {
299 let Some(key) = roster_key(&body) else {
300 return StatusCode::BAD_REQUEST;
301 };
302 broker.unregister(&key);
303 StatusCode::ACCEPTED
304}
305
306async fn roster_handler(State(broker): State<Broker>) -> Response {
307 Json(json!({ "roster": broker.roster(crate::now_ms()) })).into_response()
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 fn broker(cap: usize) -> Broker {
315 Broker::new(Store::in_memory().unwrap(), cap)
316 }
317
318 #[tokio::test]
319 async fn enqueue_recv_ack_roundtrip() {
320 let b = broker(8);
321 b.enqueue("alice", json!({ "hi": 1 }), 5).await.unwrap();
322 let (env, ack) = b
323 .recv("alice", Duration::from_millis(50))
324 .await
325 .unwrap()
326 .unwrap();
327 assert_eq!(env.payload, json!({ "hi": 1 }));
328 assert_eq!(env.ts, 5);
329 assert_eq!(b.depth("alice").await.unwrap(), 1);
331 b.ack(&ack).await.unwrap();
332 assert_eq!(b.depth("alice").await.unwrap(), 0);
333 }
334
335 #[tokio::test]
336 async fn redelivers_until_acked() {
337 let b = broker(8);
338 b.enqueue("alice", json!("x"), 1).await.unwrap();
339 let (_e1, ack) = b
340 .recv("alice", Duration::from_millis(50))
341 .await
342 .unwrap()
343 .unwrap();
344 let (_e2, ack2) = b
346 .recv("alice", Duration::from_millis(50))
347 .await
348 .unwrap()
349 .unwrap();
350 assert_eq!(ack, ack2);
351 b.ack(&ack).await.unwrap();
352 assert!(
353 b.recv("alice", Duration::from_millis(20))
354 .await
355 .unwrap()
356 .is_none()
357 );
358 }
359
360 #[tokio::test]
361 async fn recv_times_out_when_empty() {
362 let b = broker(8);
363 assert!(
364 b.recv("nobody", Duration::from_millis(10))
365 .await
366 .unwrap()
367 .is_none()
368 );
369 }
370
371 #[tokio::test]
372 async fn fifo_order_across_acks() {
373 let b = broker(8);
374 for i in 0..3 {
375 b.enqueue("bob", json!(i), i).await.unwrap();
376 }
377 for i in 0..3 {
378 let (env, ack) = b
379 .recv("bob", Duration::from_millis(50))
380 .await
381 .unwrap()
382 .unwrap();
383 assert_eq!(env.payload, json!(i));
384 b.ack(&ack).await.unwrap();
385 }
386 }
387
388 #[tokio::test]
389 async fn bounded_queue_drops_oldest() {
390 let b = broker(2);
391 for i in 0..4 {
392 b.enqueue("bob", json!(i), i).await.unwrap();
393 }
394 assert_eq!(b.depth("bob").await.unwrap(), 2);
395 let (env, _) = b
396 .recv("bob", Duration::from_millis(50))
397 .await
398 .unwrap()
399 .unwrap();
400 assert_eq!(env.payload, json!(2), "0 and 1 were evicted");
401 }
402
403 #[tokio::test]
404 async fn a_waiting_recv_is_woken_by_a_later_send() {
405 let b = broker(8);
406 let b2 = b.clone();
407 let waiter = tokio::spawn(async move { b2.recv("alice", Duration::from_secs(2)).await });
408 tokio::time::sleep(Duration::from_millis(20)).await;
409 b.enqueue("alice", json!("wake"), 1).await.unwrap();
410 let (env, _) = waiter.await.unwrap().unwrap().unwrap();
411 assert_eq!(env.payload, json!("wake"));
412 }
413
414 #[test]
415 fn roster_stores_and_expires() {
416 let b = broker(8);
417 b.announce(
418 "keyA".into(),
419 json!({"pubkey":"keyA","name":"alice"}),
420 1_000,
421 );
422 b.announce("keyB".into(), json!({"pubkey":"keyB","name":"bob"}), 1_000);
423 assert_eq!(b.roster(1_000).len(), 2);
424 assert_eq!(
425 b.roster(1_000 + AWAY_RETAIN_MS - 1).len(),
426 2,
427 "retained within the away window"
428 );
429 assert_eq!(
430 b.roster(1_000 + AWAY_RETAIN_MS + 1).len(),
431 0,
432 "dropped past the retention bound"
433 );
434 }
435
436 #[test]
437 fn roster_stamps_age_ms() {
438 let b = broker(8);
439 b.announce(
440 "keyA".into(),
441 json!({"pubkey":"keyA","name":"alice"}),
442 1_000,
443 );
444 let r = b.roster(1_000 + 5_000);
445 assert_eq!(r.len(), 1);
446 assert_eq!(r[0]["age_ms"], json!(5_000u64), "age since last refresh");
447 assert_eq!(r[0]["pubkey"], json!("keyA"));
449 }
450
451 #[test]
452 fn announce_upserts_by_pubkey() {
453 let b = broker(8);
454 b.announce("keyA".into(), json!({"pubkey":"keyA","name":"old"}), 1_000);
455 b.announce("keyA".into(), json!({"pubkey":"keyA","name":"new"}), 2_000);
456 let r = b.roster(2_000);
457 assert_eq!(r.len(), 1, "same pubkey replaces, not appends");
458 assert_eq!(r[0]["name"], "new");
459 }
460
461 #[test]
462 fn roster_key_scopes_by_session() {
463 let s1 = json!({"pubkey":"keyA","session":{"session_id":"aa11"}});
464 let s2 = json!({"pubkey":"keyA","session":{"session_id":"bb22"}});
465 assert_eq!(roster_key(&s1).unwrap(), "keyA#aa11");
466 assert_ne!(
467 roster_key(&s1),
468 roster_key(&s2),
469 "distinct sessions distinct"
470 );
471 assert_eq!(roster_key(&json!({"pubkey":"keyA"})).unwrap(), "keyA");
473 assert!(
474 roster_key(&json!({"name":"x"})).is_none(),
475 "pubkey required"
476 );
477 }
478
479 #[test]
480 fn two_sessions_under_one_identity_coexist() {
481 let b = broker(8);
482 let s1 = json!({"pubkey":"keyA","session":{"session_id":"aa11"}});
483 let s2 = json!({"pubkey":"keyA","session":{"session_id":"bb22"}});
484 b.announce(roster_key(&s1).unwrap(), s1, 1_000);
485 b.announce(roster_key(&s2).unwrap(), s2, 1_000);
486 assert_eq!(b.roster(1_000).len(), 2, "same identity, two live sessions");
487 }
488
489 #[test]
490 fn unregister_removes_one_session_immediately() {
491 let b = broker(8);
492 let s1 = json!({"pubkey":"keyA","session":{"session_id":"aa11"}});
493 let s2 = json!({"pubkey":"keyA","session":{"session_id":"bb22"}});
494 b.announce(roster_key(&s1).unwrap(), s1.clone(), 1_000);
495 b.announce(roster_key(&s2).unwrap(), s2, 1_000);
496 b.unregister(&roster_key(&s1).unwrap());
497 let r = b.roster(1_000);
498 assert_eq!(r.len(), 1, "only the unregistered session is gone");
499 assert_eq!(r[0]["session"]["session_id"], "bb22");
500 }
501}