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 p_tags: Vec<String>,
24 pub k_tags: Vec<String>,
27 pub authors: Vec<String>,
29 pub since: Option<u64>,
31 pub until: Option<u64>,
34 pub limit: Option<usize>,
36}
37
38impl Query {
39 pub fn matches(&self, event: &Event) -> bool {
41 if !self.kinds.is_empty() && !self.kinds.iter().any(|k| Kind::Custom(*k) == event.kind) {
45 return false;
46 }
47 if let Some(since) = self.since {
48 if event.created_at.as_secs() < since {
49 return false;
50 }
51 }
52 if let Some(until) = self.until {
53 if event.created_at.as_secs() > until {
54 return false;
55 }
56 }
57 if !self.authors.is_empty() && !self.authors.iter().any(|a| *a == event.pubkey.to_hex()) {
58 return false;
59 }
60 if !self.z_tags.is_empty() && !self.matches_single_letter("z", &self.z_tags, event) {
61 return false;
62 }
63 if !self.d_tags.is_empty() && !self.matches_single_letter("d", &self.d_tags, event) {
64 return false;
65 }
66 if !self.p_tags.is_empty() && !self.matches_single_letter("p", &self.p_tags, event) {
67 return false;
68 }
69 if !self.k_tags.is_empty() && !self.matches_single_letter("k", &self.k_tags, event) {
70 return false;
71 }
72 true
73 }
74
75 fn matches_single_letter(&self, name: &str, wanted: &[String], event: &Event) -> bool {
76 event.tags.iter().any(|t| {
79 let s = t.as_slice();
80 s.len() >= 2 && s[0] == name && wanted.iter().any(|w| *w == s[1])
81 })
82 }
83
84 pub fn to_filter(&self) -> Filter {
86 let mut filter = Filter::new();
87 if !self.kinds.is_empty() {
88 filter = filter.kinds(self.kinds.iter().map(|k| Kind::Custom(*k)));
89 }
90 if !self.z_tags.is_empty() {
91 filter = filter
92 .custom_tags(SingleLetterTag::lowercase(Alphabet::Z), self.z_tags.clone());
93 }
94 if !self.d_tags.is_empty() {
95 filter = filter.identifiers(self.d_tags.clone());
96 }
97 if !self.p_tags.is_empty() {
98 filter = filter
99 .custom_tags(SingleLetterTag::lowercase(Alphabet::P), self.p_tags.clone());
100 }
101 if !self.k_tags.is_empty() {
102 filter = filter
103 .custom_tags(SingleLetterTag::lowercase(Alphabet::K), self.k_tags.clone());
104 }
105 if !self.authors.is_empty() {
106 let authors: Vec<PublicKey> =
107 self.authors.iter().filter_map(|a| PublicKey::from_hex(a).ok()).collect();
108 if !authors.is_empty() {
109 filter = filter.authors(authors);
110 }
111 }
112 if let Some(since) = self.since {
113 filter = filter.since(Timestamp::from_secs(since));
114 }
115 if let Some(until) = self.until {
116 filter = filter.until(Timestamp::from_secs(until));
117 }
118 if let Some(limit) = self.limit {
119 filter = filter.limit(limit);
120 }
121 filter
122 }
123}
124
125#[async_trait::async_trait]
129pub trait Transport {
130 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String>;
132 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String>;
134
135 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String>;
143}
144
145pub const MAX_PUBLISH_ATTEMPTS: usize = 30;
148
149pub const FETCH_UNION_GRACE_MS: u64 = 3500;
153
154pub const CONFIRM_WINDOW: std::time::Duration = std::time::Duration::from_secs(30);
158
159pub async fn durable_broadcast<'a, F>(
166 relays: &[String],
167 max_attempts: usize,
168 backoff: std::time::Duration,
169 mut send_round: F,
170) -> Result<(), String>
171where
172 F: FnMut(Vec<String>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Vec<String>> + Send + 'a>>,
173{
174 let mut pending: Vec<String> = Vec::new();
175 for r in relays {
176 if !pending.contains(r) {
177 pending.push(r.clone());
178 }
179 }
180 let total = pending.len();
181 if total == 0 {
182 return Err("no relays to broadcast to".to_string());
183 }
184 for attempt in 0..max_attempts {
185 if pending.is_empty() {
186 break;
187 }
188 let acked = send_round(pending.clone()).await;
189 pending.retain(|r| !acked.contains(r));
190 if pending.is_empty() || attempt + 1 == max_attempts {
191 break;
192 }
193 if !backoff.is_zero() {
194 tokio::time::sleep(backoff).await;
195 }
196 }
197 if pending.len() < total {
198 Ok(()) } else {
200 Err(format!("no relay accepted the event after {max_attempts} attempts each"))
201 }
202}
203
204pub trait CommunityIngestSink: Send + Sync + 'static {
213 fn ingest_stragglers(&self, events: Vec<Event>);
214}
215
216static INGEST_SINK: std::sync::OnceLock<Box<dyn CommunityIngestSink>> = std::sync::OnceLock::new();
217
218pub fn set_community_ingest_sink(sink: Box<dyn CommunityIngestSink>) {
220 let _ = INGEST_SINK.set(sink);
221}
222
223fn submit_stragglers(events: Vec<Event>) {
224 if events.is_empty() {
225 return;
226 }
227 if let Some(sink) = INGEST_SINK.get() {
228 sink.ingest_stragglers(events);
229 }
230}
231
232static WARMED_RELAYS: std::sync::LazyLock<std::sync::Mutex<(u64, std::collections::HashSet<String>)>> =
238 std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashSet::new())));
239
240pub fn forget_warmed_relay(url: &str) {
245 WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner()).1.remove(url);
246}
247
248#[cfg(feature = "tor")]
252const TOR_READY_WAIT: std::time::Duration = std::time::Duration::from_secs(30);
253
254#[allow(dead_code)]
264async fn wait_until_tor_ready<F: Fn() -> bool>(
265 is_blocked: F,
266 max_wait: std::time::Duration,
267) -> Result<(), String> {
268 if !is_blocked() {
269 return Ok(());
270 }
271 let deadline = std::time::Instant::now() + max_wait;
272 while is_blocked() {
273 if std::time::Instant::now() >= deadline {
274 return Err("Tor is still connecting. Wait a moment and try again.".to_string());
275 }
276 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
277 }
278 Ok(())
279}
280
281pub async fn prune_unneeded_community_relays(candidates: &[String]) {
289 if candidates.is_empty() {
290 return;
291 }
292 let Some(client) = crate::state::nostr_client() else { return };
293
294 let mut still_needed: std::collections::HashSet<String> = std::collections::HashSet::new();
295 if let Ok(ids) = crate::db::community::list_community_ids() {
296 for id in ids {
297 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
298 for r in &c.relays {
299 still_needed.insert(r.clone());
300 }
301 }
302 }
303 }
304
305 let pool = client.pool();
306 let pooled = pool.all_relays().await;
308 for url in candidates {
309 if still_needed.contains(url) {
310 continue;
311 }
312 if let Ok(parsed) = nostr_sdk::RelayUrl::parse(url) {
313 if let Some(relay) = pooled.get(&parsed) {
314 if relay.flags().has_read() || relay.flags().has_write() {
315 continue; }
317 }
318 let _ = pool.force_remove_relay(parsed).await; forget_warmed_relay(url);
320 }
321 }
322}
323
324pub struct LiveTransport {
331 timeout: std::time::Duration,
332}
333
334impl Default for LiveTransport {
335 fn default() -> Self {
336 Self { timeout: std::time::Duration::from_secs(10) }
337 }
338}
339
340impl LiveTransport {
341 pub fn new() -> Self {
342 Self::default()
343 }
344
345 pub fn with_timeout(timeout: std::time::Duration) -> Self {
346 Self { timeout }
347 }
348
349 async fn warm_client(relays: &[String], connect_timeout: std::time::Duration) -> Result<Client, String> {
355 if relays.is_empty() {
356 return Err("community has no relays configured".to_string());
357 }
358 #[cfg(feature = "tor")]
364 wait_until_tor_ready(
365 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
366 TOR_READY_WAIT,
367 ).await?;
368 let client = crate::state::nostr_client().ok_or_else(|| "nostr client not initialized".to_string())?;
369
370 let generation = crate::state::current_session_generation();
374 {
375 let warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
376 if warmed.0 == generation && relays.iter().all(|r| warmed.1.contains(r)) {
377 return Ok(client);
378 }
379 }
380
381 let mut added_new = false;
386 let mut succeeded: Vec<&String> = Vec::new();
387 for url in relays {
388 let opts = crate::community_relay_options();
389 match client.pool().add_relay(url.as_str(), opts).await {
390 Ok(true) => { added_new = true; succeeded.push(url); }
391 Ok(false) => { succeeded.push(url); }
392 Err(_) => {}
393 }
394 }
395 if succeeded.is_empty() {
396 return Err("no valid community relays could be added".to_string());
397 }
398 if added_new {
399 let _ = client.try_connect(connect_timeout).await;
404 } else {
405 client.connect().await;
407 }
408
409 {
412 let mut warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
413 if warmed.0 != generation {
414 warmed.0 = generation;
415 warmed.1.clear();
416 }
417 for url in succeeded {
418 warmed.1.insert(url.clone());
419 }
420 }
421 Ok(client)
422 }
423}
424
425#[async_trait::async_trait]
426impl Transport for LiveTransport {
427 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
428 let client = Self::warm_client(relays, self.timeout).await?;
429 let timeout = self.timeout;
430 let mut targets: Vec<String> = Vec::new();
431 for r in relays { if !targets.contains(r) { targets.push(r.clone()); } }
432 use futures_util::stream::{FuturesUnordered, StreamExt};
438 let mut sends: FuturesUnordered<_> = targets
439 .into_iter()
440 .map(|r| {
441 let client = client.clone();
442 let event = event.clone();
443 tokio::spawn(async move {
444 matches!(
445 tokio::time::timeout(timeout, client.send_event_to(vec![r.clone()], &event)).await,
446 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains(&u)).unwrap_or(false)
447 )
448 })
449 })
450 .collect();
451 while let Some(joined) = sends.next().await {
452 if matches!(joined, Ok(true)) {
453 return Ok(()); }
455 }
456 Err("no relay accepted the event".to_string())
457 }
458
459 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
460 let client = Self::warm_client(relays, self.timeout).await?;
461 let timeout = self.timeout;
462 let filter = query.to_filter();
463
464 let mut targets: Vec<String> = Vec::new();
465 for r in relays {
466 if !targets.contains(r) {
467 targets.push(r.clone());
468 }
469 }
470
471 if targets.len() <= 1 {
473 return client
474 .fetch_events_from(targets, filter, timeout)
475 .await
476 .map(|evs| evs.into_iter().collect())
477 .map_err(|e| e.to_string());
478 }
479
480 use futures_util::stream::{FuturesUnordered, StreamExt};
484 let mut fetches: FuturesUnordered<_> = targets
485 .into_iter()
486 .map(|r| {
487 let client = client.clone();
488 let filter = filter.clone();
489 tokio::spawn(async move {
490 client
491 .fetch_events_from(vec![r], filter, timeout)
492 .await
493 .map(|evs| evs.into_iter().collect::<Vec<Event>>())
494 .unwrap_or_default()
495 })
496 })
497 .collect();
498
499 let mut result: Vec<Event> = Vec::new();
508 loop {
509 match fetches.next().await {
510 Some(Ok(evs)) => {
511 result = evs;
512 break;
513 }
514 Some(Err(_)) => continue, None => break, }
517 }
518 let mut union_ids: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
519 if query.until.is_some() {
520 while let Some(joined) = fetches.next().await {
521 if let Ok(evs) = joined {
522 for e in evs {
523 if union_ids.insert(e.id) {
524 result.push(e);
525 }
526 }
527 }
528 }
529 } else if !fetches.is_empty() {
530 let grace = tokio::time::sleep(std::time::Duration::from_millis(FETCH_UNION_GRACE_MS));
531 tokio::pin!(grace);
532 loop {
533 tokio::select! {
534 _ = &mut grace => break,
535 next = fetches.next() => match next {
536 Some(Ok(evs)) => {
537 for e in evs {
538 if union_ids.insert(e.id) {
539 result.push(e);
540 }
541 }
542 }
543 Some(Err(_)) => continue,
544 None => break,
545 }
546 }
547 }
548 }
549
550 if !fetches.is_empty() {
554 let seen: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
555 let session = crate::state::SessionGuard::capture();
558 tokio::spawn(async move {
559 let mut extra: Vec<Event> = Vec::new();
560 let mut extra_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
561 while let Some(joined) = fetches.next().await {
562 if let Ok(evs) = joined {
563 for e in evs {
564 if !seen.contains(&e.id) && extra_ids.insert(e.id) {
565 extra.push(e);
566 }
567 }
568 }
569 }
570 if !session.is_valid() {
571 return;
572 }
573 submit_stragglers(extra);
574 });
575 }
576
577 Ok(result)
578 }
579
580 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
581 let client = Self::warm_client(relays, self.timeout).await?;
586 let timeout = self.timeout;
587 let event = event.clone();
588 let backoff = std::time::Duration::from_millis(750);
589 let mut pending: Vec<String> = Vec::new();
590 for r in relays { if !pending.contains(r) { pending.push(r.clone()); } }
591 if pending.is_empty() {
592 return Err("no relays to broadcast to".to_string());
593 }
594
595 let mut acked_any = false;
600 let _ = tokio::time::timeout(CONFIRM_WINDOW, async {
601 loop {
602 let sends = pending.iter().cloned().map(|r| {
603 let client = &client;
604 let event = &event;
605 Box::pin(async move {
606 match tokio::time::timeout(timeout, client.send_event_to(vec![r.clone()], event)).await {
607 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains(&u)).unwrap_or(false) => Ok(r),
608 _ => Err(()),
609 }
610 })
611 });
612 if let Ok((winner, _losers)) = futures_util::future::select_ok(sends).await {
613 acked_any = true;
614 pending.retain(|r| r != &winner);
615 break;
616 }
617 tokio::time::sleep(backoff).await;
618 }
619 })
620 .await;
621
622 if !acked_any {
623 return Err(format!("no relay accepted the event within {}s", CONFIRM_WINDOW.as_secs()));
624 }
625 if pending.is_empty() {
626 return Ok(()); }
628
629 tokio::spawn(async move {
634 let client_ref = &client;
635 let event_ref = &event;
636 let _ = durable_broadcast(&pending, MAX_PUBLISH_ATTEMPTS, backoff, move |round| {
637 Box::pin(async move {
638 match tokio::time::timeout(timeout, client_ref.send_event_to(round.clone(), event_ref)).await {
639 Ok(Ok(output)) => round.into_iter().filter(|p| RelayUrl::parse(p).map(|u| output.success.contains(&u)).unwrap_or(false)).collect(),
640 _ => Vec::new(),
641 }
642 })
643 })
644 .await;
645 });
646 Ok(())
647 }
648}
649
650#[cfg(test)]
651pub(crate) mod memory {
652 use super::*;
653 use std::collections::{HashMap, HashSet};
654 use std::sync::Mutex;
655
656 pub struct MemoryRelay {
661 per_relay: Mutex<HashMap<String, Vec<Event>>>,
662 subscribers: Mutex<Vec<(Query, tokio::sync::mpsc::UnboundedSender<Event>)>>,
663 }
664
665 fn is_ephemeral(kind: u16) -> bool {
668 (20000..30000).contains(&kind)
669 }
670
671 impl MemoryRelay {
672 pub fn new() -> Self {
673 MemoryRelay {
674 per_relay: Mutex::new(HashMap::new()),
675 subscribers: Mutex::new(Vec::new()),
676 }
677 }
678
679 pub fn subscribe(&self, query: Query) -> tokio::sync::mpsc::UnboundedReceiver<Event> {
682 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
683 self.subscribers.lock().unwrap().push((query, tx));
684 rx
685 }
686
687 fn deliver(&self, event: &Event) {
689 self.subscribers.lock().unwrap().retain(|(q, tx)| {
690 if q.matches(event) {
691 tx.send(event.clone()).is_ok()
692 } else {
693 !tx.is_closed()
694 }
695 });
696 }
697
698 pub fn inject(&self, event: &Event, relays: &[String]) {
701 self.deliver(event);
702 if is_ephemeral(event.kind.as_u16()) {
703 return; }
705 let d_tag = |e: &Event| e.tags.iter().find_map(|t| {
711 let s = t.as_slice();
712 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
713 }).unwrap_or_default();
714 let k = event.kind.as_u16();
715 let replaceable = (30000..40000).contains(&k) || (10000..20000).contains(&k) || k == 0 || k == 3;
716 let coord = (event.kind.as_u16(), event.pubkey, d_tag(event));
717 let mut map = self.per_relay.lock().unwrap();
718 for r in relays {
719 let v = map.entry(r.clone()).or_default();
720 if replaceable {
721 v.retain(|e| (e.kind.as_u16(), e.pubkey, d_tag(e)) != coord);
722 }
723 v.push(event.clone());
724 }
725 }
726
727 pub fn count_on(&self, relay: &str) -> usize {
729 self.per_relay.lock().unwrap().get(relay).map_or(0, |v| v.len())
730 }
731
732 fn apply_deletion(&self, deletion: &Event, relays: &[String]) {
737 let mut id_targets: HashSet<String> = HashSet::new();
738 let mut coord_targets: HashSet<String> = HashSet::new();
739 for t in deletion.tags.iter() {
740 let s = t.as_slice();
741 if s.len() >= 2 && s[0] == "e" {
742 id_targets.insert(s[1].clone());
743 } else if s.len() >= 2 && s[0] == "a" {
744 coord_targets.insert(s[1].clone());
745 }
746 }
747 let mut map = self.per_relay.lock().unwrap();
748 for r in relays {
749 if let Some(events) = map.get_mut(r) {
750 events.retain(|e| {
751 if e.pubkey != deletion.pubkey {
752 return true;
753 }
754 if id_targets.contains(&e.id.to_hex()) {
755 return false;
756 }
757 let d = e.tags.iter().find_map(|t| {
759 let s = t.as_slice();
760 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
761 }).unwrap_or_default();
762 let coord = format!("{}:{}:{}", e.kind.as_u16(), e.pubkey.to_hex(), d);
763 !coord_targets.contains(&coord)
764 });
765 }
766 }
767 }
768 }
769
770 #[async_trait::async_trait]
771 impl Transport for MemoryRelay {
772 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
773 if event.kind == Kind::EventDeletion {
775 self.apply_deletion(event, relays);
776 self.deliver(event);
777 } else {
778 self.inject(event, relays);
779 }
780 Ok(())
781 }
782
783 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
786 self.publish(event, relays).await
787 }
788
789 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
790 let map = self.per_relay.lock().unwrap();
791 let mut seen = HashSet::new();
792 let mut out = Vec::new();
793 for r in relays {
794 if let Some(events) = map.get(r) {
795 for ev in events {
796 if is_ephemeral(ev.kind.as_u16()) {
799 continue;
800 }
801 if query.matches(ev) && seen.insert(ev.id) {
802 out.push(ev.clone());
803 }
804 }
805 }
806 }
807 if let Some(limit) = query.limit {
810 out.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id)));
811 out.truncate(limit);
812 }
813 Ok(out)
814 }
815 }
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821
822 #[tokio::test]
827 async fn tor_gate_passes_immediately_when_not_blocked() {
828 let start = std::time::Instant::now();
829 let res = wait_until_tor_ready(|| false, std::time::Duration::from_secs(30)).await;
830 assert!(res.is_ok());
831 assert!(start.elapsed() < std::time::Duration::from_secs(1), "must not wait when Tor is ready");
832 }
833
834 #[tokio::test]
835 async fn tor_gate_errors_honestly_after_timeout_when_perpetually_blocked() {
836 let res = wait_until_tor_ready(|| true, std::time::Duration::from_millis(300)).await;
839 let err = res.expect_err("should error when Tor never activates");
840 assert!(err.to_lowercase().contains("tor"), "error must name Tor, got: {err}");
841 }
842
843 #[tokio::test]
844 async fn tor_gate_passes_once_circuit_comes_up_mid_wait() {
845 let calls = std::sync::atomic::AtomicUsize::new(0);
846 let res = wait_until_tor_ready(
848 || calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 3,
849 std::time::Duration::from_secs(5),
850 ).await;
851 assert!(res.is_ok(), "should succeed once Tor activates within the window");
852 }
853
854 fn evt(kind: u16, z: &str) -> Event {
855 EventBuilder::new(Kind::Custom(kind), "x")
856 .tags([Tag::custom(
857 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
858 [z.to_string()],
859 )])
860 .sign_with_keys(&Keys::generate())
861 .unwrap()
862 }
863
864 #[test]
865 fn query_matches_kind_and_z() {
866 let e = evt(3300, "abc");
867 assert!(Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: None, ..Default::default() }.matches(&e));
868 assert!(!Query { kinds: vec![3301], ..Default::default() }.matches(&e));
869 assert!(!Query { kinds: vec![], z_tags: vec!["xyz".into()], since: None, ..Default::default() }.matches(&e));
870 assert!(Query::default().matches(&e), "empty query matches anything");
871 }
872
873 fn evt_at(kind: u16, secs: u64) -> Event {
874 EventBuilder::new(Kind::Custom(kind), "x")
875 .custom_created_at(Timestamp::from(secs))
876 .sign_with_keys(&Keys::generate())
877 .unwrap()
878 }
879
880 fn evt_z_at(kind: u16, z: &str, secs: u64) -> Event {
883 EventBuilder::new(Kind::Custom(kind), "x")
884 .custom_created_at(Timestamp::from(secs))
885 .tags([Tag::custom(
886 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
887 [z.to_string()],
888 )])
889 .sign_with_keys(&Keys::generate())
890 .unwrap()
891 }
892
893 #[tokio::test]
894 async fn fetch_pages_with_until_and_limit_newest_first() {
895 let relay = super::memory::MemoryRelay::new();
897 let relays = vec!["r1".to_string()];
898 for s in 1..=5u64 {
899 relay.inject(&evt_z_at(3300, "pg", s), &relays);
900 }
901 let secs = |evs: &[Event]| evs.iter().map(|e| e.created_at.as_secs()).collect::<Vec<_>>();
902
903 let latest = relay
905 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], limit: Some(2), ..Default::default() }, &relays)
906 .await
907 .unwrap();
908 assert_eq!(secs(&latest), vec![5, 4]);
909
910 let older = relay
912 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(3), limit: Some(2), ..Default::default() }, &relays)
913 .await
914 .unwrap();
915 assert_eq!(secs(&older), vec![3, 2]);
916
917 let start = relay
920 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(1), limit: Some(2), ..Default::default() }, &relays)
921 .await
922 .unwrap();
923 assert_eq!(secs(&start), vec![1]);
924 }
925
926 #[test]
927 fn to_filter_translates_kinds_z_and_since() {
928 let q = Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: Some(100), ..Default::default() };
931 let filter = q.to_filter();
932 let matching = EventBuilder::new(Kind::Custom(3300), "x")
933 .custom_created_at(Timestamp::from(150))
934 .tags([Tag::custom(
935 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
936 ["abc".to_string()],
937 )])
938 .sign_with_keys(&Keys::generate())
939 .unwrap();
940 assert!(filter.match_event(&matching, MatchEventOptions::new()), "to_filter must accept what matches() accepts");
941 assert!(q.matches(&matching));
942
943 let wrong_kind = EventBuilder::new(Kind::Custom(3301), "x")
945 .custom_created_at(Timestamp::from(150))
946 .tags([Tag::custom(
947 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
948 ["abc".to_string()],
949 )])
950 .sign_with_keys(&Keys::generate())
951 .unwrap();
952 assert!(!filter.match_event(&wrong_kind, MatchEventOptions::new()));
953 }
954
955 fn evt_sl(kind: u16, letter: Alphabet, value: &str) -> Event {
957 EventBuilder::new(Kind::Custom(kind), "x")
958 .tags([Tag::custom(
959 TagKind::SingleLetter(SingleLetterTag::lowercase(letter)),
960 [value.to_string()],
961 )])
962 .sign_with_keys(&Keys::generate())
963 .unwrap()
964 }
965
966 #[test]
967 fn to_filter_and_matches_agree_on_authors() {
968 let keys = Keys::generate();
969 let e = EventBuilder::new(Kind::Custom(1059), "x").sign_with_keys(&keys).unwrap();
970 let q = Query { kinds: vec![1059], authors: vec![keys.public_key().to_hex()], ..Default::default() };
971 assert!(q.matches(&e));
972 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
973 let miss = Query { kinds: vec![1059], authors: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
974 assert!(!miss.matches(&e));
975 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
976 }
977
978 #[test]
979 fn to_filter_and_matches_agree_on_p_tags() {
980 let recipient = Keys::generate().public_key().to_hex();
981 let e = evt_sl(1059, Alphabet::P, &recipient);
982 let q = Query { kinds: vec![1059], p_tags: vec![recipient], ..Default::default() };
983 assert!(q.matches(&e));
984 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
985 let miss = Query { kinds: vec![1059], p_tags: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
986 assert!(!miss.matches(&e));
987 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
988 }
989
990 #[test]
991 fn to_filter_and_matches_agree_on_k_tags() {
992 let e = evt_sl(1059, Alphabet::K, "3311");
993 let q = Query { kinds: vec![1059], k_tags: vec!["3311".into()], ..Default::default() };
994 assert!(q.matches(&e));
995 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
996 let miss = Query { kinds: vec![1059], k_tags: vec!["3300".into()], ..Default::default() };
997 assert!(!miss.matches(&e));
998 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
999 }
1000
1001 #[test]
1002 fn to_filter_empty_kinds_only_constrains_z_and_since() {
1003 let q = Query { kinds: vec![], z_tags: vec!["p".into()], since: None, ..Default::default() };
1005 let filter = q.to_filter();
1006 let e = evt(3300, "p");
1007 assert!(filter.match_event(&e, MatchEventOptions::new()));
1008 assert!(q.matches(&e));
1009 }
1010
1011 #[test]
1012 fn since_is_an_inclusive_lower_bound() {
1013 let below = evt_at(3300, 99);
1014 let exact = evt_at(3300, 100);
1015 let above = evt_at(3300, 101);
1016 let q = Query { kinds: vec![3300], z_tags: vec![], since: Some(100), ..Default::default() };
1017 assert!(!q.matches(&below), "below the floor is excluded");
1018 assert!(q.matches(&exact), "exactly the floor is included");
1019 assert!(q.matches(&above), "above the floor is included");
1020 }
1021
1022 #[test]
1023 fn z_tags_match_as_or_set() {
1024 let e = evt(3300, "p2");
1027 let q = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p2".into()], since: None, ..Default::default() };
1028 assert!(q.matches(&e));
1029 let miss = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p3".into()], since: None, ..Default::default() };
1030 assert!(!miss.matches(&e));
1031 }
1032
1033 #[tokio::test]
1034 async fn fetch_unions_and_dedups_across_relays() {
1035 use super::memory::MemoryRelay;
1036 let relay = MemoryRelay::new();
1037 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1038 let e = evt(3300, "p");
1039 relay.publish(&e, &relays).await.unwrap();
1040 let got = relay
1041 .fetch(&Query { kinds: vec![3300], z_tags: vec!["p".into()], since: None, ..Default::default() }, &relays)
1042 .await
1043 .unwrap();
1044 assert_eq!(got.len(), 1, "same event on 3 relays dedups to 1");
1045 }
1046
1047 #[tokio::test]
1048 async fn durable_broadcast_retries_only_the_failing_relays_until_they_ack() {
1049 use std::cell::Cell;
1052 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1053 let round = Cell::new(0usize);
1054 let r2_round_seen = Cell::new(0usize);
1055 let res = durable_broadcast(&relays, 30, std::time::Duration::ZERO, |pending| {
1056 let n = round.get();
1057 round.set(n + 1);
1058 if n >= 1 {
1060 assert_eq!(pending, vec!["r2".to_string()], "only the failing relay is retried");
1061 r2_round_seen.set(r2_round_seen.get() + 1);
1062 }
1063 Box::pin(async move {
1064 pending.into_iter().filter(|r| r != "r2" || n >= 4).collect()
1065 })
1066 })
1067 .await;
1068 assert!(res.is_ok(), "all relays eventually ACK → Ok");
1069 assert!(round.get() >= 5, "kept retrying r2 across rounds");
1070 }
1071
1072 #[tokio::test]
1073 async fn durable_broadcast_is_ok_if_some_ack_even_when_one_never_does() {
1074 let relays = vec!["r1".to_string(), "r2".to_string()];
1077 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |pending| {
1078 Box::pin(async move { pending.into_iter().filter(|r| r == "r1").collect() })
1079 })
1080 .await;
1081 assert!(res.is_ok(), "≥1 relay accepted → Ok despite a permanently-failing relay");
1082 }
1083
1084 #[tokio::test]
1085 async fn durable_broadcast_errs_only_if_zero_relays_ever_accept() {
1086 let relays = vec!["r1".to_string(), "r2".to_string()];
1087 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |_pending| {
1088 Box::pin(async move { Vec::new() }) })
1090 .await;
1091 assert!(res.is_err(), "zero acceptances after the retry cap → Err");
1092 }
1093
1094 #[tokio::test]
1095 async fn redundancy_self_heals_a_missing_relay() {
1096 use super::memory::MemoryRelay;
1097 let relay = MemoryRelay::new();
1098 let all = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1099 let e = evt(3300, "p");
1100 relay.inject(&e, &["r2".to_string()]);
1102 assert_eq!(relay.count_on("r1"), 0);
1103 assert_eq!(relay.count_on("r2"), 1);
1104 let got = relay.fetch(&Query { kinds: vec![3300], ..Default::default() }, &all).await.unwrap();
1106 assert_eq!(got.len(), 1);
1107 }
1108
1109 #[tokio::test]
1110 async fn ephemeral_kind_streams_live_but_is_never_stored_or_fetched() {
1111 use super::memory::MemoryRelay;
1112 let relay = MemoryRelay::new();
1113 let relays = vec!["r1".to_string()];
1114 let mut sub = relay.subscribe(Query { kinds: vec![21059], ..Default::default() });
1115 let e = evt(21059, "p");
1116 relay.publish(&e, &relays).await.unwrap();
1117 assert_eq!(relay.count_on("r1"), 0, "ephemeral is never stored");
1118 let got = relay
1119 .fetch(&Query { kinds: vec![21059], ..Default::default() }, &relays)
1120 .await
1121 .unwrap();
1122 assert!(got.is_empty(), "a real relay never serves an ephemeral from a fetch");
1123 assert_eq!(sub.try_recv().unwrap().id, e.id, "but a live subscriber receives it");
1124 }
1125
1126 #[tokio::test]
1127 async fn stored_kind_is_fetchable_and_delivered_live() {
1128 use super::memory::MemoryRelay;
1129 let relay = MemoryRelay::new();
1130 let relays = vec!["r1".to_string()];
1131 let mut sub = relay.subscribe(Query { kinds: vec![1059], ..Default::default() });
1132 let e = evt(1059, "p");
1133 relay.publish(&e, &relays).await.unwrap();
1134 let got = relay
1135 .fetch(&Query { kinds: vec![1059], ..Default::default() }, &relays)
1136 .await
1137 .unwrap();
1138 assert_eq!(got.len(), 1, "stored kind is fetchable");
1139 assert_eq!(sub.try_recv().unwrap().id, e.id, "and delivered to the live subscriber");
1140 }
1141
1142 #[tokio::test]
1143 async fn p_tags_route_a_giftwrap_to_the_matching_subscriber() {
1144 use super::memory::MemoryRelay;
1145 let relay = MemoryRelay::new();
1146 let relays = vec!["r1".to_string()];
1147 let alice = Keys::generate().public_key().to_hex();
1148 let bob = Keys::generate().public_key().to_hex();
1149 let mut sub_alice =
1150 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![alice.clone()], ..Default::default() });
1151 let mut sub_bob =
1152 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![bob.clone()], ..Default::default() });
1153 let wrap = evt_sl(1059, Alphabet::P, &alice);
1154 relay.publish(&wrap, &relays).await.unwrap();
1155 assert_eq!(sub_alice.try_recv().unwrap().id, wrap.id, "addressed recipient gets it live");
1156 assert!(sub_bob.try_recv().is_err(), "a differently-addressed subscriber does not");
1157 let for_alice = relay
1159 .fetch(&Query { kinds: vec![1059], p_tags: vec![alice], ..Default::default() }, &relays)
1160 .await
1161 .unwrap();
1162 assert_eq!(for_alice.len(), 1);
1163 let for_bob = relay
1164 .fetch(&Query { kinds: vec![1059], p_tags: vec![bob], ..Default::default() }, &relays)
1165 .await
1166 .unwrap();
1167 assert!(for_bob.is_empty());
1168 }
1169}