1use nostr_sdk::prelude::*;
9
10#[derive(Clone, Debug, Default)]
14pub struct Query {
15 pub kinds: Vec<u16>,
16 pub z_tags: Vec<String>,
18 pub d_tags: Vec<String>,
21 pub authors: Vec<String>,
23 pub since: Option<u64>,
25 pub until: Option<u64>,
28 pub limit: Option<usize>,
30}
31
32impl Query {
33 pub fn matches(&self, event: &Event) -> bool {
35 if !self.kinds.is_empty() && !self.kinds.iter().any(|k| Kind::Custom(*k) == event.kind) {
39 return false;
40 }
41 if let Some(since) = self.since {
42 if event.created_at.as_secs() < since {
43 return false;
44 }
45 }
46 if let Some(until) = self.until {
47 if event.created_at.as_secs() > until {
48 return false;
49 }
50 }
51 if !self.authors.is_empty() && !self.authors.iter().any(|a| *a == event.pubkey.to_hex()) {
52 return false;
53 }
54 if !self.z_tags.is_empty() && !self.matches_single_letter("z", &self.z_tags, event) {
55 return false;
56 }
57 if !self.d_tags.is_empty() && !self.matches_single_letter("d", &self.d_tags, event) {
58 return false;
59 }
60 true
61 }
62
63 fn matches_single_letter(&self, name: &str, wanted: &[String], event: &Event) -> bool {
64 let val = event.tags.iter().find_map(|t| {
65 let s = t.as_slice();
66 (s.len() >= 2 && s[0] == name).then(|| s[1].clone())
67 });
68 matches!(val, Some(v) if wanted.iter().any(|w| *w == v))
69 }
70
71 pub fn to_filter(&self) -> Filter {
73 let mut filter = Filter::new();
74 if !self.kinds.is_empty() {
75 filter = filter.kinds(self.kinds.iter().map(|k| Kind::Custom(*k)));
76 }
77 if !self.z_tags.is_empty() {
78 filter = filter
79 .custom_tags(SingleLetterTag::lowercase(Alphabet::Z), self.z_tags.clone());
80 }
81 if !self.d_tags.is_empty() {
82 filter = filter.identifiers(self.d_tags.clone());
83 }
84 if !self.authors.is_empty() {
85 let authors: Vec<PublicKey> =
86 self.authors.iter().filter_map(|a| PublicKey::from_hex(a).ok()).collect();
87 if !authors.is_empty() {
88 filter = filter.authors(authors);
89 }
90 }
91 if let Some(since) = self.since {
92 filter = filter.since(Timestamp::from_secs(since));
93 }
94 if let Some(until) = self.until {
95 filter = filter.until(Timestamp::from_secs(until));
96 }
97 if let Some(limit) = self.limit {
98 filter = filter.limit(limit);
99 }
100 filter
101 }
102}
103
104#[async_trait::async_trait]
108pub trait Transport {
109 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String>;
111 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String>;
113
114 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String>;
122}
123
124pub const MAX_PUBLISH_ATTEMPTS: usize = 30;
127
128pub const FETCH_UNION_GRACE_MS: u64 = 3500;
132
133pub const CONFIRM_WINDOW: std::time::Duration = std::time::Duration::from_secs(30);
137
138pub async fn durable_broadcast<'a, F>(
145 relays: &[String],
146 max_attempts: usize,
147 backoff: std::time::Duration,
148 mut send_round: F,
149) -> Result<(), String>
150where
151 F: FnMut(Vec<String>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Vec<String>> + Send + 'a>>,
152{
153 let mut pending: Vec<String> = Vec::new();
154 for r in relays {
155 if !pending.contains(r) {
156 pending.push(r.clone());
157 }
158 }
159 let total = pending.len();
160 if total == 0 {
161 return Err("no relays to broadcast to".to_string());
162 }
163 for attempt in 0..max_attempts {
164 if pending.is_empty() {
165 break;
166 }
167 let acked = send_round(pending.clone()).await;
168 pending.retain(|r| !acked.contains(r));
169 if pending.is_empty() || attempt + 1 == max_attempts {
170 break;
171 }
172 if !backoff.is_zero() {
173 tokio::time::sleep(backoff).await;
174 }
175 }
176 if pending.len() < total {
177 Ok(()) } else {
179 Err(format!("no relay accepted the event after {max_attempts} attempts each"))
180 }
181}
182
183pub trait CommunityIngestSink: Send + Sync + 'static {
192 fn ingest_stragglers(&self, events: Vec<Event>);
193}
194
195static INGEST_SINK: std::sync::OnceLock<Box<dyn CommunityIngestSink>> = std::sync::OnceLock::new();
196
197pub fn set_community_ingest_sink(sink: Box<dyn CommunityIngestSink>) {
199 let _ = INGEST_SINK.set(sink);
200}
201
202fn submit_stragglers(events: Vec<Event>) {
203 if events.is_empty() {
204 return;
205 }
206 if let Some(sink) = INGEST_SINK.get() {
207 sink.ingest_stragglers(events);
208 }
209}
210
211static WARMED_RELAYS: std::sync::LazyLock<std::sync::Mutex<(u64, std::collections::HashSet<String>)>> =
217 std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashSet::new())));
218
219pub fn forget_warmed_relay(url: &str) {
224 WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner()).1.remove(url);
225}
226
227pub async fn prune_unneeded_community_relays(candidates: &[String]) {
235 if candidates.is_empty() {
236 return;
237 }
238 let Some(client) = crate::state::nostr_client() else { return };
239
240 let mut still_needed: std::collections::HashSet<String> = std::collections::HashSet::new();
241 if let Ok(ids) = crate::db::community::list_community_ids() {
242 for id in ids {
243 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
244 for r in &c.relays {
245 still_needed.insert(r.clone());
246 }
247 }
248 }
249 }
250
251 let pool = client.pool();
252 let pooled = pool.all_relays().await;
254 for url in candidates {
255 if still_needed.contains(url) {
256 continue;
257 }
258 if let Ok(parsed) = nostr_sdk::RelayUrl::parse(url) {
259 if let Some(relay) = pooled.get(&parsed) {
260 if relay.flags().has_read() || relay.flags().has_write() {
261 continue; }
263 }
264 let _ = pool.force_remove_relay(parsed).await; forget_warmed_relay(url);
266 }
267 }
268}
269
270pub struct LiveTransport {
277 timeout: std::time::Duration,
278}
279
280impl Default for LiveTransport {
281 fn default() -> Self {
282 Self { timeout: std::time::Duration::from_secs(10) }
283 }
284}
285
286impl LiveTransport {
287 pub fn new() -> Self {
288 Self::default()
289 }
290
291 pub fn with_timeout(timeout: std::time::Duration) -> Self {
292 Self { timeout }
293 }
294
295 async fn warm_client(relays: &[String], connect_timeout: std::time::Duration) -> Result<Client, String> {
301 if relays.is_empty() {
302 return Err("community has no relays configured".to_string());
303 }
304 let client = crate::state::nostr_client().ok_or_else(|| "nostr client not initialized".to_string())?;
305
306 let generation = crate::state::current_session_generation();
310 {
311 let warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
312 if warmed.0 == generation && relays.iter().all(|r| warmed.1.contains(r)) {
313 return Ok(client);
314 }
315 }
316
317 let mut added_new = false;
322 let mut succeeded: Vec<&String> = Vec::new();
323 for url in relays {
324 let opts = crate::community_relay_options();
325 match client.pool().add_relay(url.as_str(), opts).await {
326 Ok(true) => { added_new = true; succeeded.push(url); }
327 Ok(false) => { succeeded.push(url); }
328 Err(_) => {}
329 }
330 }
331 if succeeded.is_empty() {
332 return Err("no valid community relays could be added".to_string());
333 }
334 if added_new {
335 let _ = client.try_connect(connect_timeout).await;
340 } else {
341 client.connect().await;
343 }
344
345 {
348 let mut warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
349 if warmed.0 != generation {
350 warmed.0 = generation;
351 warmed.1.clear();
352 }
353 for url in succeeded {
354 warmed.1.insert(url.clone());
355 }
356 }
357 Ok(client)
358 }
359}
360
361#[async_trait::async_trait]
362impl Transport for LiveTransport {
363 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
364 let client = Self::warm_client(relays, self.timeout).await?;
365 let timeout = self.timeout;
366 let mut targets: Vec<String> = Vec::new();
367 for r in relays { if !targets.contains(r) { targets.push(r.clone()); } }
368 use futures_util::stream::{FuturesUnordered, StreamExt};
374 let mut sends: FuturesUnordered<_> = targets
375 .into_iter()
376 .map(|r| {
377 let client = client.clone();
378 let event = event.clone();
379 tokio::spawn(async move {
380 matches!(
381 tokio::time::timeout(timeout, client.send_event_to(vec![r.clone()], &event)).await,
382 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains(&u)).unwrap_or(false)
383 )
384 })
385 })
386 .collect();
387 while let Some(joined) = sends.next().await {
388 if matches!(joined, Ok(true)) {
389 return Ok(()); }
391 }
392 Err("no relay accepted the event".to_string())
393 }
394
395 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
396 let client = Self::warm_client(relays, self.timeout).await?;
397 let timeout = self.timeout;
398 let filter = query.to_filter();
399
400 let mut targets: Vec<String> = Vec::new();
401 for r in relays {
402 if !targets.contains(r) {
403 targets.push(r.clone());
404 }
405 }
406
407 if targets.len() <= 1 {
409 return client
410 .fetch_events_from(targets, filter, timeout)
411 .await
412 .map(|evs| evs.into_iter().collect())
413 .map_err(|e| e.to_string());
414 }
415
416 use futures_util::stream::{FuturesUnordered, StreamExt};
420 let mut fetches: FuturesUnordered<_> = targets
421 .into_iter()
422 .map(|r| {
423 let client = client.clone();
424 let filter = filter.clone();
425 tokio::spawn(async move {
426 client
427 .fetch_events_from(vec![r], filter, timeout)
428 .await
429 .map(|evs| evs.into_iter().collect::<Vec<Event>>())
430 .unwrap_or_default()
431 })
432 })
433 .collect();
434
435 let mut result: Vec<Event> = Vec::new();
444 loop {
445 match fetches.next().await {
446 Some(Ok(evs)) => {
447 result = evs;
448 break;
449 }
450 Some(Err(_)) => continue, None => break, }
453 }
454 let mut union_ids: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
455 if query.until.is_some() {
456 while let Some(joined) = fetches.next().await {
457 if let Ok(evs) = joined {
458 for e in evs {
459 if union_ids.insert(e.id) {
460 result.push(e);
461 }
462 }
463 }
464 }
465 } else if !fetches.is_empty() {
466 let grace = tokio::time::sleep(std::time::Duration::from_millis(FETCH_UNION_GRACE_MS));
467 tokio::pin!(grace);
468 loop {
469 tokio::select! {
470 _ = &mut grace => break,
471 next = fetches.next() => match next {
472 Some(Ok(evs)) => {
473 for e in evs {
474 if union_ids.insert(e.id) {
475 result.push(e);
476 }
477 }
478 }
479 Some(Err(_)) => continue,
480 None => break,
481 }
482 }
483 }
484 }
485
486 if !fetches.is_empty() {
490 let seen: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
491 let session = crate::state::SessionGuard::capture();
494 tokio::spawn(async move {
495 let mut extra: Vec<Event> = Vec::new();
496 let mut extra_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
497 while let Some(joined) = fetches.next().await {
498 if let Ok(evs) = joined {
499 for e in evs {
500 if !seen.contains(&e.id) && extra_ids.insert(e.id) {
501 extra.push(e);
502 }
503 }
504 }
505 }
506 if !session.is_valid() {
507 return;
508 }
509 submit_stragglers(extra);
510 });
511 }
512
513 Ok(result)
514 }
515
516 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
517 let client = Self::warm_client(relays, self.timeout).await?;
522 let timeout = self.timeout;
523 let event = event.clone();
524 let backoff = std::time::Duration::from_millis(750);
525 let mut pending: Vec<String> = Vec::new();
526 for r in relays { if !pending.contains(r) { pending.push(r.clone()); } }
527 if pending.is_empty() {
528 return Err("no relays to broadcast to".to_string());
529 }
530
531 let mut acked_any = false;
536 let _ = tokio::time::timeout(CONFIRM_WINDOW, async {
537 loop {
538 let sends = pending.iter().cloned().map(|r| {
539 let client = &client;
540 let event = &event;
541 Box::pin(async move {
542 match tokio::time::timeout(timeout, client.send_event_to(vec![r.clone()], event)).await {
543 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains(&u)).unwrap_or(false) => Ok(r),
544 _ => Err(()),
545 }
546 })
547 });
548 if let Ok((winner, _losers)) = futures_util::future::select_ok(sends).await {
549 acked_any = true;
550 pending.retain(|r| r != &winner);
551 break;
552 }
553 tokio::time::sleep(backoff).await;
554 }
555 })
556 .await;
557
558 if !acked_any {
559 return Err(format!("no relay accepted the event within {}s", CONFIRM_WINDOW.as_secs()));
560 }
561 if pending.is_empty() {
562 return Ok(()); }
564
565 tokio::spawn(async move {
570 let client_ref = &client;
571 let event_ref = &event;
572 let _ = durable_broadcast(&pending, MAX_PUBLISH_ATTEMPTS, backoff, move |round| {
573 Box::pin(async move {
574 match tokio::time::timeout(timeout, client_ref.send_event_to(round.clone(), event_ref)).await {
575 Ok(Ok(output)) => round.into_iter().filter(|p| RelayUrl::parse(p).map(|u| output.success.contains(&u)).unwrap_or(false)).collect(),
576 _ => Vec::new(),
577 }
578 })
579 })
580 .await;
581 });
582 Ok(())
583 }
584}
585
586#[cfg(test)]
587pub(crate) mod memory {
588 use super::*;
589 use std::collections::{HashMap, HashSet};
590 use std::sync::Mutex;
591
592 pub struct MemoryRelay {
597 per_relay: Mutex<HashMap<String, Vec<Event>>>,
598 }
599
600 impl MemoryRelay {
601 pub fn new() -> Self {
602 MemoryRelay { per_relay: Mutex::new(HashMap::new()) }
603 }
604
605 pub fn inject(&self, event: &Event, relays: &[String]) {
608 let d_tag = |e: &Event| e.tags.iter().find_map(|t| {
613 let s = t.as_slice();
614 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
615 }).unwrap_or_default();
616 let replaceable = (30000..40000).contains(&event.kind.as_u16());
617 let coord = (event.kind.as_u16(), event.pubkey, d_tag(event));
618 let mut map = self.per_relay.lock().unwrap();
619 for r in relays {
620 let v = map.entry(r.clone()).or_default();
621 if replaceable {
622 v.retain(|e| (e.kind.as_u16(), e.pubkey, d_tag(e)) != coord);
623 }
624 v.push(event.clone());
625 }
626 }
627
628 pub fn count_on(&self, relay: &str) -> usize {
630 self.per_relay.lock().unwrap().get(relay).map_or(0, |v| v.len())
631 }
632
633 fn apply_deletion(&self, deletion: &Event, relays: &[String]) {
638 let mut id_targets: HashSet<String> = HashSet::new();
639 let mut coord_targets: HashSet<String> = HashSet::new();
640 for t in deletion.tags.iter() {
641 let s = t.as_slice();
642 if s.len() >= 2 && s[0] == "e" {
643 id_targets.insert(s[1].clone());
644 } else if s.len() >= 2 && s[0] == "a" {
645 coord_targets.insert(s[1].clone());
646 }
647 }
648 let mut map = self.per_relay.lock().unwrap();
649 for r in relays {
650 if let Some(events) = map.get_mut(r) {
651 events.retain(|e| {
652 if e.pubkey != deletion.pubkey {
653 return true;
654 }
655 if id_targets.contains(&e.id.to_hex()) {
656 return false;
657 }
658 let d = e.tags.iter().find_map(|t| {
660 let s = t.as_slice();
661 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
662 }).unwrap_or_default();
663 let coord = format!("{}:{}:{}", e.kind.as_u16(), e.pubkey.to_hex(), d);
664 !coord_targets.contains(&coord)
665 });
666 }
667 }
668 }
669 }
670
671 #[async_trait::async_trait]
672 impl Transport for MemoryRelay {
673 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
674 if event.kind == Kind::EventDeletion {
676 self.apply_deletion(event, relays);
677 } else {
678 self.inject(event, relays);
679 }
680 Ok(())
681 }
682
683 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
686 self.publish(event, relays).await
687 }
688
689 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
690 let map = self.per_relay.lock().unwrap();
691 let mut seen = HashSet::new();
692 let mut out = Vec::new();
693 for r in relays {
694 if let Some(events) = map.get(r) {
695 for ev in events {
696 if query.matches(ev) && seen.insert(ev.id) {
697 out.push(ev.clone());
698 }
699 }
700 }
701 }
702 if let Some(limit) = query.limit {
705 out.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id)));
706 out.truncate(limit);
707 }
708 Ok(out)
709 }
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 fn evt(kind: u16, z: &str) -> Event {
718 EventBuilder::new(Kind::Custom(kind), "x")
719 .tags([Tag::custom(
720 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
721 [z.to_string()],
722 )])
723 .sign_with_keys(&Keys::generate())
724 .unwrap()
725 }
726
727 #[test]
728 fn query_matches_kind_and_z() {
729 let e = evt(3300, "abc");
730 assert!(Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: None, ..Default::default() }.matches(&e));
731 assert!(!Query { kinds: vec![3301], ..Default::default() }.matches(&e));
732 assert!(!Query { kinds: vec![], z_tags: vec!["xyz".into()], since: None, ..Default::default() }.matches(&e));
733 assert!(Query::default().matches(&e), "empty query matches anything");
734 }
735
736 fn evt_at(kind: u16, secs: u64) -> Event {
737 EventBuilder::new(Kind::Custom(kind), "x")
738 .custom_created_at(Timestamp::from(secs))
739 .sign_with_keys(&Keys::generate())
740 .unwrap()
741 }
742
743 fn evt_z_at(kind: u16, z: &str, secs: u64) -> Event {
746 EventBuilder::new(Kind::Custom(kind), "x")
747 .custom_created_at(Timestamp::from(secs))
748 .tags([Tag::custom(
749 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
750 [z.to_string()],
751 )])
752 .sign_with_keys(&Keys::generate())
753 .unwrap()
754 }
755
756 #[tokio::test]
757 async fn fetch_pages_with_until_and_limit_newest_first() {
758 let relay = super::memory::MemoryRelay::new();
760 let relays = vec!["r1".to_string()];
761 for s in 1..=5u64 {
762 relay.inject(&evt_z_at(3300, "pg", s), &relays);
763 }
764 let secs = |evs: &[Event]| evs.iter().map(|e| e.created_at.as_secs()).collect::<Vec<_>>();
765
766 let latest = relay
768 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], limit: Some(2), ..Default::default() }, &relays)
769 .await
770 .unwrap();
771 assert_eq!(secs(&latest), vec![5, 4]);
772
773 let older = relay
775 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(3), limit: Some(2), ..Default::default() }, &relays)
776 .await
777 .unwrap();
778 assert_eq!(secs(&older), vec![3, 2]);
779
780 let start = relay
783 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(1), limit: Some(2), ..Default::default() }, &relays)
784 .await
785 .unwrap();
786 assert_eq!(secs(&start), vec![1]);
787 }
788
789 #[test]
790 fn to_filter_translates_kinds_z_and_since() {
791 let q = Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: Some(100), ..Default::default() };
794 let filter = q.to_filter();
795 let matching = EventBuilder::new(Kind::Custom(3300), "x")
796 .custom_created_at(Timestamp::from(150))
797 .tags([Tag::custom(
798 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
799 ["abc".to_string()],
800 )])
801 .sign_with_keys(&Keys::generate())
802 .unwrap();
803 assert!(filter.match_event(&matching, MatchEventOptions::new()), "to_filter must accept what matches() accepts");
804 assert!(q.matches(&matching));
805
806 let wrong_kind = EventBuilder::new(Kind::Custom(3301), "x")
808 .custom_created_at(Timestamp::from(150))
809 .tags([Tag::custom(
810 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
811 ["abc".to_string()],
812 )])
813 .sign_with_keys(&Keys::generate())
814 .unwrap();
815 assert!(!filter.match_event(&wrong_kind, MatchEventOptions::new()));
816 }
817
818 #[test]
819 fn to_filter_empty_kinds_only_constrains_z_and_since() {
820 let q = Query { kinds: vec![], z_tags: vec!["p".into()], since: None, ..Default::default() };
822 let filter = q.to_filter();
823 let e = evt(3300, "p");
824 assert!(filter.match_event(&e, MatchEventOptions::new()));
825 assert!(q.matches(&e));
826 }
827
828 #[test]
829 fn since_is_an_inclusive_lower_bound() {
830 let below = evt_at(3300, 99);
831 let exact = evt_at(3300, 100);
832 let above = evt_at(3300, 101);
833 let q = Query { kinds: vec![3300], z_tags: vec![], since: Some(100), ..Default::default() };
834 assert!(!q.matches(&below), "below the floor is excluded");
835 assert!(q.matches(&exact), "exactly the floor is included");
836 assert!(q.matches(&above), "above the floor is included");
837 }
838
839 #[test]
840 fn z_tags_match_as_or_set() {
841 let e = evt(3300, "p2");
844 let q = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p2".into()], since: None, ..Default::default() };
845 assert!(q.matches(&e));
846 let miss = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p3".into()], since: None, ..Default::default() };
847 assert!(!miss.matches(&e));
848 }
849
850 #[tokio::test]
851 async fn fetch_unions_and_dedups_across_relays() {
852 use super::memory::MemoryRelay;
853 let relay = MemoryRelay::new();
854 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
855 let e = evt(3300, "p");
856 relay.publish(&e, &relays).await.unwrap();
857 let got = relay
858 .fetch(&Query { kinds: vec![3300], z_tags: vec!["p".into()], since: None, ..Default::default() }, &relays)
859 .await
860 .unwrap();
861 assert_eq!(got.len(), 1, "same event on 3 relays dedups to 1");
862 }
863
864 #[tokio::test]
865 async fn durable_broadcast_retries_only_the_failing_relays_until_they_ack() {
866 use std::cell::Cell;
869 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
870 let round = Cell::new(0usize);
871 let r2_round_seen = Cell::new(0usize);
872 let res = durable_broadcast(&relays, 30, std::time::Duration::ZERO, |pending| {
873 let n = round.get();
874 round.set(n + 1);
875 if n >= 1 {
877 assert_eq!(pending, vec!["r2".to_string()], "only the failing relay is retried");
878 r2_round_seen.set(r2_round_seen.get() + 1);
879 }
880 Box::pin(async move {
881 pending.into_iter().filter(|r| r != "r2" || n >= 4).collect()
882 })
883 })
884 .await;
885 assert!(res.is_ok(), "all relays eventually ACK → Ok");
886 assert!(round.get() >= 5, "kept retrying r2 across rounds");
887 }
888
889 #[tokio::test]
890 async fn durable_broadcast_is_ok_if_some_ack_even_when_one_never_does() {
891 let relays = vec!["r1".to_string(), "r2".to_string()];
894 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |pending| {
895 Box::pin(async move { pending.into_iter().filter(|r| r == "r1").collect() })
896 })
897 .await;
898 assert!(res.is_ok(), "≥1 relay accepted → Ok despite a permanently-failing relay");
899 }
900
901 #[tokio::test]
902 async fn durable_broadcast_errs_only_if_zero_relays_ever_accept() {
903 let relays = vec!["r1".to_string(), "r2".to_string()];
904 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |_pending| {
905 Box::pin(async move { Vec::new() }) })
907 .await;
908 assert!(res.is_err(), "zero acceptances after the retry cap → Err");
909 }
910
911 #[tokio::test]
912 async fn redundancy_self_heals_a_missing_relay() {
913 use super::memory::MemoryRelay;
914 let relay = MemoryRelay::new();
915 let all = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
916 let e = evt(3300, "p");
917 relay.inject(&e, &["r2".to_string()]);
919 assert_eq!(relay.count_on("r1"), 0);
920 assert_eq!(relay.count_on("r2"), 1);
921 let got = relay.fetch(&Query { kinds: vec![3300], ..Default::default() }, &all).await.unwrap();
923 assert_eq!(got.len(), 1);
924 }
925}