1use std::collections::{HashMap, HashSet};
7use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
8use std::sync::{Arc, Mutex, Weak};
9use std::time::Instant;
10
11use nostr_sdk::prelude::*;
12use std::sync::LazyLock;
13
14use crate::state::nostr_client;
15
16pub struct EventPublishTracker {
46 event_id: EventId,
47 successes: Mutex<Vec<RelayUrl>>,
50 notify: tokio::sync::Notify,
51 in_flight: AtomicUsize,
55}
56
57impl EventPublishTracker {
58 fn new(event_id: EventId, initial_in_flight: usize) -> Arc<Self> {
59 Arc::new(Self {
60 event_id,
61 successes: Mutex::new(Vec::new()),
62 notify: tokio::sync::Notify::new(),
63 in_flight: AtomicUsize::new(initial_in_flight),
64 })
65 }
66
67 fn note_success(&self, url: RelayUrl) {
69 self.successes.lock().unwrap().push(url);
70 self.notify.notify_waiters();
71 }
72
73 fn note_settled(&self) {
77 if self.in_flight.fetch_sub(1, Ordering::SeqCst) == 1 {
78 self.notify.notify_waiters();
79 PUBLISH_TRACKERS.lock().unwrap().remove(&self.event_id);
80 }
81 }
82
83 pub async fn next_success(&self, cursor: &mut usize) -> Option<RelayUrl> {
89 loop {
90 let notified = self.notify.notified();
94 tokio::pin!(notified);
95 notified.as_mut().enable();
96
97 let (next, done) = {
98 let successes = self.successes.lock().unwrap();
99 let next = successes.get(*cursor).cloned();
100 let done = self.in_flight.load(Ordering::SeqCst) == 0
101 && *cursor >= successes.len();
102 (next, done)
103 };
104
105 if let Some(url) = next {
106 *cursor += 1;
107 return Some(url);
108 }
109 if done {
110 return None;
111 }
112
113 notified.await;
114 }
115 }
116}
117
118static PUBLISH_TRACKERS: LazyLock<Mutex<HashMap<EventId, Arc<EventPublishTracker>>>> =
121 LazyLock::new(|| Mutex::new(HashMap::new()));
122
123pub fn get_publish_tracker(event_id: &EventId) -> Option<Arc<EventPublishTracker>> {
129 PUBLISH_TRACKERS.lock().unwrap().get(event_id).cloned()
130}
131
132pub fn spawn_tracked_publish(
143 resolved: Vec<(RelayUrl, Relay)>,
144 event: Event,
145) -> Vec<tokio::task::JoinHandle<(RelayUrl, Result<EventId, String>)>> {
146 let event_id = event.id;
147 if resolved.is_empty() {
150 return Vec::new();
151 }
152 let tracker = EventPublishTracker::new(event_id, resolved.len());
153 PUBLISH_TRACKERS.lock().unwrap().insert(event_id, tracker.clone());
154
155 let mut handles = Vec::with_capacity(resolved.len());
156 for (url, relay) in resolved {
157 let event = event.clone();
158 let tracker = tracker.clone();
159 handles.push(tokio::spawn(async move {
160 let result = relay
161 .send_event(&event)
162 .await
163 .map_err(|e| e.to_string());
164 if result.is_ok() {
165 tracker.note_success(url.clone());
166 }
167 tracker.note_settled();
168 (url, result)
169 }));
170 }
171 handles
172}
173
174const CACHE_TTL_SECS: u64 = 3600; const CACHE_TTL_ERROR_SECS: u64 = 60; struct CachedRelays {
185 relays: Vec<String>,
186 fetched_at: Instant,
187 fetch_ok: bool,
190}
191
192static INBOX_RELAY_CACHE: LazyLock<Mutex<HashMap<PublicKey, CachedRelays>>> =
193 LazyLock::new(|| Mutex::new(HashMap::new()));
194
195pub fn clear_inbox_relay_cache() {
201 if let Ok(mut cache) = INBOX_RELAY_CACHE.lock() {
202 cache.clear();
203 }
204}
205
206static FETCH_LOCKS: LazyLock<Mutex<HashMap<PublicKey, Weak<tokio::sync::Mutex<()>>>>> =
213 LazyLock::new(|| Mutex::new(HashMap::new()));
214
215static PRUNE_COUNTER: AtomicU64 = AtomicU64::new(0);
219
220#[cfg(not(test))]
223const PRUNE_INTERVAL: u64 = 100;
224
225#[cfg(test)]
228const PRUNE_INTERVAL: u64 = 1;
229
230struct FetchLockEntryCleanup {
233 pubkey: PublicKey,
234 key_lock: Arc<tokio::sync::Mutex<()>>,
235}
236
237impl FetchLockEntryCleanup {
238 fn new(pubkey: PublicKey, key_lock: Arc<tokio::sync::Mutex<()>>) -> Self {
239 Self { pubkey, key_lock }
240 }
241}
242
243impl Drop for FetchLockEntryCleanup {
244 fn drop(&mut self) {
245 let mut locks = match FETCH_LOCKS.lock() {
246 Ok(locks) => locks,
247 Err(_) => return, };
249
250 let should_remove = match locks.get(&self.pubkey).and_then(|weak| weak.upgrade()) {
251 Some(current) => {
252 Arc::ptr_eq(¤t, &self.key_lock) && Arc::strong_count(¤t) == 2
256 }
257 None => false,
258 };
259 if should_remove {
260 locks.remove(&self.pubkey);
261 }
262 }
263}
264
265struct FetchResult {
271 relays: Vec<String>,
272 fetch_ok: bool,
274}
275
276async fn fetch_inbox_relays(client: &Client, pubkey: &PublicKey) -> FetchResult {
278 let filter = Filter::new()
279 .author(*pubkey)
280 .kind(Kind::Custom(10050))
281 .limit(1);
282
283 let events = match client
284 .fetch_events(filter, std::time::Duration::from_secs(5))
285 .await
286 {
287 Ok(events) => events,
288 Err(e) => {
289 eprintln!("[InboxRelays] Failed to fetch 10050 for {}: {}", pubkey, e);
290 return FetchResult { relays: Vec::new(), fetch_ok: false };
291 }
292 };
293
294 let event = match events.into_iter().next() {
296 Some(e) => e,
297 None => return FetchResult { relays: Vec::new(), fetch_ok: true },
298 };
299
300 FetchResult { relays: parse_relay_tags(&event.tags), fetch_ok: true }
301}
302
303fn parse_relay_tags(tags: &Tags) -> Vec<String> {
306 tags.iter()
307 .filter_map(|tag| {
308 let values: Vec<&str> = tag.as_slice().iter().map(|s| s.as_str()).collect();
309 if values.len() >= 2 && values[0] == "relay" {
310 Some(values[1].to_string())
311 } else {
312 None
313 }
314 })
315 .collect()
316}
317
318async fn get_or_fetch_with_lock<F, Fut>(pubkey: &PublicKey, fetch_fn: F) -> Vec<String>
323where
324 F: FnOnce() -> Fut,
325 Fut: std::future::Future<Output = FetchResult>,
326{
327 {
329 let cache = INBOX_RELAY_CACHE.lock().unwrap();
330 if let Some(entry) = cache.get(pubkey) {
331 let ttl = if entry.fetch_ok { CACHE_TTL_SECS } else { CACHE_TTL_ERROR_SECS };
332 if entry.fetched_at.elapsed().as_secs() < ttl {
333 return entry.relays.clone();
334 }
335 }
336 }
337
338 let cleanup_guard = {
341 let mut locks = FETCH_LOCKS.lock().unwrap();
342
343 if PRUNE_COUNTER.fetch_add(1, Ordering::Relaxed) % PRUNE_INTERVAL == 0 {
347 locks.retain(|_, weak| Weak::strong_count(weak) > 0);
348 }
349
350 let weak = locks.entry(*pubkey).or_insert_with(|| Weak::new());
351 let key_lock = match weak.upgrade() {
354 Some(arc) => arc,
355 None => {
356 let new_arc = Arc::new(tokio::sync::Mutex::new(()));
357 *weak = Arc::downgrade(&new_arc);
358 new_arc
359 }
360 };
361 FetchLockEntryCleanup::new(*pubkey, key_lock)
363 };
364 let relays = {
365 let _guard = cleanup_guard.key_lock.lock().await;
366
367 let cached_relays = {
369 let cache = INBOX_RELAY_CACHE.lock().unwrap();
370 if let Some(entry) = cache.get(pubkey) {
371 let ttl = if entry.fetch_ok { CACHE_TTL_SECS } else { CACHE_TTL_ERROR_SECS };
372 if entry.fetched_at.elapsed().as_secs() < ttl {
373 Some(entry.relays.clone())
374 } else {
375 None
376 }
377 } else {
378 None
379 }
380 };
381
382 match cached_relays {
383 Some(relays) => relays,
384 None => {
385 let result = fetch_fn().await;
387
388 {
390 let mut cache = INBOX_RELAY_CACHE.lock().unwrap();
391 cache.insert(
392 *pubkey,
393 CachedRelays {
394 relays: result.relays.clone(),
395 fetched_at: Instant::now(),
396 fetch_ok: result.fetch_ok,
397 },
398 );
399 }
400
401 result.relays
402 }
403 }
404 }; drop(cleanup_guard);
409 relays
410}
411
412async fn get_or_fetch_inbox_relays(client: &Client, pubkey: &PublicKey) -> Vec<String> {
414 get_or_fetch_with_lock(pubkey, || fetch_inbox_relays(client, pubkey)).await
415}
416
417static TRUSTED_RELAY_URLS: LazyLock<Vec<RelayUrl>> = LazyLock::new(|| {
423 crate::state::TRUSTED_RELAYS
424 .iter()
425 .filter_map(|s| RelayUrl::parse(s).ok())
426 .collect()
427});
428
429pub fn trusted_relay_urls() -> Vec<RelayUrl> {
431 TRUSTED_RELAY_URLS.clone()
432}
433
434pub async fn send_event_first_ok(
444 client: &Client,
445 urls: Vec<RelayUrl>,
446 event: &Event,
447) -> Result<Output<EventId>, nostr_sdk::client::Error> {
448 let pool = client.pool();
449 let relays = pool.relays().await;
450 let event_id = event.id;
451
452 let mut resolved: Vec<(RelayUrl, Relay)> = Vec::new();
454 for url in urls {
455 if let Some(relay) = relays.get(&url) {
456 resolved.push((url, relay.clone()));
457 }
458 }
459
460 if resolved.is_empty() {
461 return client.send_event(event).await;
462 }
463
464 let handles = spawn_tracked_publish(resolved, event.clone());
468
469 let mut output = Output {
471 val: event_id,
472 success: std::collections::HashSet::new(),
473 failed: HashMap::new(),
474 };
475
476 let mut remaining = handles;
477 while !remaining.is_empty() {
478 let (result, _index, rest) = futures_util::future::select_all(remaining).await;
479 remaining = rest;
480
481 if let Ok((url, relay_result)) = result {
482 match relay_result {
483 Ok(_) => {
484 output.success.insert(url);
485 drop(remaining);
489 return Ok(output);
490 }
491 Err(e) => {
492 output.failed.insert(url, e);
493 }
494 }
495 }
496 }
497
498 Ok(output)
500}
501
502pub async fn send_event_pool_first_ok(
505 client: &Client,
506 event: &Event,
507) -> Result<Output<EventId>, nostr_sdk::client::Error> {
508 let pool = client.pool();
509 let relays = pool.relays().await;
510 let write_urls: Vec<RelayUrl> = relays
511 .iter()
512 .filter(|(_, r)| r.flags().has_write())
513 .map(|(url, _)| url.clone())
514 .collect();
515 send_event_first_ok(&client, write_urls, event).await
516}
517
518pub fn wrap_with_retained_key(
528 receiver: &PublicKey,
529 seal: &Event,
530 extra_tags: impl IntoIterator<Item = Tag>,
531) -> Result<(Event, SecretKey), String> {
532 use nostr_sdk::nips::nip44;
533 use nostr_sdk::nips::nip59::RANGE_RANDOM_TIMESTAMP_TWEAK;
534
535 if seal.kind != Kind::Seal {
536 return Err(format!("expected Seal kind, got {:?}", seal.kind));
537 }
538 let keys = Keys::generate();
539 let secret = keys.secret_key().clone();
540 let content = nip44::encrypt(
541 keys.secret_key(),
542 receiver,
543 seal.as_json(),
544 nip44::Version::default(),
545 )
546 .map_err(|e| format!("nip44 encrypt: {}", e))?;
547 let mut tags: Vec<Tag> = extra_tags.into_iter().collect();
548 tags.push(Tag::public_key(*receiver));
549 let event = EventBuilder::new(Kind::GiftWrap, content)
550 .tags(tags)
551 .custom_created_at(Timestamp::tweaked(RANGE_RANDOM_TIMESTAMP_TWEAK))
552 .sign_with_keys(&keys)
553 .map_err(|e| format!("sign wrap: {}", e))?;
554 Ok((event, secret))
555}
556
557pub struct GiftWrapSendOutcome {
561 pub output: Output<EventId>,
562 pub wrap_event_id: EventId,
563 pub wrap_secret: SecretKey,
564 pub targeted_relays: Vec<String>,
567}
568
569pub async fn send_gift_wrap_retained(
580 client: &Client,
581 recipient: &PublicKey,
582 rumor: UnsignedEvent,
583 extra_tags: impl IntoIterator<Item = Tag>,
584) -> Result<GiftWrapSendOutcome, String> {
585 let signer = client.signer().await.map_err(|e| e.to_string())?;
586 let seal: Event = EventBuilder::seal(&signer, recipient, rumor)
587 .await
588 .map_err(|e| e.to_string())?
589 .sign(&signer)
590 .await
591 .map_err(|e| e.to_string())?;
592 let (event, secret) = wrap_with_retained_key(recipient, &seal, extra_tags)?;
593 let wrap_event_id = event.id;
594
595 let inbox_strs = get_or_fetch_inbox_relays(client, recipient).await;
598 let targeted_strs: Vec<String> = if !inbox_strs.is_empty() {
599 inbox_strs.clone()
600 } else {
601 let pool = client.pool();
602 let relays = pool.relays().await;
603 relays.iter()
604 .filter(|(_, r)| r.flags().has_write())
605 .map(|(url, _)| url.to_string())
606 .collect()
607 };
608 fn normalize_url_for_match(s: &str) -> String {
615 s.trim_end_matches('/').to_ascii_lowercase()
616 }
617 let pool = client.pool();
618 let pool_relays = pool.relays().await;
619 let pool_norm: Vec<(String, RelayUrl, Relay)> = pool_relays.iter()
620 .map(|(url, relay)| (
621 normalize_url_for_match(&url.to_string()),
622 url.clone(),
623 relay.clone(),
624 ))
625 .collect();
626 let mut resolved: Vec<(RelayUrl, Relay)> = targeted_strs
627 .iter()
628 .filter_map(|s| {
629 let norm = normalize_url_for_match(s);
630 pool_norm.iter()
631 .find(|(pnorm, _, _)| pnorm == &norm)
632 .map(|(_, url, relay)| (url.clone(), relay.clone()))
633 })
634 .collect();
635
636 let mut transient_added: Vec<RelayUrl> = Vec::new();
642 if !inbox_strs.is_empty() {
643 for s in &targeted_strs {
644 let norm = normalize_url_for_match(s);
645 let in_pool = pool_norm.iter().any(|(p, _, _)| p == &norm);
646 let already_added = transient_added.iter()
647 .any(|u| normalize_url_for_match(&u.to_string()) == norm);
648 if in_pool || already_added { continue; }
649
650 let opts = crate::tor_aware_relay_options(RelayOptions::new().reconnect(false));
651 if pool.add_relay(s.as_str(), opts).await.is_ok() {
652 if let Ok(relay) = pool.relay(s.as_str()).await {
653 let _ = relay.try_connect(std::time::Duration::from_secs(6)).await;
654 transient_added.push(relay.url().clone());
655 resolved.push((relay.url().clone(), relay));
656 }
657 }
658 }
659 if !transient_added.is_empty() {
660 crate::log_info!(
661 "[InboxRelays] on-demand connected {} inbox relay(s) for {} (transient)",
662 transient_added.len(),
663 recipient,
664 );
665 }
666 }
667
668 if resolved.is_empty() {
671 let output = client
674 .send_event(&event)
675 .await
676 .map_err(|e| e.to_string())?;
677 return Ok(GiftWrapSendOutcome {
678 output,
679 wrap_event_id,
680 wrap_secret: secret,
681 targeted_relays: targeted_strs,
682 });
683 }
684
685 if !inbox_strs.is_empty() {
686 println!(
687 "[InboxRelays] Routing gift-wrap to {} inbox relays for {}",
688 resolved.len(),
689 recipient
690 );
691 }
692
693 let handles = spawn_tracked_publish(resolved, event.clone());
701
702 let mut output = Output {
707 val: wrap_event_id,
708 success: HashSet::new(),
709 failed: HashMap::new(),
710 };
711 let mut remaining = handles;
712 while !remaining.is_empty() {
713 let (result, _idx, rest) = futures_util::future::select_all(remaining).await;
714 remaining = rest;
715 if let Ok((url, relay_result)) = result {
716 match relay_result {
717 Ok(_) => {
718 output.success.insert(url);
719 drop(remaining);
720 break;
721 }
722 Err(e) => {
723 output.failed.insert(url, e.to_string());
724 }
725 }
726 }
727 }
728
729 for url in &transient_added {
734 let _ = pool.remove_relay(url).await;
735 }
736
737 Ok(GiftWrapSendOutcome {
738 output,
739 wrap_event_id,
740 wrap_secret: secret,
741 targeted_relays: targeted_strs,
742 })
743}
744
745pub async fn send_gift_wrap(
758 client: &Client,
759 recipient: &PublicKey,
760 rumor: UnsignedEvent,
761 extra_tags: impl IntoIterator<Item = Tag>,
762) -> Result<Output<EventId>, String> {
763 let outcome = send_gift_wrap_retained(client, recipient, rumor, extra_tags).await?;
764 Ok(outcome.output)
765}
766
767pub async fn publish_inbox_relays(client: &Client) -> Result<(), String> {
775 let relays: Vec<String> = client
778 .pool()
779 .relays()
780 .await
781 .iter()
782 .filter(|(_, relay)| relay.flags().has_read())
783 .map(|(url, _)| url.to_string())
784 .collect();
785
786 let mut builder = EventBuilder::new(Kind::Custom(10050), "");
789 for url in &relays {
790 builder = builder.tag(Tag::custom(TagKind::custom("relay"), vec![url.clone()]));
791 }
792
793 client
794 .send_event_builder(builder)
795 .await
796 .map_err(|e| format!("Failed to publish inbox relays: {}", e))?;
797
798 println!(
799 "[InboxRelays] Published kind 10050 with {} relay(s)",
800 relays.len()
801 );
802 Ok(())
803}
804
805static REPUBLISH_GEN: AtomicU64 = AtomicU64::new(0);
808
809#[cfg(test)]
811static DEBOUNCE_PASS_COUNT: AtomicU64 = AtomicU64::new(0);
812
813pub fn republish_inbox_relays_debounced() {
817 let gen = REPUBLISH_GEN.fetch_add(1, Ordering::SeqCst) + 1;
818 let session = crate::state::SessionGuard::capture();
823 tokio::spawn(async move {
824 tokio::time::sleep(std::time::Duration::from_millis(800)).await;
827 if REPUBLISH_GEN.load(Ordering::SeqCst) != gen {
828 return; }
830 if !session.is_valid() {
831 return; }
833 #[cfg(test)]
834 DEBOUNCE_PASS_COUNT.fetch_add(1, Ordering::SeqCst);
835 let client = match nostr_client() {
836 Some(c) => c,
837 None => return,
838 };
839 if let Err(e) = publish_inbox_relays(&client).await {
840 eprintln!("[InboxRelays] Failed to republish after config change: {}", e);
841 }
842 });
843}
844
845#[cfg(test)]
846mod tests {
847 use super::*;
848
849 #[test]
852 fn parse_relay_tags_extracts_urls() {
853 let tags = Tags::from_list(vec![
854 Tag::custom(TagKind::custom("relay"), vec!["wss://relay.example.com"]),
855 Tag::custom(TagKind::custom("relay"), vec!["wss://other.example.com"]),
856 ]);
857 let result = parse_relay_tags(&tags);
858 assert_eq!(result, vec![
859 "wss://relay.example.com".to_string(),
860 "wss://other.example.com".to_string(),
861 ]);
862 }
863
864 #[test]
865 fn parse_relay_tags_ignores_non_relay_tags() {
866 let tags = Tags::from_list(vec![
867 Tag::custom(TagKind::custom("relay"), vec!["wss://good.example.com"]),
868 Tag::custom(TagKind::custom("p"), vec!["deadbeef"]),
869 Tag::custom(TagKind::custom("e"), vec!["cafebabe"]),
870 ]);
871 let result = parse_relay_tags(&tags);
872 assert_eq!(result, vec!["wss://good.example.com".to_string()]);
873 }
874
875 #[test]
876 fn parse_relay_tags_empty() {
877 let tags = Tags::new();
878 let result = parse_relay_tags(&tags);
879 assert!(result.is_empty());
880 }
881
882 #[test]
883 fn parse_relay_tags_ignores_relay_tag_without_value() {
884 let tags = Tags::from_list(vec![
886 Tag::custom(TagKind::custom("relay"), Vec::<String>::new()),
887 ]);
888 let result = parse_relay_tags(&tags);
889 assert!(result.is_empty());
890 }
891
892 fn test_pubkey() -> PublicKey {
895 let keys = Keys::generate();
896 keys.public_key()
897 }
898
899 static TEST_GLOBALS_LOCK: LazyLock<tokio::sync::Mutex<()>> =
901 LazyLock::new(|| tokio::sync::Mutex::new(()));
902
903 #[test]
904 fn cache_stores_and_retrieves() {
905 let _guard = TEST_GLOBALS_LOCK.blocking_lock();
906 let pk = test_pubkey();
907 let relays = vec!["wss://a.example.com".to_string()];
908
909 {
910 let mut cache = INBOX_RELAY_CACHE.lock().unwrap();
911 cache.insert(pk, CachedRelays {
912 relays: relays.clone(),
913 fetched_at: Instant::now(),
914 fetch_ok: true,
915 });
916 }
917
918 let cache = INBOX_RELAY_CACHE.lock().unwrap();
919 let entry = cache.get(&pk).unwrap();
920 assert_eq!(entry.relays, relays);
921 assert!(entry.fetch_ok);
922 assert!(entry.fetched_at.elapsed().as_secs() < CACHE_TTL_SECS);
923 }
924
925 #[test]
926 fn cache_expires_after_ttl() {
927 let _guard = TEST_GLOBALS_LOCK.blocking_lock();
928 let pk = test_pubkey();
929
930 {
931 let mut cache = INBOX_RELAY_CACHE.lock().unwrap();
932 cache.insert(pk, CachedRelays {
933 relays: vec!["wss://stale.example.com".to_string()],
934 fetched_at: Instant::now() - std::time::Duration::from_secs(CACHE_TTL_SECS + 1),
935 fetch_ok: true,
936 });
937 }
938
939 let cache = INBOX_RELAY_CACHE.lock().unwrap();
940 let entry = cache.get(&pk).unwrap();
941 assert!(entry.fetched_at.elapsed().as_secs() >= CACHE_TTL_SECS);
942 }
943
944 #[test]
945 fn cache_stores_empty_results() {
946 let _guard = TEST_GLOBALS_LOCK.blocking_lock();
947 let pk = test_pubkey();
948
949 {
950 let mut cache = INBOX_RELAY_CACHE.lock().unwrap();
951 cache.insert(pk, CachedRelays {
952 relays: vec![],
953 fetched_at: Instant::now(),
954 fetch_ok: true,
955 });
956 }
957
958 let cache = INBOX_RELAY_CACHE.lock().unwrap();
959 let entry = cache.get(&pk).unwrap();
960 assert!(entry.relays.is_empty());
961 assert!(entry.fetch_ok);
962 assert!(entry.fetched_at.elapsed().as_secs() < CACHE_TTL_SECS);
963 }
964
965 #[test]
966 fn cache_error_uses_short_ttl() {
967 let _guard = TEST_GLOBALS_LOCK.blocking_lock();
968 let pk = test_pubkey();
969
970 {
971 let mut cache = INBOX_RELAY_CACHE.lock().unwrap();
972 cache.insert(pk, CachedRelays {
973 relays: vec![],
974 fetched_at: Instant::now() - std::time::Duration::from_secs(120),
976 fetch_ok: false,
977 });
978 }
979
980 let cache = INBOX_RELAY_CACHE.lock().unwrap();
981 let entry = cache.get(&pk).unwrap();
982 assert!(!entry.fetch_ok);
983 assert!(entry.fetched_at.elapsed().as_secs() >= CACHE_TTL_ERROR_SECS);
985 assert!(entry.fetched_at.elapsed().as_secs() < CACHE_TTL_SECS);
987 }
988
989 #[tokio::test]
992 async fn concurrent_fetches_for_same_pubkey_serialize() {
993 let _guard = TEST_GLOBALS_LOCK.lock().await;
994 let pk = test_pubkey();
995
996 {
998 let mut cache = INBOX_RELAY_CACHE.lock().unwrap();
999 cache.remove(&pk);
1000 }
1001
1002 let fetch_counter = Arc::new(AtomicU64::new(0));
1003
1004 let mut handles = vec![];
1007 for _ in 0..10 {
1008 let counter = fetch_counter.clone();
1009 let handle = tokio::spawn(async move {
1010 get_or_fetch_with_lock(&pk, || async {
1011 counter.fetch_add(1, Ordering::SeqCst);
1012 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1014 FetchResult {
1015 relays: vec!["wss://test.example.com".to_string()],
1016 fetch_ok: true,
1017 }
1018 })
1019 .await
1020 });
1021 handles.push(handle);
1022 }
1023
1024 let results = futures_util::future::join_all(handles).await;
1026
1027 for result in &results {
1029 assert!(result.is_ok());
1030 let relays = result.as_ref().unwrap();
1031 assert_eq!(relays, &vec!["wss://test.example.com".to_string()]);
1032 }
1033
1034 assert_eq!(
1036 fetch_counter.load(Ordering::SeqCst),
1037 1,
1038 "Expected exactly 1 fetch for 10 concurrent requests to same pubkey"
1039 );
1040
1041 let locks_after = {
1042 let locks = FETCH_LOCKS.lock().unwrap();
1043 locks.len()
1044 };
1045 assert_eq!(locks_after, 0, "Lock entry should be removed after all waiters complete");
1046 }
1047
1048 #[tokio::test]
1049 async fn fetch_locks_do_not_accumulate_after_calls_complete() {
1050 let _guard = TEST_GLOBALS_LOCK.lock().await;
1051
1052 let pk1 = test_pubkey();
1056 let pk2 = test_pubkey();
1057 let pk3 = test_pubkey();
1058
1059 {
1061 let mut cache = INBOX_RELAY_CACHE.lock().unwrap();
1062 cache.clear();
1063 }
1064 {
1065 let mut locks = FETCH_LOCKS.lock().unwrap();
1066 locks.clear();
1067 }
1068
1069 get_or_fetch_with_lock(&pk1, || async {
1071 FetchResult {
1072 relays: vec!["wss://relay1.example.com".to_string()],
1073 fetch_ok: true,
1074 }
1075 })
1076 .await;
1077
1078 let locks_after_pk1 = {
1081 let locks = FETCH_LOCKS.lock().unwrap();
1082 locks.len()
1083 };
1084 assert_eq!(locks_after_pk1, 0, "No lock entries should remain after pk1 call");
1085
1086 get_or_fetch_with_lock(&pk2, || async {
1088 FetchResult {
1089 relays: vec!["wss://relay2.example.com".to_string()],
1090 fetch_ok: true,
1091 }
1092 })
1093 .await;
1094
1095 let locks_after_pk2 = {
1096 let locks = FETCH_LOCKS.lock().unwrap();
1097 locks.len()
1098 };
1099 assert_eq!(locks_after_pk2, 0, "No lock entries should remain after pk2 call");
1100
1101 get_or_fetch_with_lock(&pk3, || async {
1103 FetchResult {
1104 relays: vec!["wss://relay3.example.com".to_string()],
1105 fetch_ok: true,
1106 }
1107 })
1108 .await;
1109
1110 let locks_after_pk3 = {
1111 let locks = FETCH_LOCKS.lock().unwrap();
1112 locks.len()
1113 };
1114 assert_eq!(locks_after_pk3, 0, "No lock entries should remain after pk3 call");
1115 }
1116
1117 #[tokio::test]
1118 async fn cancelled_fetch_cleans_up_lock_entry() {
1119 let _guard = TEST_GLOBALS_LOCK.lock().await;
1120 let pk = test_pubkey();
1121
1122 {
1123 let mut cache = INBOX_RELAY_CACHE.lock().unwrap();
1124 cache.clear();
1125 }
1126 {
1127 let mut locks = FETCH_LOCKS.lock().unwrap();
1128 locks.clear();
1129 }
1130
1131 let (started_tx, started_rx) = tokio::sync::oneshot::channel::<()>();
1132 let task_pk = pk;
1133 let handle = tokio::spawn(async move {
1134 get_or_fetch_with_lock(&task_pk, || async move {
1135 let _ = started_tx.send(());
1136 tokio::time::sleep(std::time::Duration::from_secs(30)).await;
1137 FetchResult { relays: Vec::new(), fetch_ok: false }
1138 })
1139 .await
1140 });
1141
1142 started_rx.await.expect("fetch closure should start before abort");
1143 handle.abort();
1144 let _ = handle.await;
1145 tokio::task::yield_now().await;
1146
1147 let locks_after = {
1148 let locks = FETCH_LOCKS.lock().unwrap();
1149 locks.len()
1150 };
1151 assert_eq!(
1152 locks_after, 0,
1153 "Lock entry should be removed even if fetch task is cancelled"
1154 );
1155 }
1156
1157 #[tokio::test(start_paused = true)]
1164 async fn debounce_coalesces_rapid_calls_into_one() {
1165 let gen_before = REPUBLISH_GEN.load(Ordering::SeqCst);
1167 let pass_before = DEBOUNCE_PASS_COUNT.load(Ordering::SeqCst);
1168
1169 republish_inbox_relays_debounced();
1171 republish_inbox_relays_debounced();
1172 republish_inbox_relays_debounced();
1173
1174 let gen_after = REPUBLISH_GEN.load(Ordering::SeqCst);
1175 assert_eq!(gen_after, gen_before + 3);
1176
1177 tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
1179
1180 let pass_after = DEBOUNCE_PASS_COUNT.load(Ordering::SeqCst);
1181 assert_eq!(pass_after - pass_before, 1);
1185 }
1186}