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::store::Store;
25
26pub const DEFAULT_RECV_TIMEOUT_MS: u64 = 25_000;
27
28pub const ROSTER_TTL_MS: u64 = 90_000;
31
32const ROSTER_CAP: usize = 4096;
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub struct Envelope {
39 pub payload: Value,
40 pub ts: u64,
42}
43
44#[derive(Clone)]
45pub struct Broker {
46 store: Store,
47 notifies: Arc<DashMap<String, Arc<Notify>>>,
50 roster: Arc<DashMap<String, (Value, u64)>>,
56 cap: usize,
57}
58
59impl Broker {
60 pub fn new(store: Store, cap: usize) -> Self {
61 Self {
62 store,
63 notifies: Arc::new(DashMap::new()),
64 roster: Arc::new(DashMap::new()),
65 cap: cap.max(1),
66 }
67 }
68
69 pub fn announce(&self, key: String, announcement: Value, now: u64) {
74 self.roster
75 .retain(|_, (_, at)| now.saturating_sub(*at) < ROSTER_TTL_MS);
76 if self.roster.len() >= ROSTER_CAP && !self.roster.contains_key(&key) {
77 return; }
79 self.roster.insert(key, (announcement, now));
80 }
81
82 pub fn unregister(&self, key: &str) {
87 self.roster.remove(key);
88 }
89
90 pub fn roster(&self, now: u64) -> Vec<Value> {
92 self.roster
93 .iter()
94 .filter(|e| now.saturating_sub(e.value().1) < ROSTER_TTL_MS)
95 .map(|e| e.value().0.clone())
96 .collect()
97 }
98
99 fn notify_handle(&self, id: &str) -> Arc<Notify> {
100 self.notifies
101 .entry(id.to_string())
102 .or_insert_with(|| Arc::new(Notify::new()))
103 .clone()
104 }
105
106 pub async fn enqueue(&self, to: &str, payload: Value, ts: u64) -> anyhow::Result<()> {
108 let bytes = serde_json::to_vec(&Envelope { payload, ts })?;
109 self.store.enqueue(to.to_string(), bytes).await?;
110 while self.store.depth(to.to_string()).await? > self.cap {
113 match self.store.peek_oldest(to.to_string()).await? {
114 Some((old_key, _)) => {
115 self.store.ack(old_key).await?;
116 tracing::warn!(to, cap = self.cap, "queue full; dropped oldest");
117 }
118 None => break,
119 }
120 }
121 self.notify_handle(to).notify_one();
122 Ok(())
123 }
124
125 pub async fn recv(
128 &self,
129 id: &str,
130 wait: Duration,
131 ) -> anyhow::Result<Option<(Envelope, String)>> {
132 let deadline = tokio::time::Instant::now() + wait;
133 let notify = self.notify_handle(id);
134 loop {
135 if let Some((key, bytes)) = self.store.peek_oldest(id.to_string()).await? {
136 return Ok(Some((serde_json::from_slice(&bytes)?, key)));
137 }
138 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
139 if remaining.is_zero() {
140 return Ok(None);
141 }
142 let notified = notify.notified();
145 if timeout(remaining, notified).await.is_err() {
146 return Ok(None);
147 }
148 }
149 }
150
151 pub async fn ack(&self, key: &str) -> anyhow::Result<()> {
152 self.store.ack(key.to_string()).await
153 }
154
155 pub async fn depth(&self, id: &str) -> anyhow::Result<usize> {
156 self.store.depth(id.to_string()).await
157 }
158
159 pub fn router(self) -> Router {
160 Router::new()
161 .route("/send", post(send))
162 .route("/recv", get(recv_handler))
163 .route("/ack", post(ack_handler))
164 .route("/announce", post(announce_handler))
165 .route("/unregister", post(unregister_handler))
166 .route("/roster", get(roster_handler))
167 .with_state(self)
168 }
169}
170
171#[derive(Deserialize)]
172struct SendBody {
173 to: String,
174 payload: Value,
175}
176
177async fn send(State(broker): State<Broker>, Json(body): Json<SendBody>) -> StatusCode {
178 match broker
179 .enqueue(&body.to, body.payload, crate::now_ms())
180 .await
181 {
182 Ok(()) => StatusCode::ACCEPTED,
183 Err(e) => {
184 tracing::error!("enqueue failed: {e}");
185 StatusCode::INTERNAL_SERVER_ERROR
186 }
187 }
188}
189
190#[derive(Deserialize)]
191struct RecvQuery {
192 me: String,
193 #[serde(default = "default_timeout")]
194 timeout_ms: u64,
195}
196
197fn default_timeout() -> u64 {
198 DEFAULT_RECV_TIMEOUT_MS
199}
200
201async fn recv_handler(State(broker): State<Broker>, Query(q): Query<RecvQuery>) -> Response {
202 match broker
203 .recv(&q.me, Duration::from_millis(q.timeout_ms))
204 .await
205 {
206 Ok(Some((env, ack))) => {
207 Json(json!({ "status": "message", "envelope": env, "ack": ack })).into_response()
208 }
209 Ok(None) => Json(json!({ "status": "timeout" })).into_response(),
210 Err(e) => {
211 tracing::error!("recv failed: {e}");
212 (
215 StatusCode::INTERNAL_SERVER_ERROR,
216 Json(json!({ "status": "error" })),
217 )
218 .into_response()
219 }
220 }
221}
222
223#[derive(Deserialize)]
224struct AckBody {
225 me: String,
226 ack: String,
227}
228
229async fn ack_handler(State(broker): State<Broker>, Json(body): Json<AckBody>) -> StatusCode {
230 if !body.ack.starts_with(&format!("{}\u{0}", body.me)) {
234 return StatusCode::FORBIDDEN;
235 }
236 match broker.ack(&body.ack).await {
237 Ok(()) => StatusCode::OK,
238 Err(e) => {
239 tracing::error!("ack failed: {e}");
240 StatusCode::INTERNAL_SERVER_ERROR
241 }
242 }
243}
244
245fn roster_key(body: &Value) -> Option<String> {
249 let pubkey = body.get("pubkey").and_then(|v| v.as_str())?;
250 let session_id = body
251 .get("session")
252 .and_then(|s| s.get("session_id"))
253 .and_then(|v| v.as_str())
254 .filter(|s| !s.is_empty());
255 Some(match session_id {
256 Some(sid) => format!("{pubkey}#{sid}"),
257 None => pubkey.to_string(),
258 })
259}
260
261async fn announce_handler(State(broker): State<Broker>, Json(body): Json<Value>) -> StatusCode {
263 let Some(key) = roster_key(&body) else {
264 return StatusCode::BAD_REQUEST;
265 };
266 broker.announce(key, body, crate::now_ms());
267 StatusCode::ACCEPTED
268}
269
270async fn unregister_handler(State(broker): State<Broker>, Json(body): Json<Value>) -> StatusCode {
273 let Some(key) = roster_key(&body) else {
274 return StatusCode::BAD_REQUEST;
275 };
276 broker.unregister(&key);
277 StatusCode::ACCEPTED
278}
279
280async fn roster_handler(State(broker): State<Broker>) -> Response {
281 Json(json!({ "roster": broker.roster(crate::now_ms()) })).into_response()
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 fn broker(cap: usize) -> Broker {
289 Broker::new(Store::in_memory().unwrap(), cap)
290 }
291
292 #[tokio::test]
293 async fn enqueue_recv_ack_roundtrip() {
294 let b = broker(8);
295 b.enqueue("alice", json!({ "hi": 1 }), 5).await.unwrap();
296 let (env, ack) = b
297 .recv("alice", Duration::from_millis(50))
298 .await
299 .unwrap()
300 .unwrap();
301 assert_eq!(env.payload, json!({ "hi": 1 }));
302 assert_eq!(env.ts, 5);
303 assert_eq!(b.depth("alice").await.unwrap(), 1);
305 b.ack(&ack).await.unwrap();
306 assert_eq!(b.depth("alice").await.unwrap(), 0);
307 }
308
309 #[tokio::test]
310 async fn redelivers_until_acked() {
311 let b = broker(8);
312 b.enqueue("alice", json!("x"), 1).await.unwrap();
313 let (_e1, ack) = b
314 .recv("alice", Duration::from_millis(50))
315 .await
316 .unwrap()
317 .unwrap();
318 let (_e2, ack2) = b
320 .recv("alice", Duration::from_millis(50))
321 .await
322 .unwrap()
323 .unwrap();
324 assert_eq!(ack, ack2);
325 b.ack(&ack).await.unwrap();
326 assert!(
327 b.recv("alice", Duration::from_millis(20))
328 .await
329 .unwrap()
330 .is_none()
331 );
332 }
333
334 #[tokio::test]
335 async fn recv_times_out_when_empty() {
336 let b = broker(8);
337 assert!(
338 b.recv("nobody", Duration::from_millis(10))
339 .await
340 .unwrap()
341 .is_none()
342 );
343 }
344
345 #[tokio::test]
346 async fn fifo_order_across_acks() {
347 let b = broker(8);
348 for i in 0..3 {
349 b.enqueue("bob", json!(i), i).await.unwrap();
350 }
351 for i in 0..3 {
352 let (env, ack) = b
353 .recv("bob", Duration::from_millis(50))
354 .await
355 .unwrap()
356 .unwrap();
357 assert_eq!(env.payload, json!(i));
358 b.ack(&ack).await.unwrap();
359 }
360 }
361
362 #[tokio::test]
363 async fn bounded_queue_drops_oldest() {
364 let b = broker(2);
365 for i in 0..4 {
366 b.enqueue("bob", json!(i), i).await.unwrap();
367 }
368 assert_eq!(b.depth("bob").await.unwrap(), 2);
369 let (env, _) = b
370 .recv("bob", Duration::from_millis(50))
371 .await
372 .unwrap()
373 .unwrap();
374 assert_eq!(env.payload, json!(2), "0 and 1 were evicted");
375 }
376
377 #[tokio::test]
378 async fn a_waiting_recv_is_woken_by_a_later_send() {
379 let b = broker(8);
380 let b2 = b.clone();
381 let waiter = tokio::spawn(async move { b2.recv("alice", Duration::from_secs(2)).await });
382 tokio::time::sleep(Duration::from_millis(20)).await;
383 b.enqueue("alice", json!("wake"), 1).await.unwrap();
384 let (env, _) = waiter.await.unwrap().unwrap().unwrap();
385 assert_eq!(env.payload, json!("wake"));
386 }
387
388 #[test]
389 fn roster_stores_and_expires() {
390 let b = broker(8);
391 b.announce(
392 "keyA".into(),
393 json!({"pubkey":"keyA","name":"alice"}),
394 1_000,
395 );
396 b.announce("keyB".into(), json!({"pubkey":"keyB","name":"bob"}), 1_000);
397 assert_eq!(b.roster(1_000).len(), 2);
398 assert_eq!(b.roster(1_000 + ROSTER_TTL_MS - 1).len(), 2, "within TTL");
399 assert_eq!(
400 b.roster(1_000 + ROSTER_TTL_MS + 1).len(),
401 0,
402 "expired past TTL"
403 );
404 }
405
406 #[test]
407 fn announce_upserts_by_pubkey() {
408 let b = broker(8);
409 b.announce("keyA".into(), json!({"pubkey":"keyA","name":"old"}), 1_000);
410 b.announce("keyA".into(), json!({"pubkey":"keyA","name":"new"}), 2_000);
411 let r = b.roster(2_000);
412 assert_eq!(r.len(), 1, "same pubkey replaces, not appends");
413 assert_eq!(r[0]["name"], "new");
414 }
415
416 #[test]
417 fn roster_key_scopes_by_session() {
418 let s1 = json!({"pubkey":"keyA","session":{"session_id":"aa11"}});
419 let s2 = json!({"pubkey":"keyA","session":{"session_id":"bb22"}});
420 assert_eq!(roster_key(&s1).unwrap(), "keyA#aa11");
421 assert_ne!(
422 roster_key(&s1),
423 roster_key(&s2),
424 "distinct sessions distinct"
425 );
426 assert_eq!(roster_key(&json!({"pubkey":"keyA"})).unwrap(), "keyA");
428 assert!(
429 roster_key(&json!({"name":"x"})).is_none(),
430 "pubkey required"
431 );
432 }
433
434 #[test]
435 fn two_sessions_under_one_identity_coexist() {
436 let b = broker(8);
437 let s1 = json!({"pubkey":"keyA","session":{"session_id":"aa11"}});
438 let s2 = json!({"pubkey":"keyA","session":{"session_id":"bb22"}});
439 b.announce(roster_key(&s1).unwrap(), s1, 1_000);
440 b.announce(roster_key(&s2).unwrap(), s2, 1_000);
441 assert_eq!(b.roster(1_000).len(), 2, "same identity, two live sessions");
442 }
443
444 #[test]
445 fn unregister_removes_one_session_immediately() {
446 let b = broker(8);
447 let s1 = json!({"pubkey":"keyA","session":{"session_id":"aa11"}});
448 let s2 = json!({"pubkey":"keyA","session":{"session_id":"bb22"}});
449 b.announce(roster_key(&s1).unwrap(), s1.clone(), 1_000);
450 b.announce(roster_key(&s2).unwrap(), s2, 1_000);
451 b.unregister(&roster_key(&s1).unwrap());
452 let r = b.roster(1_000);
453 assert_eq!(r.len(), 1, "only the unregistered session is gone");
454 assert_eq!(r[0]["session"]["session_id"], "bb22");
455 }
456}