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
248pub async fn prune_unneeded_community_relays(candidates: &[String]) {
256 if candidates.is_empty() {
257 return;
258 }
259 let Some(client) = crate::state::nostr_client() else { return };
260
261 let mut still_needed: std::collections::HashSet<String> = std::collections::HashSet::new();
262 if let Ok(ids) = crate::db::community::list_community_ids() {
263 for id in ids {
264 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
265 for r in &c.relays {
266 still_needed.insert(r.clone());
267 }
268 }
269 }
270 }
271
272 let pool = client.pool();
273 let pooled = pool.all_relays().await;
275 for url in candidates {
276 if still_needed.contains(url) {
277 continue;
278 }
279 if let Ok(parsed) = nostr_sdk::RelayUrl::parse(url) {
280 if let Some(relay) = pooled.get(&parsed) {
281 if relay.flags().has_read() || relay.flags().has_write() {
282 continue; }
284 }
285 let _ = pool.force_remove_relay(parsed).await; forget_warmed_relay(url);
287 }
288 }
289}
290
291pub struct LiveTransport {
298 timeout: std::time::Duration,
299}
300
301impl Default for LiveTransport {
302 fn default() -> Self {
303 Self { timeout: std::time::Duration::from_secs(10) }
304 }
305}
306
307impl LiveTransport {
308 pub fn new() -> Self {
309 Self::default()
310 }
311
312 pub fn with_timeout(timeout: std::time::Duration) -> Self {
313 Self { timeout }
314 }
315
316 async fn warm_client(relays: &[String], connect_timeout: std::time::Duration) -> Result<Client, String> {
322 if relays.is_empty() {
323 return Err("community has no relays configured".to_string());
324 }
325 let client = crate::state::nostr_client().ok_or_else(|| "nostr client not initialized".to_string())?;
326
327 let generation = crate::state::current_session_generation();
331 {
332 let warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
333 if warmed.0 == generation && relays.iter().all(|r| warmed.1.contains(r)) {
334 return Ok(client);
335 }
336 }
337
338 let mut added_new = false;
343 let mut succeeded: Vec<&String> = Vec::new();
344 for url in relays {
345 let opts = crate::community_relay_options();
346 match client.pool().add_relay(url.as_str(), opts).await {
347 Ok(true) => { added_new = true; succeeded.push(url); }
348 Ok(false) => { succeeded.push(url); }
349 Err(_) => {}
350 }
351 }
352 if succeeded.is_empty() {
353 return Err("no valid community relays could be added".to_string());
354 }
355 if added_new {
356 let _ = client.try_connect(connect_timeout).await;
361 } else {
362 client.connect().await;
364 }
365
366 {
369 let mut warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
370 if warmed.0 != generation {
371 warmed.0 = generation;
372 warmed.1.clear();
373 }
374 for url in succeeded {
375 warmed.1.insert(url.clone());
376 }
377 }
378 Ok(client)
379 }
380}
381
382#[async_trait::async_trait]
383impl Transport for LiveTransport {
384 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
385 let client = Self::warm_client(relays, self.timeout).await?;
386 let timeout = self.timeout;
387 let mut targets: Vec<String> = Vec::new();
388 for r in relays { if !targets.contains(r) { targets.push(r.clone()); } }
389 use futures_util::stream::{FuturesUnordered, StreamExt};
395 let mut sends: FuturesUnordered<_> = targets
396 .into_iter()
397 .map(|r| {
398 let client = client.clone();
399 let event = event.clone();
400 tokio::spawn(async move {
401 matches!(
402 tokio::time::timeout(timeout, client.send_event_to(vec![r.clone()], &event)).await,
403 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains(&u)).unwrap_or(false)
404 )
405 })
406 })
407 .collect();
408 while let Some(joined) = sends.next().await {
409 if matches!(joined, Ok(true)) {
410 return Ok(()); }
412 }
413 Err("no relay accepted the event".to_string())
414 }
415
416 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
417 let client = Self::warm_client(relays, self.timeout).await?;
418 let timeout = self.timeout;
419 let filter = query.to_filter();
420
421 let mut targets: Vec<String> = Vec::new();
422 for r in relays {
423 if !targets.contains(r) {
424 targets.push(r.clone());
425 }
426 }
427
428 if targets.len() <= 1 {
430 return client
431 .fetch_events_from(targets, filter, timeout)
432 .await
433 .map(|evs| evs.into_iter().collect())
434 .map_err(|e| e.to_string());
435 }
436
437 use futures_util::stream::{FuturesUnordered, StreamExt};
441 let mut fetches: FuturesUnordered<_> = targets
442 .into_iter()
443 .map(|r| {
444 let client = client.clone();
445 let filter = filter.clone();
446 tokio::spawn(async move {
447 client
448 .fetch_events_from(vec![r], filter, timeout)
449 .await
450 .map(|evs| evs.into_iter().collect::<Vec<Event>>())
451 .unwrap_or_default()
452 })
453 })
454 .collect();
455
456 let mut result: Vec<Event> = Vec::new();
465 loop {
466 match fetches.next().await {
467 Some(Ok(evs)) => {
468 result = evs;
469 break;
470 }
471 Some(Err(_)) => continue, None => break, }
474 }
475 let mut union_ids: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
476 if query.until.is_some() {
477 while let Some(joined) = fetches.next().await {
478 if let Ok(evs) = joined {
479 for e in evs {
480 if union_ids.insert(e.id) {
481 result.push(e);
482 }
483 }
484 }
485 }
486 } else if !fetches.is_empty() {
487 let grace = tokio::time::sleep(std::time::Duration::from_millis(FETCH_UNION_GRACE_MS));
488 tokio::pin!(grace);
489 loop {
490 tokio::select! {
491 _ = &mut grace => break,
492 next = fetches.next() => match next {
493 Some(Ok(evs)) => {
494 for e in evs {
495 if union_ids.insert(e.id) {
496 result.push(e);
497 }
498 }
499 }
500 Some(Err(_)) => continue,
501 None => break,
502 }
503 }
504 }
505 }
506
507 if !fetches.is_empty() {
511 let seen: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
512 let session = crate::state::SessionGuard::capture();
515 tokio::spawn(async move {
516 let mut extra: Vec<Event> = Vec::new();
517 let mut extra_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
518 while let Some(joined) = fetches.next().await {
519 if let Ok(evs) = joined {
520 for e in evs {
521 if !seen.contains(&e.id) && extra_ids.insert(e.id) {
522 extra.push(e);
523 }
524 }
525 }
526 }
527 if !session.is_valid() {
528 return;
529 }
530 submit_stragglers(extra);
531 });
532 }
533
534 Ok(result)
535 }
536
537 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
538 let client = Self::warm_client(relays, self.timeout).await?;
543 let timeout = self.timeout;
544 let event = event.clone();
545 let backoff = std::time::Duration::from_millis(750);
546 let mut pending: Vec<String> = Vec::new();
547 for r in relays { if !pending.contains(r) { pending.push(r.clone()); } }
548 if pending.is_empty() {
549 return Err("no relays to broadcast to".to_string());
550 }
551
552 let mut acked_any = false;
557 let _ = tokio::time::timeout(CONFIRM_WINDOW, async {
558 loop {
559 let sends = pending.iter().cloned().map(|r| {
560 let client = &client;
561 let event = &event;
562 Box::pin(async move {
563 match tokio::time::timeout(timeout, client.send_event_to(vec![r.clone()], event)).await {
564 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains(&u)).unwrap_or(false) => Ok(r),
565 _ => Err(()),
566 }
567 })
568 });
569 if let Ok((winner, _losers)) = futures_util::future::select_ok(sends).await {
570 acked_any = true;
571 pending.retain(|r| r != &winner);
572 break;
573 }
574 tokio::time::sleep(backoff).await;
575 }
576 })
577 .await;
578
579 if !acked_any {
580 return Err(format!("no relay accepted the event within {}s", CONFIRM_WINDOW.as_secs()));
581 }
582 if pending.is_empty() {
583 return Ok(()); }
585
586 tokio::spawn(async move {
591 let client_ref = &client;
592 let event_ref = &event;
593 let _ = durable_broadcast(&pending, MAX_PUBLISH_ATTEMPTS, backoff, move |round| {
594 Box::pin(async move {
595 match tokio::time::timeout(timeout, client_ref.send_event_to(round.clone(), event_ref)).await {
596 Ok(Ok(output)) => round.into_iter().filter(|p| RelayUrl::parse(p).map(|u| output.success.contains(&u)).unwrap_or(false)).collect(),
597 _ => Vec::new(),
598 }
599 })
600 })
601 .await;
602 });
603 Ok(())
604 }
605}
606
607#[cfg(test)]
608pub(crate) mod memory {
609 use super::*;
610 use std::collections::{HashMap, HashSet};
611 use std::sync::Mutex;
612
613 pub struct MemoryRelay {
618 per_relay: Mutex<HashMap<String, Vec<Event>>>,
619 subscribers: Mutex<Vec<(Query, tokio::sync::mpsc::UnboundedSender<Event>)>>,
620 }
621
622 fn is_ephemeral(kind: u16) -> bool {
625 (20000..30000).contains(&kind)
626 }
627
628 impl MemoryRelay {
629 pub fn new() -> Self {
630 MemoryRelay {
631 per_relay: Mutex::new(HashMap::new()),
632 subscribers: Mutex::new(Vec::new()),
633 }
634 }
635
636 pub fn subscribe(&self, query: Query) -> tokio::sync::mpsc::UnboundedReceiver<Event> {
639 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
640 self.subscribers.lock().unwrap().push((query, tx));
641 rx
642 }
643
644 fn deliver(&self, event: &Event) {
646 self.subscribers.lock().unwrap().retain(|(q, tx)| {
647 if q.matches(event) {
648 tx.send(event.clone()).is_ok()
649 } else {
650 !tx.is_closed()
651 }
652 });
653 }
654
655 pub fn inject(&self, event: &Event, relays: &[String]) {
658 self.deliver(event);
659 if is_ephemeral(event.kind.as_u16()) {
660 return; }
662 let d_tag = |e: &Event| e.tags.iter().find_map(|t| {
668 let s = t.as_slice();
669 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
670 }).unwrap_or_default();
671 let k = event.kind.as_u16();
672 let replaceable = (30000..40000).contains(&k) || (10000..20000).contains(&k) || k == 0 || k == 3;
673 let coord = (event.kind.as_u16(), event.pubkey, d_tag(event));
674 let mut map = self.per_relay.lock().unwrap();
675 for r in relays {
676 let v = map.entry(r.clone()).or_default();
677 if replaceable {
678 v.retain(|e| (e.kind.as_u16(), e.pubkey, d_tag(e)) != coord);
679 }
680 v.push(event.clone());
681 }
682 }
683
684 pub fn count_on(&self, relay: &str) -> usize {
686 self.per_relay.lock().unwrap().get(relay).map_or(0, |v| v.len())
687 }
688
689 fn apply_deletion(&self, deletion: &Event, relays: &[String]) {
694 let mut id_targets: HashSet<String> = HashSet::new();
695 let mut coord_targets: HashSet<String> = HashSet::new();
696 for t in deletion.tags.iter() {
697 let s = t.as_slice();
698 if s.len() >= 2 && s[0] == "e" {
699 id_targets.insert(s[1].clone());
700 } else if s.len() >= 2 && s[0] == "a" {
701 coord_targets.insert(s[1].clone());
702 }
703 }
704 let mut map = self.per_relay.lock().unwrap();
705 for r in relays {
706 if let Some(events) = map.get_mut(r) {
707 events.retain(|e| {
708 if e.pubkey != deletion.pubkey {
709 return true;
710 }
711 if id_targets.contains(&e.id.to_hex()) {
712 return false;
713 }
714 let d = e.tags.iter().find_map(|t| {
716 let s = t.as_slice();
717 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
718 }).unwrap_or_default();
719 let coord = format!("{}:{}:{}", e.kind.as_u16(), e.pubkey.to_hex(), d);
720 !coord_targets.contains(&coord)
721 });
722 }
723 }
724 }
725 }
726
727 #[async_trait::async_trait]
728 impl Transport for MemoryRelay {
729 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
730 if event.kind == Kind::EventDeletion {
732 self.apply_deletion(event, relays);
733 self.deliver(event);
734 } else {
735 self.inject(event, relays);
736 }
737 Ok(())
738 }
739
740 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
743 self.publish(event, relays).await
744 }
745
746 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
747 let map = self.per_relay.lock().unwrap();
748 let mut seen = HashSet::new();
749 let mut out = Vec::new();
750 for r in relays {
751 if let Some(events) = map.get(r) {
752 for ev in events {
753 if is_ephemeral(ev.kind.as_u16()) {
756 continue;
757 }
758 if query.matches(ev) && seen.insert(ev.id) {
759 out.push(ev.clone());
760 }
761 }
762 }
763 }
764 if let Some(limit) = query.limit {
767 out.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id)));
768 out.truncate(limit);
769 }
770 Ok(out)
771 }
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778
779 fn evt(kind: u16, z: &str) -> Event {
780 EventBuilder::new(Kind::Custom(kind), "x")
781 .tags([Tag::custom(
782 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
783 [z.to_string()],
784 )])
785 .sign_with_keys(&Keys::generate())
786 .unwrap()
787 }
788
789 #[test]
790 fn query_matches_kind_and_z() {
791 let e = evt(3300, "abc");
792 assert!(Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: None, ..Default::default() }.matches(&e));
793 assert!(!Query { kinds: vec![3301], ..Default::default() }.matches(&e));
794 assert!(!Query { kinds: vec![], z_tags: vec!["xyz".into()], since: None, ..Default::default() }.matches(&e));
795 assert!(Query::default().matches(&e), "empty query matches anything");
796 }
797
798 fn evt_at(kind: u16, secs: u64) -> Event {
799 EventBuilder::new(Kind::Custom(kind), "x")
800 .custom_created_at(Timestamp::from(secs))
801 .sign_with_keys(&Keys::generate())
802 .unwrap()
803 }
804
805 fn evt_z_at(kind: u16, z: &str, secs: u64) -> Event {
808 EventBuilder::new(Kind::Custom(kind), "x")
809 .custom_created_at(Timestamp::from(secs))
810 .tags([Tag::custom(
811 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
812 [z.to_string()],
813 )])
814 .sign_with_keys(&Keys::generate())
815 .unwrap()
816 }
817
818 #[tokio::test]
819 async fn fetch_pages_with_until_and_limit_newest_first() {
820 let relay = super::memory::MemoryRelay::new();
822 let relays = vec!["r1".to_string()];
823 for s in 1..=5u64 {
824 relay.inject(&evt_z_at(3300, "pg", s), &relays);
825 }
826 let secs = |evs: &[Event]| evs.iter().map(|e| e.created_at.as_secs()).collect::<Vec<_>>();
827
828 let latest = relay
830 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], limit: Some(2), ..Default::default() }, &relays)
831 .await
832 .unwrap();
833 assert_eq!(secs(&latest), vec![5, 4]);
834
835 let older = relay
837 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(3), limit: Some(2), ..Default::default() }, &relays)
838 .await
839 .unwrap();
840 assert_eq!(secs(&older), vec![3, 2]);
841
842 let start = relay
845 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(1), limit: Some(2), ..Default::default() }, &relays)
846 .await
847 .unwrap();
848 assert_eq!(secs(&start), vec![1]);
849 }
850
851 #[test]
852 fn to_filter_translates_kinds_z_and_since() {
853 let q = Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: Some(100), ..Default::default() };
856 let filter = q.to_filter();
857 let matching = EventBuilder::new(Kind::Custom(3300), "x")
858 .custom_created_at(Timestamp::from(150))
859 .tags([Tag::custom(
860 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
861 ["abc".to_string()],
862 )])
863 .sign_with_keys(&Keys::generate())
864 .unwrap();
865 assert!(filter.match_event(&matching, MatchEventOptions::new()), "to_filter must accept what matches() accepts");
866 assert!(q.matches(&matching));
867
868 let wrong_kind = EventBuilder::new(Kind::Custom(3301), "x")
870 .custom_created_at(Timestamp::from(150))
871 .tags([Tag::custom(
872 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
873 ["abc".to_string()],
874 )])
875 .sign_with_keys(&Keys::generate())
876 .unwrap();
877 assert!(!filter.match_event(&wrong_kind, MatchEventOptions::new()));
878 }
879
880 fn evt_sl(kind: u16, letter: Alphabet, value: &str) -> Event {
882 EventBuilder::new(Kind::Custom(kind), "x")
883 .tags([Tag::custom(
884 TagKind::SingleLetter(SingleLetterTag::lowercase(letter)),
885 [value.to_string()],
886 )])
887 .sign_with_keys(&Keys::generate())
888 .unwrap()
889 }
890
891 #[test]
892 fn to_filter_and_matches_agree_on_authors() {
893 let keys = Keys::generate();
894 let e = EventBuilder::new(Kind::Custom(1059), "x").sign_with_keys(&keys).unwrap();
895 let q = Query { kinds: vec![1059], authors: vec![keys.public_key().to_hex()], ..Default::default() };
896 assert!(q.matches(&e));
897 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
898 let miss = Query { kinds: vec![1059], authors: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
899 assert!(!miss.matches(&e));
900 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
901 }
902
903 #[test]
904 fn to_filter_and_matches_agree_on_p_tags() {
905 let recipient = Keys::generate().public_key().to_hex();
906 let e = evt_sl(1059, Alphabet::P, &recipient);
907 let q = Query { kinds: vec![1059], p_tags: vec![recipient], ..Default::default() };
908 assert!(q.matches(&e));
909 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
910 let miss = Query { kinds: vec![1059], p_tags: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
911 assert!(!miss.matches(&e));
912 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
913 }
914
915 #[test]
916 fn to_filter_and_matches_agree_on_k_tags() {
917 let e = evt_sl(1059, Alphabet::K, "3311");
918 let q = Query { kinds: vec![1059], k_tags: vec!["3311".into()], ..Default::default() };
919 assert!(q.matches(&e));
920 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
921 let miss = Query { kinds: vec![1059], k_tags: vec!["3300".into()], ..Default::default() };
922 assert!(!miss.matches(&e));
923 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
924 }
925
926 #[test]
927 fn to_filter_empty_kinds_only_constrains_z_and_since() {
928 let q = Query { kinds: vec![], z_tags: vec!["p".into()], since: None, ..Default::default() };
930 let filter = q.to_filter();
931 let e = evt(3300, "p");
932 assert!(filter.match_event(&e, MatchEventOptions::new()));
933 assert!(q.matches(&e));
934 }
935
936 #[test]
937 fn since_is_an_inclusive_lower_bound() {
938 let below = evt_at(3300, 99);
939 let exact = evt_at(3300, 100);
940 let above = evt_at(3300, 101);
941 let q = Query { kinds: vec![3300], z_tags: vec![], since: Some(100), ..Default::default() };
942 assert!(!q.matches(&below), "below the floor is excluded");
943 assert!(q.matches(&exact), "exactly the floor is included");
944 assert!(q.matches(&above), "above the floor is included");
945 }
946
947 #[test]
948 fn z_tags_match_as_or_set() {
949 let e = evt(3300, "p2");
952 let q = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p2".into()], since: None, ..Default::default() };
953 assert!(q.matches(&e));
954 let miss = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p3".into()], since: None, ..Default::default() };
955 assert!(!miss.matches(&e));
956 }
957
958 #[tokio::test]
959 async fn fetch_unions_and_dedups_across_relays() {
960 use super::memory::MemoryRelay;
961 let relay = MemoryRelay::new();
962 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
963 let e = evt(3300, "p");
964 relay.publish(&e, &relays).await.unwrap();
965 let got = relay
966 .fetch(&Query { kinds: vec![3300], z_tags: vec!["p".into()], since: None, ..Default::default() }, &relays)
967 .await
968 .unwrap();
969 assert_eq!(got.len(), 1, "same event on 3 relays dedups to 1");
970 }
971
972 #[tokio::test]
973 async fn durable_broadcast_retries_only_the_failing_relays_until_they_ack() {
974 use std::cell::Cell;
977 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
978 let round = Cell::new(0usize);
979 let r2_round_seen = Cell::new(0usize);
980 let res = durable_broadcast(&relays, 30, std::time::Duration::ZERO, |pending| {
981 let n = round.get();
982 round.set(n + 1);
983 if n >= 1 {
985 assert_eq!(pending, vec!["r2".to_string()], "only the failing relay is retried");
986 r2_round_seen.set(r2_round_seen.get() + 1);
987 }
988 Box::pin(async move {
989 pending.into_iter().filter(|r| r != "r2" || n >= 4).collect()
990 })
991 })
992 .await;
993 assert!(res.is_ok(), "all relays eventually ACK → Ok");
994 assert!(round.get() >= 5, "kept retrying r2 across rounds");
995 }
996
997 #[tokio::test]
998 async fn durable_broadcast_is_ok_if_some_ack_even_when_one_never_does() {
999 let relays = vec!["r1".to_string(), "r2".to_string()];
1002 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |pending| {
1003 Box::pin(async move { pending.into_iter().filter(|r| r == "r1").collect() })
1004 })
1005 .await;
1006 assert!(res.is_ok(), "≥1 relay accepted → Ok despite a permanently-failing relay");
1007 }
1008
1009 #[tokio::test]
1010 async fn durable_broadcast_errs_only_if_zero_relays_ever_accept() {
1011 let relays = vec!["r1".to_string(), "r2".to_string()];
1012 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |_pending| {
1013 Box::pin(async move { Vec::new() }) })
1015 .await;
1016 assert!(res.is_err(), "zero acceptances after the retry cap → Err");
1017 }
1018
1019 #[tokio::test]
1020 async fn redundancy_self_heals_a_missing_relay() {
1021 use super::memory::MemoryRelay;
1022 let relay = MemoryRelay::new();
1023 let all = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1024 let e = evt(3300, "p");
1025 relay.inject(&e, &["r2".to_string()]);
1027 assert_eq!(relay.count_on("r1"), 0);
1028 assert_eq!(relay.count_on("r2"), 1);
1029 let got = relay.fetch(&Query { kinds: vec![3300], ..Default::default() }, &all).await.unwrap();
1031 assert_eq!(got.len(), 1);
1032 }
1033
1034 #[tokio::test]
1035 async fn ephemeral_kind_streams_live_but_is_never_stored_or_fetched() {
1036 use super::memory::MemoryRelay;
1037 let relay = MemoryRelay::new();
1038 let relays = vec!["r1".to_string()];
1039 let mut sub = relay.subscribe(Query { kinds: vec![21059], ..Default::default() });
1040 let e = evt(21059, "p");
1041 relay.publish(&e, &relays).await.unwrap();
1042 assert_eq!(relay.count_on("r1"), 0, "ephemeral is never stored");
1043 let got = relay
1044 .fetch(&Query { kinds: vec![21059], ..Default::default() }, &relays)
1045 .await
1046 .unwrap();
1047 assert!(got.is_empty(), "a real relay never serves an ephemeral from a fetch");
1048 assert_eq!(sub.try_recv().unwrap().id, e.id, "but a live subscriber receives it");
1049 }
1050
1051 #[tokio::test]
1052 async fn stored_kind_is_fetchable_and_delivered_live() {
1053 use super::memory::MemoryRelay;
1054 let relay = MemoryRelay::new();
1055 let relays = vec!["r1".to_string()];
1056 let mut sub = relay.subscribe(Query { kinds: vec![1059], ..Default::default() });
1057 let e = evt(1059, "p");
1058 relay.publish(&e, &relays).await.unwrap();
1059 let got = relay
1060 .fetch(&Query { kinds: vec![1059], ..Default::default() }, &relays)
1061 .await
1062 .unwrap();
1063 assert_eq!(got.len(), 1, "stored kind is fetchable");
1064 assert_eq!(sub.try_recv().unwrap().id, e.id, "and delivered to the live subscriber");
1065 }
1066
1067 #[tokio::test]
1068 async fn p_tags_route_a_giftwrap_to_the_matching_subscriber() {
1069 use super::memory::MemoryRelay;
1070 let relay = MemoryRelay::new();
1071 let relays = vec!["r1".to_string()];
1072 let alice = Keys::generate().public_key().to_hex();
1073 let bob = Keys::generate().public_key().to_hex();
1074 let mut sub_alice =
1075 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![alice.clone()], ..Default::default() });
1076 let mut sub_bob =
1077 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![bob.clone()], ..Default::default() });
1078 let wrap = evt_sl(1059, Alphabet::P, &alice);
1079 relay.publish(&wrap, &relays).await.unwrap();
1080 assert_eq!(sub_alice.try_recv().unwrap().id, wrap.id, "addressed recipient gets it live");
1081 assert!(sub_bob.try_recv().is_err(), "a differently-addressed subscriber does not");
1082 let for_alice = relay
1084 .fetch(&Query { kinds: vec![1059], p_tags: vec![alice], ..Default::default() }, &relays)
1085 .await
1086 .unwrap();
1087 assert_eq!(for_alice.len(), 1);
1088 let for_bob = relay
1089 .fetch(&Query { kinds: vec![1059], p_tags: vec![bob], ..Default::default() }, &relays)
1090 .await
1091 .unwrap();
1092 assert!(for_bob.is_empty());
1093 }
1094}