1use nostr_sdk::prelude::*;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub enum Evidence {
22 Fast,
26 #[default]
33 Quorum,
34 Full,
39}
40
41#[derive(Clone, Debug, Default)]
45pub struct Query {
46 pub kinds: Vec<u16>,
47 pub z_tags: Vec<String>,
49 pub d_tags: Vec<String>,
52 pub p_tags: Vec<String>,
55 pub k_tags: Vec<String>,
58 pub authors: Vec<String>,
60 pub since: Option<u64>,
62 pub until: Option<u64>,
65 pub limit: Option<usize>,
67 pub evidence: Evidence,
71}
72
73impl Query {
74 pub fn matches(&self, event: &Event) -> bool {
76 if !self.kinds.is_empty() && !self.kinds.iter().any(|k| Kind::Custom(*k) == event.kind) {
80 return false;
81 }
82 if let Some(since) = self.since {
83 if event.created_at.as_secs() < since {
84 return false;
85 }
86 }
87 if let Some(until) = self.until {
88 if event.created_at.as_secs() > until {
89 return false;
90 }
91 }
92 if !self.authors.is_empty() && !self.authors.iter().any(|a| *a == event.pubkey.to_hex()) {
93 return false;
94 }
95 if !self.z_tags.is_empty() && !self.matches_single_letter("z", &self.z_tags, event) {
96 return false;
97 }
98 if !self.d_tags.is_empty() && !self.matches_single_letter("d", &self.d_tags, event) {
99 return false;
100 }
101 if !self.p_tags.is_empty() && !self.matches_single_letter("p", &self.p_tags, event) {
102 return false;
103 }
104 if !self.k_tags.is_empty() && !self.matches_single_letter("k", &self.k_tags, event) {
105 return false;
106 }
107 true
108 }
109
110 fn matches_single_letter(&self, name: &str, wanted: &[String], event: &Event) -> bool {
111 event.tags.iter().any(|t| {
114 let s = t.as_slice();
115 s.len() >= 2 && s[0] == name && wanted.iter().any(|w| *w == s[1])
116 })
117 }
118
119 pub fn to_filter(&self) -> Filter {
121 let mut filter = Filter::new();
122 if !self.kinds.is_empty() {
123 filter = filter.kinds(self.kinds.iter().map(|k| Kind::Custom(*k)));
124 }
125 if !self.z_tags.is_empty() {
126 filter = filter
127 .custom_tags(SingleLetterTag::lowercase(Alphabet::Z), self.z_tags.clone());
128 }
129 if !self.d_tags.is_empty() {
130 filter = filter.identifiers(self.d_tags.clone());
131 }
132 if !self.p_tags.is_empty() {
133 filter = filter
134 .custom_tags(SingleLetterTag::lowercase(Alphabet::P), self.p_tags.clone());
135 }
136 if !self.k_tags.is_empty() {
137 filter = filter
138 .custom_tags(SingleLetterTag::lowercase(Alphabet::K), self.k_tags.clone());
139 }
140 if !self.authors.is_empty() {
141 let authors: Vec<PublicKey> =
142 self.authors.iter().filter_map(|a| PublicKey::from_hex(a).ok()).collect();
143 if !authors.is_empty() {
144 filter = filter.authors(authors);
145 }
146 }
147 if let Some(since) = self.since {
148 filter = filter.since(Timestamp::from_secs(since));
149 }
150 if let Some(until) = self.until {
151 filter = filter.until(Timestamp::from_secs(until));
152 }
153 if let Some(limit) = self.limit {
154 filter = filter.limit(limit);
155 }
156 filter
157 }
158}
159
160#[async_trait::async_trait]
164pub trait Transport {
165 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String>;
167 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String>;
169
170 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<Vec<Event>, String>;
179
180 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String>;
188}
189
190pub const MAX_PUBLISH_ATTEMPTS: usize = 30;
193
194pub const RESIDUAL_GRACE_MS: u64 = 400;
198
199pub const QUORUM_GRACE_MS: u64 = 2000;
205
206const BREAKER_TRIP_THRESHOLD: u8 = 2;
208
209const BREAKER_COOLDOWN: std::time::Duration = std::time::Duration::from_secs(30);
212
213const TRIPPED_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(6);
217
218pub const CONFIRM_WINDOW: std::time::Duration = std::time::Duration::from_secs(30);
222
223pub async fn durable_broadcast<'a, F>(
230 relays: &[String],
231 max_attempts: usize,
232 backoff: std::time::Duration,
233 mut send_round: F,
234) -> Result<(), String>
235where
236 F: FnMut(Vec<String>) -> std::pin::Pin<Box<dyn std::future::Future<Output = Vec<String>> + Send + 'a>>,
237{
238 let mut pending: Vec<String> = Vec::new();
239 for r in relays {
240 if !pending.contains(r) {
241 pending.push(r.clone());
242 }
243 }
244 let total = pending.len();
245 if total == 0 {
246 return Err("no relays to broadcast to".to_string());
247 }
248 for attempt in 0..max_attempts {
249 if pending.is_empty() {
250 break;
251 }
252 let acked = send_round(pending.clone()).await;
253 pending.retain(|r| !acked.contains(r));
254 if pending.is_empty() || attempt + 1 == max_attempts {
255 break;
256 }
257 if !backoff.is_zero() {
258 tokio::time::sleep(backoff).await;
259 }
260 }
261 if pending.len() < total {
262 Ok(()) } else {
264 Err(format!("no relay accepted the event after {max_attempts} attempts each"))
265 }
266}
267
268pub trait CommunityIngestSink: Send + Sync + 'static {
277 fn ingest_stragglers(&self, events: Vec<Event>);
278}
279
280static INGEST_SINK: std::sync::OnceLock<Box<dyn CommunityIngestSink>> = std::sync::OnceLock::new();
281
282pub fn set_community_ingest_sink(sink: Box<dyn CommunityIngestSink>) {
284 let _ = INGEST_SINK.set(sink);
285}
286
287fn submit_stragglers(events: Vec<Event>) {
288 if events.is_empty() {
289 return;
290 }
291 if let Some(sink) = INGEST_SINK.get() {
292 sink.ingest_stragglers(events);
293 }
294}
295
296static WARMED_RELAYS: std::sync::LazyLock<std::sync::Mutex<(u64, std::collections::HashSet<String>)>> =
302 std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashSet::new())));
303
304pub fn forget_warmed_relay(url: &str) {
309 WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner()).1.remove(url);
310}
311
312#[derive(Default)]
319struct BreakerEntry {
320 consecutive_failures: u8,
321 tripped_until: Option<std::time::Instant>,
322}
323
324static RELAY_BREAKER: std::sync::LazyLock<
325 std::sync::Mutex<(u64, std::collections::HashMap<String, BreakerEntry>)>,
326> = std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashMap::new())));
327
328fn with_breaker_at<R>(
333 generation: u64,
334 f: impl FnOnce(&mut std::collections::HashMap<String, BreakerEntry>) -> R,
335) -> R {
336 let mut guard = RELAY_BREAKER.lock().unwrap_or_else(|e| e.into_inner());
337 if guard.0 != generation {
338 guard.0 = generation;
339 guard.1.clear();
340 }
341 f(&mut guard.1)
342}
343
344fn breaker_tripped(url: &str) -> bool {
346 breaker_tripped_at(crate::state::current_session_generation(), url)
347}
348
349fn breaker_tripped_at(generation: u64, url: &str) -> bool {
350 with_breaker_at(generation, |map| {
351 map.get(url)
352 .and_then(|e| e.tripped_until)
353 .map_or(false, |t| std::time::Instant::now() < t)
354 })
355}
356
357fn breaker_record(url: &str, success: bool, full_budget: bool) {
360 breaker_record_at(crate::state::current_session_generation(), url, success, full_budget)
361}
362
363fn breaker_record_at(generation: u64, url: &str, success: bool, full_budget: bool) {
364 with_breaker_at(generation, |map| {
365 if success {
366 map.remove(url);
367 return;
368 }
369 if !full_budget {
370 return;
371 }
372 let e = map.entry(url.to_string()).or_default();
373 e.consecutive_failures = e.consecutive_failures.saturating_add(1);
374 if e.consecutive_failures >= BREAKER_TRIP_THRESHOLD {
375 e.tripped_until = Some(std::time::Instant::now() + BREAKER_COOLDOWN);
376 }
377 })
378}
379
380fn effective_evidence(query: &Query) -> Evidence {
384 if query.until.is_some() {
385 Evidence::Full
386 } else {
387 query.evidence
388 }
389}
390
391struct PooledPlane {
399 client: Client,
400 last_used: std::time::Instant,
401}
402
403static PLANE_POOL: std::sync::LazyLock<std::sync::Mutex<(u64, std::collections::HashMap<String, PooledPlane>)>> =
404 std::sync::LazyLock::new(|| std::sync::Mutex::new((0, std::collections::HashMap::new())));
405
406const PLANE_POOL_IDLE_TTL: std::time::Duration = std::time::Duration::from_secs(90);
409const PLANE_POOL_MAX: usize = 24;
411
412fn plane_pool_key(plane_pk: &str, relays: &[String]) -> String {
413 let mut rs: Vec<&str> = relays.iter().map(|s| s.as_str()).collect();
414 rs.sort_unstable();
415 let mut k = String::with_capacity(plane_pk.len() + 1 + rs.iter().map(|r| r.len() + 1).sum::<usize>());
416 k.push_str(plane_pk);
417 k.push('|');
418 k.push_str(&rs.join(","));
419 k
420}
421
422fn disconnect_clients(clients: Vec<Client>) {
427 if clients.is_empty() {
428 return;
429 }
430 match tokio::runtime::Handle::try_current() {
431 Ok(handle) => {
432 for c in clients {
433 handle.spawn(async move {
434 let _ = c.disconnect();
435 });
436 }
437 }
438 Err(_) => drop(clients),
439 }
440}
441
442pub fn clear_plane_pool() {
445 let drained: Vec<Client> = {
446 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
447 g.0 = crate::state::current_session_generation();
452 g.1.drain().map(|(_, p)| p.client).collect()
453 };
454 disconnect_clients(drained);
455}
456
457fn plane_pool_take(generation: u64, key: &str) -> (Option<Client>, Vec<Client>) {
461 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
462 let mut evicted: Vec<Client> = Vec::new();
463 if g.0 != generation {
464 evicted.extend(g.1.drain().map(|(_, p)| p.client));
465 g.0 = generation;
466 }
467 let now = std::time::Instant::now();
469 let expired: Vec<String> = g.1.iter()
470 .filter(|(_, p)| now.duration_since(p.last_used) >= PLANE_POOL_IDLE_TTL)
471 .map(|(k, _)| k.clone())
472 .collect();
473 for k in expired {
474 if let Some(p) = g.1.remove(&k) {
475 evicted.push(p.client);
476 }
477 }
478 let hit = g.1.get_mut(key).map(|p| {
479 p.last_used = now;
480 p.client.clone()
481 });
482 (hit, evicted)
483}
484
485fn plane_pool_insert(generation: u64, key: String, client: Client) -> Vec<Client> {
492 let mut g = PLANE_POOL.lock().unwrap_or_else(|e| e.into_inner());
493 if g.0 != generation || g.1.contains_key(&key) {
496 return Vec::new();
497 }
498 let mut evicted: Vec<Client> = Vec::new();
499 if g.1.len() >= PLANE_POOL_MAX {
500 if let Some(lru_key) = g.1.iter().min_by_key(|(_, p)| p.last_used).map(|(k, _)| k.clone()) {
501 if let Some(p) = g.1.remove(&lru_key) {
502 evicted.push(p.client);
503 }
504 }
505 }
506 g.1.insert(key, PooledPlane { client, last_used: std::time::Instant::now() });
507 evicted
508}
509
510fn demotion_allowed() -> bool {
514 #[cfg(feature = "tor")]
515 {
516 matches!(crate::tor::transport_state(), crate::tor::TorTransportState::Disabled)
517 }
518 #[cfg(not(feature = "tor"))]
519 {
520 true
521 }
522}
523
524pub async fn fetch_relay_eose(
535 client: &Client,
536 url: &str,
537 filter: Filter,
538 timeout: std::time::Duration,
539) -> Result<Vec<Event>, ()> {
540 let relay = client.pool().relay(url).await.map_err(|_| ())?;
541 let mut notifications = relay.notifications();
543 let sub_id = SubscriptionId::generate();
544 let auto_close = SubscribeAutoCloseOptions::default()
545 .exit_policy(ReqExitPolicy::ExitOnEOSE)
546 .timeout(Some(timeout));
547 relay
548 .subscribe_with_id(sub_id.clone(), filter, SubscribeOptions::default().close_on(Some(auto_close)))
549 .await
550 .map_err(|_| ())?;
551 let deadline = tokio::time::Instant::now() + timeout;
552 let mut events: Vec<Event> = Vec::new();
553 let mut seen: std::collections::HashSet<EventId> = std::collections::HashSet::new();
554 loop {
555 let notification = match tokio::time::timeout_at(deadline, notifications.recv()).await {
556 Ok(Ok(n)) => n,
557 Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => continue,
561 Ok(Err(tokio::sync::broadcast::error::RecvError::Closed)) => return Err(()),
562 Err(_) => return Err(()), };
564 match notification {
565 RelayNotification::Event { subscription_id, event } if subscription_id == sub_id => {
566 if seen.insert(event.id) {
567 events.push(*event);
568 }
569 }
570 RelayNotification::Message { message } => match message {
571 RelayMessage::Event { subscription_id, event } if *subscription_id == sub_id => {
572 if seen.insert(event.id) {
573 events.push(event.into_owned());
574 }
575 }
576 RelayMessage::EndOfStoredEvents(id) if *id == sub_id => return Ok(events),
577 RelayMessage::Closed { subscription_id, .. } if *subscription_id == sub_id => {
578 return Err(()); }
580 _ => {}
581 },
582 RelayNotification::Shutdown => return Err(()),
583 _ => {}
584 }
585 }
586}
587
588pub(crate) struct UnionPlan {
592 attempted: usize,
593 successes: usize,
594 resolved: usize,
595 evidence: Evidence,
596}
597
598impl UnionPlan {
599 pub(crate) fn new(evidence: Evidence, attempted: usize) -> Self {
600 Self { attempted, successes: 0, resolved: 0, evidence }
601 }
602
603 pub(crate) fn record(&mut self, success: bool) {
604 self.resolved += 1;
605 if success {
606 self.successes += 1;
607 }
608 }
609
610 pub(crate) fn satisfied(&self) -> bool {
615 match self.evidence {
616 Evidence::Fast => self.successes >= 1,
617 Evidence::Quorum => self.successes >= (self.attempted / 2) + 1,
618 Evidence::Full => self.resolved >= self.attempted,
619 }
620 }
621
622 pub(crate) fn exhausted(&self) -> bool {
624 self.resolved >= self.attempted
625 }
626
627 pub(crate) fn successes(&self) -> usize {
628 self.successes
629 }
630
631 pub(crate) fn attempted(&self) -> usize {
632 self.attempted
633 }
634}
635
636#[cfg(feature = "tor")]
640const TOR_READY_WAIT: std::time::Duration = std::time::Duration::from_secs(30);
641
642#[allow(dead_code)]
652async fn wait_until_tor_ready<F: Fn() -> bool>(
653 is_blocked: F,
654 max_wait: std::time::Duration,
655) -> Result<(), String> {
656 if !is_blocked() {
657 return Ok(());
658 }
659 let deadline = std::time::Instant::now() + max_wait;
660 while is_blocked() {
661 if std::time::Instant::now() >= deadline {
662 return Err("Tor is still connecting. Wait a moment and try again.".to_string());
663 }
664 tokio::time::sleep(std::time::Duration::from_millis(250)).await;
665 }
666 Ok(())
667}
668
669pub async fn prune_unneeded_community_relays(candidates: &[String]) {
677 if candidates.is_empty() {
678 return;
679 }
680 let Some(client) = crate::state::nostr_client() else { return };
681
682 let mut still_needed: std::collections::HashSet<String> = std::collections::HashSet::new();
683 if let Ok(ids) = crate::db::community::list_community_ids() {
684 for id in ids {
685 if let Ok(Some(c)) = crate::db::community::load_community(&id) {
686 for r in &c.relays {
687 still_needed.insert(r.clone());
688 }
689 }
690 }
691 }
692
693 let pool = client.pool();
694 let pooled = pool.all_relays().await;
696 for url in candidates {
697 if still_needed.contains(url) {
698 continue;
699 }
700 if let Ok(parsed) = nostr_sdk::RelayUrl::parse(url) {
701 if let Some(relay) = pooled.get(&parsed) {
702 if relay.flags().has_read() || relay.flags().has_write() {
703 continue; }
705 }
706 let _ = pool.force_remove_relay(parsed).await; forget_warmed_relay(url);
708 }
709 }
710}
711
712pub struct LiveTransport {
719 timeout: std::time::Duration,
720}
721
722impl Default for LiveTransport {
723 fn default() -> Self {
724 Self { timeout: std::time::Duration::from_secs(10) }
725 }
726}
727
728impl LiveTransport {
729 pub fn new() -> Self {
730 Self::default()
731 }
732
733 pub fn with_timeout(timeout: std::time::Duration) -> Self {
734 Self { timeout }
735 }
736
737 async fn warm_client(relays: &[String], connect_timeout: std::time::Duration) -> Result<Client, String> {
743 if relays.is_empty() {
744 return Err("community has no relays configured".to_string());
745 }
746 #[cfg(feature = "tor")]
752 wait_until_tor_ready(
753 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
754 TOR_READY_WAIT,
755 ).await?;
756 let client = crate::state::nostr_client().ok_or_else(|| "nostr client not initialized".to_string())?;
757
758 let generation = crate::state::current_session_generation();
762 {
763 let warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
764 if warmed.0 == generation && relays.iter().all(|r| warmed.1.contains(r)) {
765 return Ok(client);
766 }
767 }
768
769 let mut added_new = false;
774 let mut succeeded: Vec<&String> = Vec::new();
775 for url in relays {
776 let opts = crate::community_relay_options();
777 match client.pool().add_relay(url.as_str(), opts).await {
778 Ok(true) => { added_new = true; succeeded.push(url); }
779 Ok(false) => { succeeded.push(url); }
780 Err(_) => {}
781 }
782 }
783 if succeeded.is_empty() {
784 return Err("no valid community relays could be added".to_string());
785 }
786 if added_new {
787 let _ = client.try_connect(connect_timeout).await;
792 } else {
793 client.connect().await;
795 }
796
797 {
800 let mut warmed = WARMED_RELAYS.lock().unwrap_or_else(|e| e.into_inner());
801 if warmed.0 != generation {
802 warmed.0 = generation;
803 warmed.1.clear();
804 }
805 for url in succeeded {
806 warmed.1.insert(url.clone());
807 }
808 }
809 Ok(client)
810 }
811
812 pub async fn fetch_counted(&self, query: &Query, relays: &[String]) -> Result<(Vec<Event>, usize, usize), String> {
817 let client = Self::warm_client(relays, self.timeout).await?;
818 let base_timeout = self.timeout;
819 let filter = query.to_filter();
820
821 let evidence = effective_evidence(query);
822
823 let mut targets: Vec<String> = Vec::new();
824 for r in relays {
825 if !targets.contains(r) {
826 targets.push(r.clone());
827 }
828 }
829
830 if evidence == Evidence::Fast && targets.len() >= 2 {
835 let alive: Vec<String> =
836 targets.iter().filter(|r| !breaker_tripped(r)).cloned().collect();
837 if !alive.is_empty() {
838 targets = alive;
839 }
840 }
841
842 if targets.len() <= 1 {
845 let Some(url) = targets.first() else {
846 return Err("no valid relay to fetch from".to_string());
847 };
848 let res = fetch_relay_eose(&client, url, filter, base_timeout)
849 .await
850 .map_err(|_| format!("relay did not answer the fetch: {url}"));
851 breaker_record(url, res.is_ok(), true);
852 return res.map(|evs| (evs, 1, 1));
853 }
854
855 fn merge_events(
856 evs: Vec<Event>,
857 result: &mut Vec<Event>,
858 seen: &mut std::collections::HashSet<EventId>,
859 ) {
860 for e in evs {
861 if seen.insert(e.id) {
862 result.push(e);
863 }
864 }
865 }
866
867 let demote = evidence == Evidence::Full && demotion_allowed();
874 use futures_util::stream::{FuturesUnordered, StreamExt};
875 let mut fetches: FuturesUnordered<_> = targets
876 .iter()
877 .map(|r| {
878 let client = client.clone();
879 let filter = filter.clone();
880 let r = r.clone();
881 let timeout = if demote && breaker_tripped(&r) {
882 TRIPPED_TIMEOUT.min(base_timeout)
883 } else {
884 base_timeout
885 };
886 let full_budget = timeout >= base_timeout;
887 tokio::spawn(async move {
888 let out = fetch_relay_eose(&client, &r, filter, timeout).await;
889 (r, full_budget, out)
890 })
891 })
892 .collect();
893
894 let mut plan = UnionPlan::new(evidence, targets.len());
895 let mut result: Vec<Event> = Vec::new();
896 let mut union_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
897
898 let mut quorum_deadline: Option<tokio::time::Instant> = None;
903 let mut quorum_window_closed = false;
904 while !plan.satisfied() && !plan.exhausted() {
905 let next = match quorum_deadline {
906 Some(deadline) => match tokio::time::timeout_at(deadline, fetches.next()).await {
907 Ok(n) => n,
908 Err(_) => {
909 quorum_window_closed = true;
910 break; }
912 },
913 None => fetches.next().await,
914 };
915 let Some(joined) = next else { break };
916 match joined {
917 Ok((url, full_budget, Ok(evs))) => {
918 breaker_record(&url, true, full_budget);
919 merge_events(evs, &mut result, &mut union_ids);
920 plan.record(true);
921 if evidence == Evidence::Quorum && quorum_deadline.is_none() {
922 quorum_deadline = Some(
923 tokio::time::Instant::now()
924 + std::time::Duration::from_millis(QUORUM_GRACE_MS),
925 );
926 }
927 }
928 Ok((url, full_budget, Err(()))) => {
929 breaker_record(&url, false, full_budget);
930 plan.record(false);
931 }
932 Err(_) => plan.record(false), }
934 }
935
936 if plan.successes() == 0 {
937 return Err(format!(
938 "no relay answered the fetch (0/{} attempted)",
939 plan.attempted()
940 ));
941 }
942
943 if !fetches.is_empty() && !quorum_window_closed {
947 let grace = tokio::time::sleep(std::time::Duration::from_millis(RESIDUAL_GRACE_MS));
948 tokio::pin!(grace);
949 loop {
950 tokio::select! {
951 _ = &mut grace => break,
952 next = fetches.next() => match next {
953 Some(Ok((url, full_budget, Ok(evs)))) => {
954 breaker_record(&url, true, full_budget);
955 merge_events(evs, &mut result, &mut union_ids);
956 }
957 Some(Ok((url, full_budget, Err(())))) => {
958 breaker_record(&url, false, full_budget);
959 }
960 Some(Err(_)) => continue,
961 None => break,
962 }
963 }
964 }
965 }
966
967 if !fetches.is_empty() {
971 let seen: std::collections::HashSet<EventId> = result.iter().map(|e| e.id).collect();
972 let session = crate::state::SessionGuard::capture();
975 tokio::spawn(async move {
976 let mut extra: Vec<Event> = Vec::new();
977 let mut extra_ids: std::collections::HashSet<EventId> = std::collections::HashSet::new();
978 while let Some(joined) = fetches.next().await {
979 if let Ok((url, full_budget, out)) = joined {
980 breaker_record(&url, out.is_ok(), full_budget);
984 if let Ok(evs) = out {
985 for e in evs {
986 if !seen.contains(&e.id) && extra_ids.insert(e.id) {
987 extra.push(e);
988 }
989 }
990 }
991 }
992 }
993 if !session.is_valid() {
994 return;
995 }
996 submit_stragglers(extra);
997 });
998 }
999
1000 Ok((result, plan.successes(), plan.attempted()))
1001 }
1002}
1003
1004#[async_trait::async_trait]
1005impl Transport for LiveTransport {
1006 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1007 let client = Self::warm_client(relays, self.timeout).await?;
1008 let timeout = self.timeout;
1009 let mut targets: Vec<String> = Vec::new();
1010 for r in relays { if !targets.contains(r) { targets.push(r.clone()); } }
1011 use futures_util::stream::{FuturesUnordered, StreamExt};
1017 let mut sends: FuturesUnordered<_> = targets
1018 .into_iter()
1019 .map(|r| {
1020 let client = client.clone();
1021 let event = event.clone();
1022 tokio::spawn(async move {
1023 matches!(
1024 tokio::time::timeout(timeout, client.send_event_to(vec![r.clone()], &event)).await,
1025 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains(&u)).unwrap_or(false)
1026 )
1027 })
1028 })
1029 .collect();
1030 while let Some(joined) = sends.next().await {
1031 if matches!(joined, Ok(true)) {
1032 return Ok(()); }
1034 }
1035 Err("no relay accepted the event".to_string())
1036 }
1037
1038 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1039 self.fetch_counted(query, relays).await.map(|(events, _successes, _attempted)| events)
1040 }
1041
1042 async fn fetch_plane(&self, plane: &Keys, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1043 if relays.is_empty() {
1044 return Ok(Vec::new());
1045 }
1046 #[cfg(feature = "tor")]
1047 wait_until_tor_ready(
1048 || matches!(crate::tor::transport_state(), crate::tor::TorTransportState::RequiredButInactive),
1049 TOR_READY_WAIT,
1050 ).await?;
1051 let mut targets: Vec<String> = relays.iter().filter(|r| !breaker_tripped(r)).cloned().collect();
1056 if targets.is_empty() {
1057 targets = relays.to_vec();
1058 }
1059 let filter = query.to_filter();
1060 let generation = crate::state::current_session_generation();
1061 let key = plane_pool_key(&plane.public_key().to_hex(), &targets);
1062
1063 let (hit, evicted) = plane_pool_take(generation, &key);
1066 disconnect_clients(evicted);
1067 let client = if let Some(c) = hit {
1068 c
1069 } else {
1070 let opts = crate::nostr_client_options().automatic_authentication(true);
1073 let client = nostr_sdk::Client::builder().signer(plane.clone()).opts(opts).build();
1074 for r in &targets {
1079 let _ = client.pool().add_relay(r.clone(), crate::community_relay_options()).await;
1080 }
1081 client.connect().await;
1082 for r in &targets {
1085 let _ = client
1086 .fetch_events_from(vec![r.clone()], filter.clone(), std::time::Duration::from_secs(5))
1087 .await;
1088 }
1089 let ev = plane_pool_insert(generation, key, client.clone());
1090 disconnect_clients(ev);
1091 client
1092 };
1093
1094 let mut result: Vec<Event> = Vec::new();
1095 let mut seen: std::collections::HashSet<EventId> = std::collections::HashSet::new();
1096 let mut successes = 0usize;
1097 for r in &targets {
1098 let res = fetch_relay_eose(&client, r, filter.clone(), self.timeout).await;
1099 breaker_record(r, res.is_ok(), true);
1102 if let Ok(events) = res {
1103 successes += 1;
1104 for e in events {
1105 if seen.insert(e.id) {
1106 result.push(e);
1107 }
1108 }
1109 }
1110 }
1111 if successes == 0 {
1116 return Err(format!("no relay answered the plane fetch (0/{} attempted)", targets.len()));
1117 }
1118 Ok(result)
1120 }
1121
1122 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1123 let client = Self::warm_client(relays, self.timeout).await?;
1128 let timeout = self.timeout;
1129 let event = event.clone();
1130 let backoff = std::time::Duration::from_millis(750);
1131 let mut pending: Vec<String> = Vec::new();
1132 for r in relays { if !pending.contains(r) { pending.push(r.clone()); } }
1133 if pending.is_empty() {
1134 return Err("no relays to broadcast to".to_string());
1135 }
1136
1137 let mut acked_any = false;
1142 let _ = tokio::time::timeout(CONFIRM_WINDOW, async {
1143 loop {
1144 let sends = pending.iter().cloned().map(|r| {
1145 let client = &client;
1146 let event = &event;
1147 Box::pin(async move {
1148 match tokio::time::timeout(timeout, client.send_event_to(vec![r.clone()], event)).await {
1149 Ok(Ok(out)) if RelayUrl::parse(&r).map(|u| out.success.contains(&u)).unwrap_or(false) => Ok(r),
1150 _ => Err(()),
1151 }
1152 })
1153 });
1154 if let Ok((winner, _losers)) = futures_util::future::select_ok(sends).await {
1155 acked_any = true;
1156 pending.retain(|r| r != &winner);
1157 break;
1158 }
1159 tokio::time::sleep(backoff).await;
1160 }
1161 })
1162 .await;
1163
1164 if !acked_any {
1165 return Err(format!("no relay accepted the event within {}s", CONFIRM_WINDOW.as_secs()));
1166 }
1167 if pending.is_empty() {
1168 return Ok(()); }
1170
1171 tokio::spawn(async move {
1176 let client_ref = &client;
1177 let event_ref = &event;
1178 let _ = durable_broadcast(&pending, MAX_PUBLISH_ATTEMPTS, backoff, move |round| {
1179 Box::pin(async move {
1180 match tokio::time::timeout(timeout, client_ref.send_event_to(round.clone(), event_ref)).await {
1181 Ok(Ok(output)) => round.into_iter().filter(|p| RelayUrl::parse(p).map(|u| output.success.contains(&u)).unwrap_or(false)).collect(),
1182 _ => Vec::new(),
1183 }
1184 })
1185 })
1186 .await;
1187 });
1188 Ok(())
1189 }
1190}
1191
1192#[cfg(test)]
1193pub(crate) mod memory {
1194 use super::*;
1195 use std::collections::{HashMap, HashSet};
1196 use std::sync::Mutex;
1197
1198 pub struct MemoryRelay {
1203 per_relay: Mutex<HashMap<String, Vec<Event>>>,
1204 subscribers: Mutex<Vec<(Query, tokio::sync::mpsc::UnboundedSender<Event>)>>,
1205 }
1206
1207 fn is_ephemeral(kind: u16) -> bool {
1210 (20000..30000).contains(&kind)
1211 }
1212
1213 impl MemoryRelay {
1214 pub fn new() -> Self {
1215 MemoryRelay {
1216 per_relay: Mutex::new(HashMap::new()),
1217 subscribers: Mutex::new(Vec::new()),
1218 }
1219 }
1220
1221 pub fn subscribe(&self, query: Query) -> tokio::sync::mpsc::UnboundedReceiver<Event> {
1224 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
1225 self.subscribers.lock().unwrap().push((query, tx));
1226 rx
1227 }
1228
1229 fn deliver(&self, event: &Event) {
1231 self.subscribers.lock().unwrap().retain(|(q, tx)| {
1232 if q.matches(event) {
1233 tx.send(event.clone()).is_ok()
1234 } else {
1235 !tx.is_closed()
1236 }
1237 });
1238 }
1239
1240 pub fn inject(&self, event: &Event, relays: &[String]) {
1243 self.deliver(event);
1244 if is_ephemeral(event.kind.as_u16()) {
1245 return; }
1247 let d_tag = |e: &Event| e.tags.iter().find_map(|t| {
1253 let s = t.as_slice();
1254 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
1255 }).unwrap_or_default();
1256 let k = event.kind.as_u16();
1257 let replaceable = (30000..40000).contains(&k) || (10000..20000).contains(&k) || k == 0 || k == 3;
1258 let coord = (event.kind.as_u16(), event.pubkey, d_tag(event));
1259 let mut map = self.per_relay.lock().unwrap();
1260 for r in relays {
1261 let v = map.entry(r.clone()).or_default();
1262 if replaceable {
1263 v.retain(|e| (e.kind.as_u16(), e.pubkey, d_tag(e)) != coord);
1264 }
1265 v.push(event.clone());
1266 }
1267 }
1268
1269 pub fn count_on(&self, relay: &str) -> usize {
1271 self.per_relay.lock().unwrap().get(relay).map_or(0, |v| v.len())
1272 }
1273
1274 fn apply_deletion(&self, deletion: &Event, relays: &[String]) {
1279 let mut id_targets: HashSet<String> = HashSet::new();
1280 let mut coord_targets: HashSet<String> = HashSet::new();
1281 for t in deletion.tags.iter() {
1282 let s = t.as_slice();
1283 if s.len() >= 2 && s[0] == "e" {
1284 id_targets.insert(s[1].clone());
1285 } else if s.len() >= 2 && s[0] == "a" {
1286 coord_targets.insert(s[1].clone());
1287 }
1288 }
1289 let mut map = self.per_relay.lock().unwrap();
1290 for r in relays {
1291 if let Some(events) = map.get_mut(r) {
1292 events.retain(|e| {
1293 if e.pubkey != deletion.pubkey {
1294 return true;
1295 }
1296 if id_targets.contains(&e.id.to_hex()) {
1297 return false;
1298 }
1299 let d = e.tags.iter().find_map(|t| {
1301 let s = t.as_slice();
1302 (s.len() >= 2 && s[0] == "d").then(|| s[1].clone())
1303 }).unwrap_or_default();
1304 let coord = format!("{}:{}:{}", e.kind.as_u16(), e.pubkey.to_hex(), d);
1305 !coord_targets.contains(&coord)
1306 });
1307 }
1308 }
1309 }
1310 }
1311
1312 #[async_trait::async_trait]
1313 impl Transport for MemoryRelay {
1314 async fn publish(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1315 if event.kind == Kind::EventDeletion {
1317 self.apply_deletion(event, relays);
1318 self.deliver(event);
1319 } else {
1320 self.inject(event, relays);
1321 }
1322 Ok(())
1323 }
1324
1325 async fn publish_durable(&self, event: &Event, relays: &[String]) -> Result<(), String> {
1328 self.publish(event, relays).await
1329 }
1330
1331 async fn fetch(&self, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1332 let map = self.per_relay.lock().unwrap();
1333 let mut seen = HashSet::new();
1334 let mut out = Vec::new();
1335 for r in relays {
1336 if let Some(events) = map.get(r) {
1337 for ev in events {
1338 if is_ephemeral(ev.kind.as_u16()) {
1341 continue;
1342 }
1343 if query.matches(ev) && seen.insert(ev.id) {
1344 out.push(ev.clone());
1345 }
1346 }
1347 }
1348 }
1349 if let Some(limit) = query.limit {
1352 out.sort_by(|a, b| b.created_at.cmp(&a.created_at).then_with(|| b.id.cmp(&a.id)));
1353 out.truncate(limit);
1354 }
1355 Ok(out)
1356 }
1357
1358 async fn fetch_plane(&self, _plane: &Keys, query: &Query, relays: &[String]) -> Result<Vec<Event>, String> {
1359 self.fetch(query, relays).await
1361 }
1362 }
1363}
1364
1365#[cfg(test)]
1366mod tests {
1367 use super::*;
1368
1369 #[tokio::test]
1374 async fn tor_gate_passes_immediately_when_not_blocked() {
1375 let start = std::time::Instant::now();
1376 let res = wait_until_tor_ready(|| false, std::time::Duration::from_secs(30)).await;
1377 assert!(res.is_ok());
1378 assert!(start.elapsed() < std::time::Duration::from_secs(1), "must not wait when Tor is ready");
1379 }
1380
1381 #[tokio::test]
1382 async fn tor_gate_errors_honestly_after_timeout_when_perpetually_blocked() {
1383 let res = wait_until_tor_ready(|| true, std::time::Duration::from_millis(300)).await;
1386 let err = res.expect_err("should error when Tor never activates");
1387 assert!(err.to_lowercase().contains("tor"), "error must name Tor, got: {err}");
1388 }
1389
1390 #[tokio::test]
1391 async fn tor_gate_passes_once_circuit_comes_up_mid_wait() {
1392 let calls = std::sync::atomic::AtomicUsize::new(0);
1393 let res = wait_until_tor_ready(
1395 || calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 3,
1396 std::time::Duration::from_secs(5),
1397 ).await;
1398 assert!(res.is_ok(), "should succeed once Tor activates within the window");
1399 }
1400
1401 fn evt(kind: u16, z: &str) -> Event {
1402 EventBuilder::new(Kind::Custom(kind), "x")
1403 .tags([Tag::custom(
1404 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
1405 [z.to_string()],
1406 )])
1407 .sign_with_keys(&Keys::generate())
1408 .unwrap()
1409 }
1410
1411 #[test]
1412 fn query_matches_kind_and_z() {
1413 let e = evt(3300, "abc");
1414 assert!(Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: None, ..Default::default() }.matches(&e));
1415 assert!(!Query { kinds: vec![3301], ..Default::default() }.matches(&e));
1416 assert!(!Query { kinds: vec![], z_tags: vec!["xyz".into()], since: None, ..Default::default() }.matches(&e));
1417 assert!(Query::default().matches(&e), "empty query matches anything");
1418 }
1419
1420 fn evt_at(kind: u16, secs: u64) -> Event {
1421 EventBuilder::new(Kind::Custom(kind), "x")
1422 .custom_created_at(Timestamp::from(secs))
1423 .sign_with_keys(&Keys::generate())
1424 .unwrap()
1425 }
1426
1427 fn evt_z_at(kind: u16, z: &str, secs: u64) -> Event {
1430 EventBuilder::new(Kind::Custom(kind), "x")
1431 .custom_created_at(Timestamp::from(secs))
1432 .tags([Tag::custom(
1433 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
1434 [z.to_string()],
1435 )])
1436 .sign_with_keys(&Keys::generate())
1437 .unwrap()
1438 }
1439
1440 #[tokio::test]
1441 async fn fetch_pages_with_until_and_limit_newest_first() {
1442 let relay = super::memory::MemoryRelay::new();
1444 let relays = vec!["r1".to_string()];
1445 for s in 1..=5u64 {
1446 relay.inject(&evt_z_at(3300, "pg", s), &relays);
1447 }
1448 let secs = |evs: &[Event]| evs.iter().map(|e| e.created_at.as_secs()).collect::<Vec<_>>();
1449
1450 let latest = relay
1452 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], limit: Some(2), ..Default::default() }, &relays)
1453 .await
1454 .unwrap();
1455 assert_eq!(secs(&latest), vec![5, 4]);
1456
1457 let older = relay
1459 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(3), limit: Some(2), ..Default::default() }, &relays)
1460 .await
1461 .unwrap();
1462 assert_eq!(secs(&older), vec![3, 2]);
1463
1464 let start = relay
1467 .fetch(&Query { kinds: vec![3300], z_tags: vec!["pg".into()], until: Some(1), limit: Some(2), ..Default::default() }, &relays)
1468 .await
1469 .unwrap();
1470 assert_eq!(secs(&start), vec![1]);
1471 }
1472
1473 #[test]
1474 fn to_filter_translates_kinds_z_and_since() {
1475 let q = Query { kinds: vec![3300], z_tags: vec!["abc".into()], since: Some(100), ..Default::default() };
1478 let filter = q.to_filter();
1479 let matching = EventBuilder::new(Kind::Custom(3300), "x")
1480 .custom_created_at(Timestamp::from(150))
1481 .tags([Tag::custom(
1482 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
1483 ["abc".to_string()],
1484 )])
1485 .sign_with_keys(&Keys::generate())
1486 .unwrap();
1487 assert!(filter.match_event(&matching, MatchEventOptions::new()), "to_filter must accept what matches() accepts");
1488 assert!(q.matches(&matching));
1489
1490 let wrong_kind = EventBuilder::new(Kind::Custom(3301), "x")
1492 .custom_created_at(Timestamp::from(150))
1493 .tags([Tag::custom(
1494 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::Z)),
1495 ["abc".to_string()],
1496 )])
1497 .sign_with_keys(&Keys::generate())
1498 .unwrap();
1499 assert!(!filter.match_event(&wrong_kind, MatchEventOptions::new()));
1500 }
1501
1502 fn evt_sl(kind: u16, letter: Alphabet, value: &str) -> Event {
1504 EventBuilder::new(Kind::Custom(kind), "x")
1505 .tags([Tag::custom(
1506 TagKind::SingleLetter(SingleLetterTag::lowercase(letter)),
1507 [value.to_string()],
1508 )])
1509 .sign_with_keys(&Keys::generate())
1510 .unwrap()
1511 }
1512
1513 #[test]
1514 fn to_filter_and_matches_agree_on_authors() {
1515 let keys = Keys::generate();
1516 let e = EventBuilder::new(Kind::Custom(1059), "x").sign_with_keys(&keys).unwrap();
1517 let q = Query { kinds: vec![1059], authors: vec![keys.public_key().to_hex()], ..Default::default() };
1518 assert!(q.matches(&e));
1519 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1520 let miss = Query { kinds: vec![1059], authors: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
1521 assert!(!miss.matches(&e));
1522 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1523 }
1524
1525 #[test]
1526 fn to_filter_and_matches_agree_on_p_tags() {
1527 let recipient = Keys::generate().public_key().to_hex();
1528 let e = evt_sl(1059, Alphabet::P, &recipient);
1529 let q = Query { kinds: vec![1059], p_tags: vec![recipient], ..Default::default() };
1530 assert!(q.matches(&e));
1531 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1532 let miss = Query { kinds: vec![1059], p_tags: vec![Keys::generate().public_key().to_hex()], ..Default::default() };
1533 assert!(!miss.matches(&e));
1534 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1535 }
1536
1537 #[test]
1538 fn to_filter_and_matches_agree_on_k_tags() {
1539 let e = evt_sl(1059, Alphabet::K, "3311");
1540 let q = Query { kinds: vec![1059], k_tags: vec!["3311".into()], ..Default::default() };
1541 assert!(q.matches(&e));
1542 assert!(q.to_filter().match_event(&e, MatchEventOptions::new()));
1543 let miss = Query { kinds: vec![1059], k_tags: vec!["3300".into()], ..Default::default() };
1544 assert!(!miss.matches(&e));
1545 assert!(!miss.to_filter().match_event(&e, MatchEventOptions::new()));
1546 }
1547
1548 #[test]
1549 fn to_filter_empty_kinds_only_constrains_z_and_since() {
1550 let q = Query { kinds: vec![], z_tags: vec!["p".into()], since: None, ..Default::default() };
1552 let filter = q.to_filter();
1553 let e = evt(3300, "p");
1554 assert!(filter.match_event(&e, MatchEventOptions::new()));
1555 assert!(q.matches(&e));
1556 }
1557
1558 #[test]
1559 fn since_is_an_inclusive_lower_bound() {
1560 let below = evt_at(3300, 99);
1561 let exact = evt_at(3300, 100);
1562 let above = evt_at(3300, 101);
1563 let q = Query { kinds: vec![3300], z_tags: vec![], since: Some(100), ..Default::default() };
1564 assert!(!q.matches(&below), "below the floor is excluded");
1565 assert!(q.matches(&exact), "exactly the floor is included");
1566 assert!(q.matches(&above), "above the floor is included");
1567 }
1568
1569 #[test]
1570 fn z_tags_match_as_or_set() {
1571 let e = evt(3300, "p2");
1574 let q = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p2".into()], since: None, ..Default::default() };
1575 assert!(q.matches(&e));
1576 let miss = Query { kinds: vec![3300], z_tags: vec!["p1".into(), "p3".into()], since: None, ..Default::default() };
1577 assert!(!miss.matches(&e));
1578 }
1579
1580 #[tokio::test]
1581 async fn fetch_unions_and_dedups_across_relays() {
1582 use super::memory::MemoryRelay;
1583 let relay = MemoryRelay::new();
1584 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1585 let e = evt(3300, "p");
1586 relay.publish(&e, &relays).await.unwrap();
1587 let got = relay
1588 .fetch(&Query { kinds: vec![3300], z_tags: vec!["p".into()], since: None, ..Default::default() }, &relays)
1589 .await
1590 .unwrap();
1591 assert_eq!(got.len(), 1, "same event on 3 relays dedups to 1");
1592 }
1593
1594 #[tokio::test]
1595 async fn durable_broadcast_retries_only_the_failing_relays_until_they_ack() {
1596 use std::cell::Cell;
1599 let relays = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1600 let round = Cell::new(0usize);
1601 let r2_round_seen = Cell::new(0usize);
1602 let res = durable_broadcast(&relays, 30, std::time::Duration::ZERO, |pending| {
1603 let n = round.get();
1604 round.set(n + 1);
1605 if n >= 1 {
1607 assert_eq!(pending, vec!["r2".to_string()], "only the failing relay is retried");
1608 r2_round_seen.set(r2_round_seen.get() + 1);
1609 }
1610 Box::pin(async move {
1611 pending.into_iter().filter(|r| r != "r2" || n >= 4).collect()
1612 })
1613 })
1614 .await;
1615 assert!(res.is_ok(), "all relays eventually ACK → Ok");
1616 assert!(round.get() >= 5, "kept retrying r2 across rounds");
1617 }
1618
1619 #[tokio::test]
1620 async fn durable_broadcast_is_ok_if_some_ack_even_when_one_never_does() {
1621 let relays = vec!["r1".to_string(), "r2".to_string()];
1624 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |pending| {
1625 Box::pin(async move { pending.into_iter().filter(|r| r == "r1").collect() })
1626 })
1627 .await;
1628 assert!(res.is_ok(), "≥1 relay accepted → Ok despite a permanently-failing relay");
1629 }
1630
1631 #[tokio::test]
1632 async fn durable_broadcast_errs_only_if_zero_relays_ever_accept() {
1633 let relays = vec!["r1".to_string(), "r2".to_string()];
1634 let res = durable_broadcast(&relays, 5, std::time::Duration::ZERO, |_pending| {
1635 Box::pin(async move { Vec::new() }) })
1637 .await;
1638 assert!(res.is_err(), "zero acceptances after the retry cap → Err");
1639 }
1640
1641 #[tokio::test]
1642 async fn redundancy_self_heals_a_missing_relay() {
1643 use super::memory::MemoryRelay;
1644 let relay = MemoryRelay::new();
1645 let all = vec!["r1".to_string(), "r2".to_string(), "r3".to_string()];
1646 let e = evt(3300, "p");
1647 relay.inject(&e, &["r2".to_string()]);
1649 assert_eq!(relay.count_on("r1"), 0);
1650 assert_eq!(relay.count_on("r2"), 1);
1651 let got = relay.fetch(&Query { kinds: vec![3300], ..Default::default() }, &all).await.unwrap();
1653 assert_eq!(got.len(), 1);
1654 }
1655
1656 #[tokio::test]
1657 async fn ephemeral_kind_streams_live_but_is_never_stored_or_fetched() {
1658 use super::memory::MemoryRelay;
1659 let relay = MemoryRelay::new();
1660 let relays = vec!["r1".to_string()];
1661 let mut sub = relay.subscribe(Query { kinds: vec![21059], ..Default::default() });
1662 let e = evt(21059, "p");
1663 relay.publish(&e, &relays).await.unwrap();
1664 assert_eq!(relay.count_on("r1"), 0, "ephemeral is never stored");
1665 let got = relay
1666 .fetch(&Query { kinds: vec![21059], ..Default::default() }, &relays)
1667 .await
1668 .unwrap();
1669 assert!(got.is_empty(), "a real relay never serves an ephemeral from a fetch");
1670 assert_eq!(sub.try_recv().unwrap().id, e.id, "but a live subscriber receives it");
1671 }
1672
1673 #[tokio::test]
1674 async fn stored_kind_is_fetchable_and_delivered_live() {
1675 use super::memory::MemoryRelay;
1676 let relay = MemoryRelay::new();
1677 let relays = vec!["r1".to_string()];
1678 let mut sub = relay.subscribe(Query { kinds: vec![1059], ..Default::default() });
1679 let e = evt(1059, "p");
1680 relay.publish(&e, &relays).await.unwrap();
1681 let got = relay
1682 .fetch(&Query { kinds: vec![1059], ..Default::default() }, &relays)
1683 .await
1684 .unwrap();
1685 assert_eq!(got.len(), 1, "stored kind is fetchable");
1686 assert_eq!(sub.try_recv().unwrap().id, e.id, "and delivered to the live subscriber");
1687 }
1688
1689 #[tokio::test]
1690 async fn p_tags_route_a_giftwrap_to_the_matching_subscriber() {
1691 use super::memory::MemoryRelay;
1692 let relay = MemoryRelay::new();
1693 let relays = vec!["r1".to_string()];
1694 let alice = Keys::generate().public_key().to_hex();
1695 let bob = Keys::generate().public_key().to_hex();
1696 let mut sub_alice =
1697 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![alice.clone()], ..Default::default() });
1698 let mut sub_bob =
1699 relay.subscribe(Query { kinds: vec![1059], p_tags: vec![bob.clone()], ..Default::default() });
1700 let wrap = evt_sl(1059, Alphabet::P, &alice);
1701 relay.publish(&wrap, &relays).await.unwrap();
1702 assert_eq!(sub_alice.try_recv().unwrap().id, wrap.id, "addressed recipient gets it live");
1703 assert!(sub_bob.try_recv().is_err(), "a differently-addressed subscriber does not");
1704 let for_alice = relay
1706 .fetch(&Query { kinds: vec![1059], p_tags: vec![alice], ..Default::default() }, &relays)
1707 .await
1708 .unwrap();
1709 assert_eq!(for_alice.len(), 1);
1710 let for_bob = relay
1711 .fetch(&Query { kinds: vec![1059], p_tags: vec![bob], ..Default::default() }, &relays)
1712 .await
1713 .unwrap();
1714 assert!(for_bob.is_empty());
1715 }
1716
1717 #[test]
1720 fn union_plan_fast_satisfied_on_first_success() {
1721 let mut p = UnionPlan::new(Evidence::Fast, 4);
1722 p.record(false);
1723 assert!(!p.satisfied(), "a failure is not evidence");
1724 p.record(true);
1725 assert!(p.satisfied(), "one genuine EOSE satisfies Fast");
1726 assert!(!p.exhausted());
1727 }
1728
1729 #[test]
1730 fn union_plan_quorum_majority_math() {
1731 for (n, need) in [(2usize, 2usize), (3, 2), (4, 3), (5, 3)] {
1733 let mut p = UnionPlan::new(Evidence::Quorum, n);
1734 for _ in 0..need - 1 {
1735 p.record(true);
1736 }
1737 assert!(!p.satisfied(), "{}/{} must not satisfy quorum", need - 1, n);
1738 p.record(true);
1739 assert!(p.satisfied(), "{}/{} satisfies quorum", need, n);
1740 }
1741 }
1742
1743 #[test]
1744 fn union_plan_quorum_failures_never_substitute_for_successes() {
1745 let mut p = UnionPlan::new(Evidence::Quorum, 3);
1746 p.record(true);
1747 p.record(false);
1748 p.record(false);
1749 assert!(!p.satisfied(), "1 success + 2 failures is not a majority");
1750 assert!(p.exhausted(), "all resolved — the degraded path returns best-effort");
1751 assert_eq!(p.successes(), 1);
1752 }
1753
1754 #[test]
1755 fn union_plan_full_requires_every_relay_resolved() {
1756 let mut p = UnionPlan::new(Evidence::Full, 3);
1757 p.record(true);
1758 p.record(true);
1759 assert!(!p.satisfied(), "Full waits for the last relay even after 2 EOSEs");
1760 p.record(false);
1761 assert!(p.satisfied(), "a timeout is a resolution — Full is done");
1762 assert!(p.exhausted());
1763 }
1764
1765 #[test]
1766 fn union_plan_all_dead_is_reportable_not_a_confident_empty() {
1767 let mut p = UnionPlan::new(Evidence::Quorum, 2);
1768 p.record(false);
1769 p.record(false);
1770 assert!(p.exhausted());
1771 assert_eq!(p.successes(), 0, "the caller must map this to Err, never Ok(vec![])");
1772 }
1773
1774 const BREAKER_TEST_GEN: u64 = u64::MAX;
1780
1781 #[test]
1782 fn breaker_trips_only_after_consecutive_full_budget_failures() {
1783 let url = "wss://breaker-test-full-budget.example";
1784 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
1785 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "one failure is below the threshold");
1786 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
1787 assert!(breaker_tripped_at(BREAKER_TEST_GEN, url), "two consecutive full-budget failures trip");
1788 }
1789
1790 #[test]
1791 fn breaker_demoted_budget_failures_never_count() {
1792 let url = "wss://breaker-test-demoted.example";
1793 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
1794 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
1795 breaker_record_at(BREAKER_TEST_GEN, url, false, false);
1796 assert!(
1797 !breaker_tripped_at(BREAKER_TEST_GEN, url),
1798 "demoted-budget failures must not trip (anti-starvation: the post-cooldown probe must stay reachable)"
1799 );
1800 }
1801
1802 #[test]
1803 fn breaker_success_resets_the_entry() {
1804 let url = "wss://breaker-test-reset.example";
1805 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
1806 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
1807 assert!(breaker_tripped_at(BREAKER_TEST_GEN, url));
1808 breaker_record_at(BREAKER_TEST_GEN, url, true, false);
1810 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "any success unconditionally resets");
1811 breaker_record_at(BREAKER_TEST_GEN, url, false, true);
1812 assert!(!breaker_tripped_at(BREAKER_TEST_GEN, url), "and the failure count restarted from zero");
1813 }
1814
1815 #[test]
1818 fn until_forces_full_evidence_and_default_is_quorum() {
1819 assert_eq!(Query::default().evidence, Evidence::Quorum, "unclassified sites get Quorum");
1820 assert_eq!(
1821 effective_evidence(&Query { until: Some(1), evidence: Evidence::Fast, ..Default::default() }),
1822 Evidence::Full,
1823 "a back-page can never ride Fast — the history-start latch needs the full union"
1824 );
1825 assert_eq!(
1826 effective_evidence(&Query { evidence: Evidence::Fast, ..Default::default() }),
1827 Evidence::Fast,
1828 "without `until` the declared tier stands"
1829 );
1830 }
1831}