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