1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::path::PathBuf;
3use std::sync::atomic::{AtomicUsize, Ordering};
4use std::sync::Arc;
5use std::sync::Mutex;
6use std::time::Duration;
7use std::time::Instant;
8
9use anyhow::{Context, Result};
10use hashtree_core::{nhash_encode_full, NHashData};
11use hashtree_nostr::{
12 CrawlConfig, CrawlReport, ListEventsOptions, NostrBridge, NostrEventStore, RelayFetchMode,
13 HASHTREE_ROOT_KIND,
14};
15use nostr::{
16 Alphabet, Event, EventBuilder, Filter, Kind, PublicKey, SingleLetterTag, Tag, TagKind,
17 Timestamp,
18};
19use nostr_sdk::{
20 pool::RelayLimits, prelude::RelayPoolNotification, Client, ClientOptions, Keys, RelayStatus,
21};
22use tokio::sync::{watch, Mutex as AsyncMutex};
23use tracing::{debug, info, warn};
24
25use crate::blossom_push::background_blossom_push_incremental_with_store;
26use crate::socialgraph::crawler::SOCIALGRAPH_RELAY_EVENT_MAX_SIZE;
27use crate::socialgraph::{self, SocialGraphBackend, SocialGraphStore};
28use crate::HashtreeStore;
29
30#[cfg(not(test))]
31const MIRROR_STARTUP_DELAY: Duration = Duration::from_secs(8);
32#[cfg(test)]
33const MIRROR_STARTUP_DELAY: Duration = Duration::from_millis(50);
34
35#[cfg(not(test))]
36const MIRROR_CONNECT_SETTLE_DELAY: Duration = Duration::from_secs(1);
37#[cfg(test)]
38const MIRROR_CONNECT_SETTLE_DELAY: Duration = Duration::from_millis(250);
39
40#[cfg(not(test))]
41const MIRROR_AUTHOR_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
42#[cfg(test)]
43const MIRROR_AUTHOR_REFRESH_INTERVAL: Duration = Duration::from_millis(100);
44
45#[cfg(not(test))]
46const MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN: Duration = Duration::from_secs(30);
47#[cfg(test)]
48const MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN: Duration = Duration::from_millis(100);
49
50const KIND_LONG_FORM_CONTENT: u16 = 30_023;
51const DEFAULT_HISTORY_KINDS: [u16; 7] = [0, 1, 3, 6, 7, 9735, KIND_LONG_FORM_CONTENT];
52const DEFAULT_EVENT_TREE_NAME: &str = "nostr-event-index";
53const DEFAULT_PROFILE_SEARCH_TREE_NAME: &str = "profile-search";
54const DEFAULT_PROFILES_BY_PUBKEY_TREE_NAME: &str = "profiles-by-pubkey";
55const MIRROR_UPLOAD_STATE_DIR: &str = "nostr-mirror";
56const MIRROR_UPLOADED_ROOT_SUFFIX: &str = ".uploaded-root";
57const NOSTR_INDEX_STATE_DIR: &str = "nostr-index";
58const NOSTR_INDEX_LATEST_ROOT_FILE: &str = "latest-root.txt";
59const METADATA_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT: usize = 1;
60const METADATA_HISTORY_SYNC_AUTHOR_BATCH_SIZE: usize = 64;
61const DEFAULT_FULL_TEXT_NOTE_HISTORY_FOLLOW_DISTANCE: u32 = 2;
62const DEFAULT_FULL_TEXT_NOTE_HISTORY_MAX_RELAY_PAGES: usize = 0;
63const FULL_TEXT_HISTORY_PRIORITY_MAX_DISTANCE: u32 = 1;
64const FULL_TEXT_HISTORY_PRIORITY_SAMPLE_LIMIT: usize = 32;
65const LARGE_HISTORY_SYNC_AUTHOR_MULTIPLIER: usize = 8;
66const LARGE_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT: usize = 16;
67const LARGE_HISTORY_SYNC_MAX_RELAY_PAGES: usize = 20;
68
69#[cfg(not(test))]
70const MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL: Duration = Duration::from_secs(300);
71#[cfg(test)]
72const MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL: Duration = Duration::from_millis(100);
73
74#[cfg(not(test))]
75const MIRROR_ROOT_PUBLISH_DEBOUNCE: Duration = Duration::from_secs(5);
76#[cfg(test)]
77const MIRROR_ROOT_PUBLISH_DEBOUNCE: Duration = Duration::from_millis(20);
78
79#[cfg(not(test))]
80const MIRROR_ROOT_PUBLISH_MAX_STALENESS: Duration = Duration::from_secs(30);
81#[cfg(test)]
82const MIRROR_ROOT_PUBLISH_MAX_STALENESS: Duration = Duration::from_millis(100);
83
84#[cfg(not(test))]
85const MIRROR_ROOT_UPLOAD_RETRY_INTERVAL: Duration = Duration::from_secs(60);
86#[cfg(test)]
87const MIRROR_ROOT_UPLOAD_RETRY_INTERVAL: Duration = Duration::from_millis(100);
88
89#[cfg(not(test))]
90const MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT: Duration = Duration::from_secs(2);
91#[cfg(test)]
92const MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT: Duration = Duration::from_millis(250);
93
94#[cfg(not(test))]
95const MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT: Duration = Duration::from_secs(5);
96#[cfg(test)]
97const MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT: Duration = Duration::from_secs(2);
98
99const MISSING_LOCAL_BLOB_PUSH_ERROR: &str = "missing local blob";
100
101fn trim_transient_allocations() {
102 #[cfg(all(target_os = "linux", target_env = "gnu"))]
103 unsafe {
104 libc::malloc_trim(0);
108 }
109}
110
111struct RootPublicationDeferral<'a> {
112 count: &'a AtomicUsize,
113}
114
115impl<'a> RootPublicationDeferral<'a> {
116 fn new(count: &'a AtomicUsize) -> Self {
117 count.fetch_add(1, Ordering::AcqRel);
118 Self { count }
119 }
120}
121
122impl Drop for RootPublicationDeferral<'_> {
123 fn drop(&mut self) {
124 self.count.fetch_sub(1, Ordering::AcqRel);
125 }
126}
127
128fn decode_hex_pubkey(value: &str) -> Option<[u8; 32]> {
129 let bytes = hex::decode(value).ok()?;
130 <[u8; 32]>::try_from(bytes.as_slice()).ok()
131}
132
133#[derive(Debug, Clone)]
134pub struct NostrMirrorConfig {
135 pub relays: Vec<String>,
136 pub publish_relays: Vec<String>,
137 pub blossom_write_servers: Vec<String>,
138 pub max_follow_distance: u32,
139 pub overmute_threshold: f64,
140 pub author_batch_size: usize,
141 pub history_sync_author_chunk_size: usize,
142 pub history_sync_per_author_event_limit: usize,
143 pub missing_profile_backfill_batch_size: usize,
144 pub fetch_timeout: Duration,
145 pub relay_event_max_size: Option<u32>,
146 pub require_negentropy: bool,
147 pub kinds: Vec<u16>,
148 pub history_sync_on_start: bool,
149 pub history_sync_on_reconnect: bool,
150 pub full_text_note_history_follow_distance: Option<u32>,
151 pub full_text_note_history_max_relay_pages: usize,
152 pub published_event_tree_name: Option<String>,
153 pub published_profile_search_tree_name: Option<String>,
154 pub published_profiles_by_pubkey_tree_name: Option<String>,
155}
156
157impl Default for NostrMirrorConfig {
158 fn default() -> Self {
159 Self {
160 relays: Vec::new(),
161 publish_relays: Vec::new(),
162 blossom_write_servers: Vec::new(),
163 max_follow_distance: 2,
164 overmute_threshold: 1.0,
165 author_batch_size: 256,
166 history_sync_author_chunk_size: 5_000,
167 history_sync_per_author_event_limit: 256,
168 missing_profile_backfill_batch_size: 5_000,
169 fetch_timeout: Duration::from_secs(15),
170 relay_event_max_size: Some(SOCIALGRAPH_RELAY_EVENT_MAX_SIZE),
171 require_negentropy: false,
172 kinds: DEFAULT_HISTORY_KINDS.to_vec(),
173 history_sync_on_start: true,
174 history_sync_on_reconnect: true,
175 full_text_note_history_follow_distance: Some(
176 DEFAULT_FULL_TEXT_NOTE_HISTORY_FOLLOW_DISTANCE,
177 ),
178 full_text_note_history_max_relay_pages: DEFAULT_FULL_TEXT_NOTE_HISTORY_MAX_RELAY_PAGES,
179 published_event_tree_name: Some(DEFAULT_EVENT_TREE_NAME.to_string()),
180 published_profile_search_tree_name: Some(DEFAULT_PROFILE_SEARCH_TREE_NAME.to_string()),
181 published_profiles_by_pubkey_tree_name: Some(
182 DEFAULT_PROFILES_BY_PUBKEY_TREE_NAME.to_string(),
183 ),
184 }
185 }
186}
187
188#[derive(Debug, Default)]
189struct RootPublishState {
190 pending_root: Option<hashtree_core::Cid>,
191 last_changed_at: Option<Instant>,
192 dirty_since: Option<Instant>,
193 last_published_root: Option<hashtree_core::Cid>,
194 last_published_at: Option<Instant>,
195 last_published_created_at: Option<Timestamp>,
196 last_uploaded_root: Option<hashtree_core::Cid>,
197 last_uploaded_at: Option<Instant>,
198 upload_in_progress_root: Option<hashtree_core::Cid>,
199 last_upload_failed_at: Option<Instant>,
200 last_upload_error: Option<String>,
201 missing_blob_rebuild_required: bool,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205struct HistorySyncPlan {
206 relay_fetch_mode: RelayFetchMode,
207 author_batch_size: usize,
208 per_author_event_limit: usize,
209 relay_page_size: usize,
210 max_relay_pages: usize,
211}
212
213pub struct BackgroundNostrMirror {
214 config: NostrMirrorConfig,
215 store: Arc<HashtreeStore>,
216 graph_store: Arc<SocialGraphStore>,
217 client: Client,
218 publish_client: Option<Client>,
219 event_publish_state: Arc<Mutex<RootPublishState>>,
220 profile_search_publish_state: Arc<Mutex<RootPublishState>>,
221 profiles_by_pubkey_publish_state: Arc<Mutex<RootPublishState>>,
222 pending_live_events: Mutex<BTreeMap<String, Event>>,
223 missing_profile_cursor: Mutex<usize>,
224 history_sync_lock: AsyncMutex<()>,
225 root_publication_deferrals: AtomicUsize,
226 shutdown_tx: watch::Sender<bool>,
227 shutdown_rx: watch::Receiver<bool>,
228}
229
230impl BackgroundNostrMirror {
231 pub async fn new(
232 config: NostrMirrorConfig,
233 store: Arc<HashtreeStore>,
234 graph_store: Arc<SocialGraphStore>,
235 publish_keys: Option<Keys>,
236 ) -> Result<Self> {
237 let client = if let Some(max_size) = config.relay_event_max_size {
238 let mut limits = RelayLimits::default();
239 limits.events.max_size = Some(max_size);
240 Client::builder()
241 .signer(Keys::generate())
242 .opts(ClientOptions::new().relay_limits(limits))
243 .build()
244 } else {
245 Client::new(Keys::generate())
246 };
247 for relay in &config.relays {
248 client
249 .add_relay(relay)
250 .await
251 .with_context(|| format!("add mirror relay {relay}"))?;
252 }
253 client.connect().await;
254
255 let publish_client = if let Some(keys) = publish_keys {
256 if config.publish_relays.is_empty() {
257 None
258 } else {
259 let client = Client::new(keys);
260 for relay in &config.publish_relays {
261 client
262 .add_relay(relay)
263 .await
264 .with_context(|| format!("add mirror publish relay {relay}"))?;
265 }
266 client.connect().await;
267 Some(client)
268 }
269 } else {
270 None
271 };
272
273 let (shutdown_tx, shutdown_rx) = watch::channel(false);
274 let event_publish_state = Self::root_publish_state_from_disk(
275 store.base_path(),
276 config.published_event_tree_name.as_deref(),
277 "event root",
278 );
279 let profile_search_publish_state = Self::root_publish_state_from_disk(
280 store.base_path(),
281 config.published_profile_search_tree_name.as_deref(),
282 "profile-search root",
283 );
284 let profiles_by_pubkey_publish_state = Self::root_publish_state_from_disk(
285 store.base_path(),
286 config.published_profiles_by_pubkey_tree_name.as_deref(),
287 "profiles-by-pubkey root",
288 );
289 Ok(Self {
290 config,
291 store,
292 graph_store,
293 client,
294 publish_client,
295 event_publish_state: Arc::new(Mutex::new(event_publish_state)),
296 profile_search_publish_state: Arc::new(Mutex::new(profile_search_publish_state)),
297 profiles_by_pubkey_publish_state: Arc::new(Mutex::new(
298 profiles_by_pubkey_publish_state,
299 )),
300 pending_live_events: Mutex::new(BTreeMap::new()),
301 missing_profile_cursor: Mutex::new(0),
302 history_sync_lock: AsyncMutex::new(()),
303 root_publication_deferrals: AtomicUsize::new(0),
304 shutdown_tx,
305 shutdown_rx,
306 })
307 }
308
309 fn root_publish_state_from_disk(
310 base_path: &std::path::Path,
311 tree_name: Option<&str>,
312 log_label: &str,
313 ) -> RootPublishState {
314 let Some(tree_name) = tree_name else {
315 return RootPublishState::default();
316 };
317 let path = Self::uploaded_root_state_path(base_path, tree_name);
318 let Some(root) = Self::read_uploaded_root_state(&path, log_label) else {
319 return RootPublishState::default();
320 };
321
322 RootPublishState {
323 last_uploaded_root: Some(root),
324 ..RootPublishState::default()
325 }
326 }
327
328 fn uploaded_root_state_path(base_path: &std::path::Path, tree_name: &str) -> PathBuf {
329 let safe_tree_name = tree_name
330 .chars()
331 .map(|ch| {
332 if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' {
333 ch
334 } else {
335 '_'
336 }
337 })
338 .collect::<String>();
339 base_path
340 .join(MIRROR_UPLOAD_STATE_DIR)
341 .join(format!("{safe_tree_name}{MIRROR_UPLOADED_ROOT_SUFFIX}"))
342 }
343
344 fn read_uploaded_root_state(
345 path: &std::path::Path,
346 log_label: &str,
347 ) -> Option<hashtree_core::Cid> {
348 let raw = match std::fs::read_to_string(path) {
349 Ok(raw) => raw,
350 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
351 Err(err) => {
352 warn!(
353 "Nostr mirror failed to read uploaded {} state {}: {}",
354 log_label,
355 path.display(),
356 err
357 );
358 return None;
359 }
360 };
361 let trimmed = raw.trim();
362 if trimmed.is_empty() {
363 return None;
364 }
365 match hashtree_core::Cid::parse(trimmed) {
366 Ok(root) => Some(root),
367 Err(err) => {
368 warn!(
369 "Nostr mirror ignored invalid uploaded {} state {}: {}",
370 log_label,
371 path.display(),
372 err
373 );
374 None
375 }
376 }
377 }
378
379 fn write_uploaded_root_state(
380 path: &std::path::Path,
381 root: &hashtree_core::Cid,
382 log_label: &str,
383 ) -> Result<()> {
384 if let Some(parent) = path.parent() {
385 std::fs::create_dir_all(parent)
386 .with_context(|| format!("create {}", parent.display()))?;
387 }
388 std::fs::write(path, format!("{root}\n"))
389 .with_context(|| format!("write uploaded {log_label} state {}", path.display()))
390 }
391
392 fn write_latest_event_root_state(
393 base_path: &std::path::Path,
394 root: Option<&hashtree_core::Cid>,
395 ) -> Result<()> {
396 let path = base_path
397 .join(NOSTR_INDEX_STATE_DIR)
398 .join(NOSTR_INDEX_LATEST_ROOT_FILE);
399 let Some(root) = root else {
400 if path.exists() {
401 std::fs::remove_file(&path)
402 .with_context(|| format!("remove {}", path.display()))?;
403 }
404 return Ok(());
405 };
406 if let Some(parent) = path.parent() {
407 std::fs::create_dir_all(parent)
408 .with_context(|| format!("create {}", parent.display()))?;
409 }
410 let encoded = nhash_encode_full(&NHashData {
411 hash: root.hash,
412 decrypt_key: root.key,
413 })
414 .context("encode mirrored Nostr index root")?;
415 let tmp_path = path.with_extension("tmp");
416 std::fs::write(&tmp_path, format!("{encoded}\n"))
417 .with_context(|| format!("write {}", tmp_path.display()))?;
418 #[cfg(windows)]
419 if path.exists() {
420 std::fs::remove_file(&path)
421 .with_context(|| format!("remove stale {}", path.display()))?;
422 }
423 std::fs::rename(&tmp_path, &path).with_context(|| format!("replace {}", path.display()))?;
424 Ok(())
425 }
426
427 pub fn shutdown(&self) {
428 let _ = self.shutdown_tx.send(true);
429 }
430
431 fn sync_publish_roots_from_store(&self) -> Result<()> {
432 self.note_public_events_root_change()?;
433 self.note_profile_search_root_change()?;
434 self.note_profiles_by_pubkey_root_change()?;
435 Ok(())
436 }
437
438 async fn publish_pending_roots(
439 &self,
440 force_event: bool,
441 force_profile_search: bool,
442 force_profiles_by_pubkey: bool,
443 ) -> (Result<()>, Result<()>, Result<()>) {
444 tokio::join!(
445 self.maybe_publish_event_root(force_event),
446 self.maybe_publish_profile_search_root(force_profile_search),
447 self.maybe_publish_profiles_by_pubkey_root(force_profiles_by_pubkey),
448 )
449 }
450
451 async fn publish_priority_roots(
452 &self,
453 force_event: bool,
454 force_profile_search: bool,
455 force_profiles_by_pubkey: bool,
456 ) -> (Result<()>, Result<()>, Result<()>) {
457 let (profile_search_result, profiles_by_pubkey_result) = tokio::join!(
458 async {
459 if force_profile_search {
460 self.maybe_publish_profile_search_root(true).await
461 } else {
462 Ok(())
463 }
464 },
465 async {
466 if force_profiles_by_pubkey {
467 self.maybe_publish_profiles_by_pubkey_root(true).await
468 } else {
469 Ok(())
470 }
471 },
472 );
473 let event_result = if force_event {
474 self.maybe_publish_event_root(true).await
475 } else {
476 Ok(())
477 };
478 (
479 event_result,
480 profile_search_result,
481 profiles_by_pubkey_result,
482 )
483 }
484
485 pub async fn run(self: Arc<Self>) -> Result<()> {
486 if self.config.relays.is_empty() || self.config.max_follow_distance == 0 {
487 return Ok(());
488 }
489
490 info!(
491 "Nostr mirror starting: relays={} max_follow_distance={} negentropy_only={} kinds={:?} history_sync_author_chunk_size={} history_sync_on_start={} history_sync_on_reconnect={}",
492 self.config.relays.len(),
493 self.config.max_follow_distance,
494 self.config.require_negentropy,
495 self.config.kinds,
496 self.config.history_sync_author_chunk_size.max(1),
497 self.config.history_sync_on_start,
498 self.config.history_sync_on_reconnect
499 );
500
501 tokio::time::sleep(MIRROR_STARTUP_DELAY).await;
502 tokio::time::sleep(MIRROR_CONNECT_SETTLE_DELAY).await;
503 let live_since = Timestamp::now();
504 self.sync_publish_roots_from_store()?;
505 let (event_result, profile_search_result, profiles_by_pubkey_result) =
506 self.publish_priority_roots(true, true, true).await;
507 if let Err(err) = event_result {
508 warn!(
509 "Nostr mirror event-root publish failed on startup: {:#}",
510 err
511 );
512 }
513 if let Err(err) = profile_search_result {
514 warn!(
515 "Nostr mirror profile-search publish failed on startup: {:#}",
516 err
517 );
518 }
519 if let Err(err) = profiles_by_pubkey_result {
520 warn!(
521 "Nostr mirror profiles-by-pubkey publish failed on startup: {:#}",
522 err
523 );
524 }
525
526 let initial_authors = self.collect_authors()?;
527 if initial_authors.is_empty() {
528 info!("Nostr mirror: no social-graph authors to mirror yet");
529 }
530
531 let mut subscribed_authors = HashSet::new();
532 self.subscribe_authors_since(&initial_authors, live_since, &mut subscribed_authors)
533 .await?;
534
535 if !initial_authors.is_empty() && self.config.history_sync_on_start {
536 self.spawn_startup_history_sync(initial_authors.clone());
537 }
538
539 let mut relay_statuses = self.capture_relay_statuses().await;
540 let mut last_reconnect_history_sync_at: Option<Instant> = None;
541 let mut last_missing_profile_backfill_at: Option<Instant> = None;
542 let mut notifications = self.client.notifications();
543 let mut shutdown_rx = self.shutdown_rx.clone();
544 let mut refresh_interval = tokio::time::interval(MIRROR_AUTHOR_REFRESH_INTERVAL);
545 let mut publish_interval = tokio::time::interval(MIRROR_ROOT_PUBLISH_DEBOUNCE);
546
547 loop {
548 tokio::select! {
549 _ = shutdown_rx.changed() => {
550 if *shutdown_rx.borrow() {
551 break;
552 }
553 }
554 _ = refresh_interval.tick() => {
555 let authors = self.collect_authors()?;
556 let mut reconnected_relay = None;
557 for (relay_url, status) in self.capture_relay_statuses().await {
558 let previous = relay_statuses.insert(relay_url.clone(), status);
559 if reconnected_relay.is_none()
560 && Self::should_history_sync_on_reconnect(
561 self.config.history_sync_on_reconnect,
562 previous,
563 status,
564 )
565 {
566 reconnected_relay = Some(relay_url);
567 }
568 }
569 if let Some(relay_url) = reconnected_relay {
570 if Self::should_run_reconnect_history_sync(
571 last_reconnect_history_sync_at.as_ref(),
572 ) && !authors.is_empty()
573 {
574 info!(
575 "Nostr mirror relay reconnected; running catch-up history sync: relay={} authors={} negentropy_only={}",
576 relay_url,
577 authors.len(),
578 self.config.require_negentropy
579 );
580 self.spawn_author_history_sync(
581 "relay reconnect catch-up",
582 authors.clone(),
583 false,
584 false,
585 );
586 last_reconnect_history_sync_at = Some(Instant::now());
587 }
588 }
589 let new_authors = authors
590 .iter()
591 .filter(|author| !subscribed_authors.contains(*author))
592 .cloned()
593 .collect::<Vec<_>>();
594 if !new_authors.is_empty() {
595 debug!(
596 "Nostr mirror discovered {} newly reachable author(s)",
597 new_authors.len()
598 );
599 self.subscribe_authors_since(
600 &new_authors,
601 Timestamp::now(),
602 &mut subscribed_authors,
603 )
604 .await?;
605 self.spawn_author_history_sync(
606 "new-author catch-up",
607 new_authors.clone(),
608 true,
609 true,
610 );
611 }
612 if self.should_backfill_missing_profiles(last_missing_profile_backfill_at) {
613 let missing_profile_authors = self.collect_missing_profile_authors(
614 self.config.missing_profile_backfill_batch_size,
615 )?;
616 if !missing_profile_authors.is_empty() {
617 info!(
618 "Nostr mirror missing-profile backfill starting: authors={}",
619 missing_profile_authors.len()
620 );
621 self.spawn_missing_profile_backfill(missing_profile_authors);
622 last_missing_profile_backfill_at = Some(Instant::now());
623 }
624 }
625 }
626 _ = publish_interval.tick() => {
627 self.sync_publish_roots_from_store()?;
628 if let Err(err) = self.flush_live_events().await {
629 warn!("Nostr mirror live event flush failed: {:#}", err);
630 }
631 let (event_result, profile_search_result, profiles_by_pubkey_result) = self
632 .publish_pending_roots(false, false, false)
633 .await;
634 if let Err(err) = event_result {
635 warn!("Nostr mirror event-root publish failed: {:#}", err);
636 }
637 if let Err(err) = profile_search_result {
638 warn!("Nostr mirror profile-search publish failed: {:#}", err);
639 }
640 if let Err(err) = profiles_by_pubkey_result {
641 warn!("Nostr mirror profiles-by-pubkey publish failed: {:#}", err);
642 }
643 }
644 notification = notifications.recv() => {
645 match notification {
646 Ok(RelayPoolNotification::Event { event, .. }) => {
647 self.ingest_live_event(&event)?;
648 }
649 Ok(RelayPoolNotification::Shutdown) => break,
650 Ok(_) => {}
651 Err(err) => {
652 warn!("Nostr mirror notification error: {}", err);
653 break;
654 }
655 }
656 }
657 }
658 }
659
660 if let Err(err) = self.flush_live_events().await {
661 warn!(
662 "Nostr mirror live event flush failed during shutdown: {:#}",
663 err
664 );
665 }
666 if let Err(err) = self.sync_publish_roots_from_store() {
667 warn!(
668 "Nostr mirror root-state refresh failed during shutdown: {:#}",
669 err
670 );
671 }
672 let (event_result, profile_search_result, profiles_by_pubkey_result) =
673 self.publish_pending_roots(true, true, true).await;
674 if let Err(err) = event_result {
675 warn!(
676 "Nostr mirror event-root publish failed during shutdown: {:#}",
677 err
678 );
679 }
680 if let Err(err) = profile_search_result {
681 warn!(
682 "Nostr mirror profile-search publish failed during shutdown: {:#}",
683 err
684 );
685 }
686 if let Err(err) = profiles_by_pubkey_result {
687 warn!(
688 "Nostr mirror profiles-by-pubkey publish failed during shutdown: {:#}",
689 err
690 );
691 }
692 let _ = self.client.disconnect().await;
693 if let Some(client) = self.publish_client.as_ref() {
694 let _ = client.disconnect().await;
695 }
696 Ok(())
697 }
698
699 fn spawn_startup_history_sync(self: &Arc<Self>, initial_authors: Vec<String>) {
700 let mirror = Arc::clone(self);
701 tokio::task::spawn_blocking(move || {
702 let runtime = tokio::runtime::Builder::new_current_thread()
703 .enable_all()
704 .build()
705 .expect("build nostr mirror startup history sync runtime");
706 runtime.block_on(async move {
707 let _guard = mirror.history_sync_lock.lock().await;
708 if let Err(err) = mirror.run_startup_history_sync(initial_authors).await {
709 warn!("Nostr mirror startup history sync failed: {:#}", err);
710 }
711 });
712 });
713 }
714
715 async fn run_startup_history_sync(&self, initial_authors: Vec<String>) -> Result<()> {
716 self.history_sync_full_text_notes_for_reachable_authors()
717 .await?;
718 self.history_sync_authors(initial_authors).await?;
719 if self.should_backfill_missing_profiles(None) {
720 let missing_profile_authors = self
721 .collect_missing_profile_authors(self.config.missing_profile_backfill_batch_size)?;
722 if !missing_profile_authors.is_empty() {
723 info!(
724 "Nostr mirror missing-profile backfill starting: authors={}",
725 missing_profile_authors.len()
726 );
727 self.history_sync_authors_with_kinds(
728 missing_profile_authors,
729 &[Kind::Metadata.as_u16()],
730 )
731 .await?;
732 }
733 }
734 Ok(())
735 }
736
737 fn spawn_author_history_sync(
738 self: &Arc<Self>,
739 label: &'static str,
740 authors: Vec<String>,
741 include_full_text_notes: bool,
742 wait_for_existing_sync: bool,
743 ) {
744 let mirror = Arc::clone(self);
745 tokio::task::spawn_blocking(move || {
746 let runtime = tokio::runtime::Builder::new_current_thread()
747 .enable_all()
748 .build()
749 .expect("build nostr mirror author history sync runtime");
750 runtime.block_on(async move {
751 if wait_for_existing_sync {
752 let _guard = mirror.history_sync_lock.lock().await;
753 if let Err(err) = mirror
754 .run_author_history_sync(authors, include_full_text_notes)
755 .await
756 {
757 warn!("Nostr mirror {label} failed: {:#}", err);
758 }
759 return;
760 }
761
762 let Ok(_guard) = mirror.history_sync_lock.try_lock() else {
763 info!("Nostr mirror {label} skipped; another history sync is running");
764 return;
765 };
766 if let Err(err) = mirror
767 .run_author_history_sync(authors, include_full_text_notes)
768 .await
769 {
770 warn!("Nostr mirror {label} failed: {:#}", err);
771 }
772 });
773 });
774 }
775
776 async fn run_author_history_sync(
777 &self,
778 authors: Vec<String>,
779 include_full_text_notes: bool,
780 ) -> Result<()> {
781 if include_full_text_notes {
782 self.history_sync_full_text_notes_for_authors(authors.clone())
783 .await?;
784 }
785 self.history_sync_authors(authors).await
786 }
787
788 fn spawn_missing_profile_backfill(self: &Arc<Self>, authors: Vec<String>) {
789 let mirror = Arc::clone(self);
790 tokio::task::spawn_blocking(move || {
791 let runtime = tokio::runtime::Builder::new_current_thread()
792 .enable_all()
793 .build()
794 .expect("build nostr mirror missing profile runtime");
795 runtime.block_on(async move {
796 let Ok(_guard) = mirror.history_sync_lock.try_lock() else {
797 info!(
798 "Nostr mirror missing-profile backfill skipped; another history sync is running"
799 );
800 return;
801 };
802 if let Err(err) = mirror
803 .history_sync_authors_with_kinds(authors, &[Kind::Metadata.as_u16()])
804 .await
805 {
806 warn!("Nostr mirror missing-profile backfill failed: {:#}", err);
807 }
808 });
809 });
810 }
811
812 async fn capture_relay_statuses(&self) -> HashMap<String, RelayStatus> {
813 let mut statuses = HashMap::new();
814 for (relay_url, relay) in self.client.relays().await {
815 statuses.insert(relay_url.to_string(), relay.status());
816 }
817 statuses
818 }
819
820 async fn has_connected_publish_relay(&self) -> bool {
821 let Some(client) = self.publish_client.as_ref() else {
822 return false;
823 };
824 Self::client_has_connected_relay(client).await
825 }
826
827 async fn client_has_connected_relay(client: &Client) -> bool {
828 for (_relay_url, relay) in client.relays().await {
829 if relay.status() == RelayStatus::Connected {
830 return true;
831 }
832 }
833 false
834 }
835
836 fn collect_authors(&self) -> Result<Vec<String>> {
837 self.collect_authors_with_max_distance(self.config.max_follow_distance)
838 }
839
840 fn collect_authors_with_max_distance(&self, max_distance: u32) -> Result<Vec<String>> {
841 let mut authors = Vec::new();
842 let mut seen = HashSet::new();
843 for distance in 0..=max_distance {
844 for pubkey in socialgraph::SocialGraphBackend::users_by_follow_distance(
845 self.graph_store.as_ref(),
846 distance,
847 )
848 .with_context(|| format!("load social-graph distance {distance}"))?
849 {
850 if self
851 .graph_store
852 .is_overmuted_user(&pubkey, self.config.overmute_threshold)?
853 {
854 continue;
855 }
856 let hex = hex::encode(pubkey);
857 if seen.insert(hex.clone()) {
858 authors.push(hex);
859 }
860 }
861 }
862 Ok(authors)
863 }
864
865 async fn prioritize_full_text_note_history_authors(
866 &self,
867 authors: Vec<String>,
868 ) -> Result<Vec<String>> {
869 let Some(root) = self.graph_store.public_events_root()? else {
870 return Ok(authors);
871 };
872
873 let event_store = NostrEventStore::new(self.store.store_arc());
874 let mut prioritized = Vec::with_capacity(authors.len());
875 let mut sampled = 0usize;
876 for (index, author) in authors.into_iter().enumerate() {
877 let distance = match decode_hex_pubkey(&author) {
878 Some(pubkey) => self
879 .graph_store
880 .follow_distance(&pubkey)?
881 .unwrap_or(u32::MAX),
882 None => u32::MAX,
883 };
884 let indexed_text_sample = if distance <= FULL_TEXT_HISTORY_PRIORITY_MAX_DISTANCE {
885 sampled = sampled.saturating_add(1);
886 event_store
887 .list_by_author_and_kind(
888 Some(&root),
889 &author,
890 Kind::TextNote.as_u16() as u32,
891 ListEventsOptions {
892 limit: Some(FULL_TEXT_HISTORY_PRIORITY_SAMPLE_LIMIT),
893 ..ListEventsOptions::default()
894 },
895 )
896 .await
897 .with_context(|| {
898 format!("sample indexed text-note history for author {author}")
899 })?
900 .len()
901 } else {
902 FULL_TEXT_HISTORY_PRIORITY_SAMPLE_LIMIT
903 };
904 prioritized.push((distance, indexed_text_sample, index, author));
905 }
906
907 prioritized.sort_by(|left, right| {
908 left.0
909 .cmp(&right.0)
910 .then_with(|| left.1.cmp(&right.1))
911 .then_with(|| left.2.cmp(&right.2))
912 });
913 info!(
914 "Nostr mirror full text content history prioritized authors: authors={} sampled_distance_le_{}={}",
915 prioritized.len(),
916 FULL_TEXT_HISTORY_PRIORITY_MAX_DISTANCE,
917 sampled
918 );
919 Ok(prioritized
920 .into_iter()
921 .map(|(_, _, _, author)| author)
922 .collect())
923 }
924
925 fn full_text_note_history_follow_distance(&self) -> Option<u32> {
926 let distance = self.config.full_text_note_history_follow_distance?;
927 if self
928 .config
929 .kinds
930 .iter()
931 .any(|kind| *kind == Kind::TextNote.as_u16() || *kind == KIND_LONG_FORM_CONTENT)
932 {
933 Some(distance.min(self.config.max_follow_distance))
934 } else {
935 None
936 }
937 }
938
939 fn full_text_note_history_max_relay_pages(&self) -> Option<usize> {
940 Self::full_text_note_history_max_relay_pages_for_config(&self.config)
941 }
942
943 fn full_text_note_history_max_relay_pages_for_config(
944 config: &NostrMirrorConfig,
945 ) -> Option<usize> {
946 let pages = config.full_text_note_history_max_relay_pages;
947 if pages == 0 {
948 None
949 } else {
950 Some(pages)
951 }
952 }
953
954 fn is_text_content_history_kind(kind: u16) -> bool {
955 kind == Kind::TextNote.as_u16() || kind == KIND_LONG_FORM_CONTENT
956 }
957
958 fn history_sync_kinds_for_config(config: &NostrMirrorConfig) -> Vec<u16> {
959 let mut kinds = config.kinds.clone();
960 kinds.retain(|kind| !Self::is_text_content_history_kind(*kind));
961 kinds
962 }
963
964 fn collect_missing_profile_authors(&self, limit: usize) -> Result<Vec<String>> {
965 if limit == 0 {
966 return Ok(Vec::new());
967 }
968
969 let authors = self.collect_authors()?;
970 if authors.is_empty() {
971 return Ok(Vec::new());
972 }
973
974 let mut cursor = self
975 .missing_profile_cursor
976 .lock()
977 .expect("missing profile cursor");
978 let mut index = (*cursor).min(authors.len());
979 let mut scanned = 0usize;
980 let mut missing = Vec::new();
981
982 while scanned < authors.len() && missing.len() < limit {
983 let author = &authors[index];
984 if self.graph_store.latest_profile_event(author)?.is_none() {
985 missing.push(author.clone());
986 }
987 index += 1;
988 if index == authors.len() {
989 index = 0;
990 }
991 scanned += 1;
992 }
993
994 *cursor = index;
995 Ok(missing)
996 }
997
998 fn should_backfill_missing_profiles(&self, last_run: Option<Instant>) -> bool {
999 if self.config.missing_profile_backfill_batch_size == 0
1000 || !self.config.kinds.contains(&Kind::Metadata.as_u16())
1001 {
1002 return false;
1003 }
1004 match last_run {
1005 Some(last_run) => last_run.elapsed() >= MIRROR_MISSING_PROFILE_BACKFILL_INTERVAL,
1006 None => true,
1007 }
1008 }
1009
1010 fn should_history_sync_on_reconnect(
1011 history_sync_on_reconnect: bool,
1012 previous: Option<RelayStatus>,
1013 status: RelayStatus,
1014 ) -> bool {
1015 history_sync_on_reconnect
1016 && status == RelayStatus::Connected
1017 && matches!(
1018 previous,
1019 Some(
1020 RelayStatus::Initialized
1021 | RelayStatus::Pending
1022 | RelayStatus::Connecting
1023 | RelayStatus::Disconnected
1024 | RelayStatus::Terminated
1025 )
1026 )
1027 }
1028
1029 fn should_run_reconnect_history_sync(last_run: Option<&Instant>) -> bool {
1030 match last_run {
1031 None => true,
1032 Some(last_run) => last_run.elapsed() >= MIRROR_RECONNECT_HISTORY_SYNC_COOLDOWN,
1033 }
1034 }
1035
1036 fn is_metadata_only_history_sync(kinds: &[u16]) -> bool {
1037 !kinds.is_empty() && kinds.iter().all(|kind| *kind == Kind::Metadata.as_u16())
1038 }
1039
1040 fn history_sync_kinds_affect_profile_or_graph(kinds: &[u16]) -> bool {
1041 kinds.is_empty()
1042 || kinds.iter().any(|kind| {
1043 *kind == Kind::Metadata.as_u16()
1044 || *kind == Kind::ContactList.as_u16()
1045 || *kind == Kind::MuteList.as_u16()
1046 })
1047 }
1048
1049 fn history_sync_plan_for(
1050 config: &NostrMirrorConfig,
1051 authors: usize,
1052 kinds: &[u16],
1053 ) -> HistorySyncPlan {
1054 let author_batch_size = config.author_batch_size.max(1);
1055 let per_author_event_limit = config.history_sync_per_author_event_limit.max(1);
1056 let relay_page_size = 1_000;
1057 let max_relay_pages = 10;
1058
1059 if Self::is_metadata_only_history_sync(kinds) {
1060 return HistorySyncPlan {
1061 relay_fetch_mode: RelayFetchMode::AuthorBatches,
1062 author_batch_size: author_batch_size.min(METADATA_HISTORY_SYNC_AUTHOR_BATCH_SIZE),
1063 per_author_event_limit: METADATA_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT,
1064 relay_page_size,
1065 max_relay_pages,
1066 };
1067 }
1068
1069 if authors > author_batch_size.saturating_mul(LARGE_HISTORY_SYNC_AUTHOR_MULTIPLIER) {
1070 return HistorySyncPlan {
1071 relay_fetch_mode: RelayFetchMode::GlobalRecent,
1072 author_batch_size,
1073 per_author_event_limit: per_author_event_limit
1074 .clamp(1, LARGE_HISTORY_SYNC_PER_AUTHOR_EVENT_LIMIT),
1075 relay_page_size,
1076 max_relay_pages: LARGE_HISTORY_SYNC_MAX_RELAY_PAGES,
1077 };
1078 }
1079
1080 HistorySyncPlan {
1081 relay_fetch_mode: RelayFetchMode::AuthorBatches,
1082 author_batch_size,
1083 per_author_event_limit,
1084 relay_page_size,
1085 max_relay_pages,
1086 }
1087 }
1088
1089 fn history_sync_plan(&self, authors: usize, kinds: &[u16]) -> HistorySyncPlan {
1090 Self::history_sync_plan_for(&self.config, authors, kinds)
1091 }
1092
1093 fn history_sync_chunk_size_for_config(
1094 config: &NostrMirrorConfig,
1095 authors: usize,
1096 kinds: &[u16],
1097 full_author_history: bool,
1098 chunk_size_override: Option<usize>,
1099 ) -> usize {
1100 let configured_chunk_size = chunk_size_override
1101 .unwrap_or(config.history_sync_author_chunk_size)
1102 .max(1);
1103 if full_author_history {
1104 return 1;
1105 }
1106 if !full_author_history
1107 && Self::history_sync_plan_for(config, authors, kinds).relay_fetch_mode
1108 == RelayFetchMode::GlobalRecent
1109 {
1110 return authors.max(1);
1111 }
1112 configured_chunk_size
1113 }
1114
1115 async fn history_sync_authors(&self, authors: Vec<String>) -> Result<()> {
1116 let kinds = Self::history_sync_kinds_for_config(&self.config);
1117 if kinds.is_empty() {
1118 info!("Nostr mirror history sync skipped: no enabled history kinds");
1119 return Ok(());
1120 }
1121 self.history_sync_authors_with_kinds(authors, &kinds).await
1122 }
1123
1124 async fn history_sync_authors_with_kinds(
1125 &self,
1126 authors: Vec<String>,
1127 kinds: &[u16],
1128 ) -> Result<()> {
1129 self.history_sync_authors_with_kinds_and_mode(authors, kinds, false, None)
1130 .await
1131 }
1132
1133 async fn history_sync_full_text_notes_for_reachable_authors(&self) -> Result<()> {
1134 let Some(distance) = self.full_text_note_history_follow_distance() else {
1135 return Ok(());
1136 };
1137 if self.full_text_note_history_max_relay_pages().is_none() {
1138 info!("Nostr mirror full text content history sync skipped: max_relay_pages=0");
1139 return Ok(());
1140 }
1141 info!(
1142 "Nostr mirror full text content history author collection starting: max_follow_distance={distance}"
1143 );
1144 let authors = self
1145 .prioritize_full_text_note_history_authors(
1146 self.collect_authors_with_max_distance(distance)?,
1147 )
1148 .await?;
1149 let Some(max_relay_pages) = self.full_text_note_history_max_relay_pages() else {
1150 info!("Nostr mirror full text content history sync skipped: max_relay_pages=0");
1151 return Ok(());
1152 };
1153 self.history_sync_distance_filtered_full_text_notes(authors, distance, max_relay_pages)
1154 .await
1155 }
1156
1157 async fn history_sync_full_text_notes_for_authors(&self, authors: Vec<String>) -> Result<()> {
1158 let Some(distance) = self.full_text_note_history_follow_distance() else {
1159 return Ok(());
1160 };
1161 let Some(max_relay_pages) = self.full_text_note_history_max_relay_pages() else {
1162 info!("Nostr mirror full text content history sync skipped: max_relay_pages=0");
1163 return Ok(());
1164 };
1165 let mut close_authors = Vec::new();
1166 for author in authors {
1167 let Some(pubkey) = decode_hex_pubkey(&author) else {
1168 continue;
1169 };
1170 if self
1171 .graph_store
1172 .follow_distance(&pubkey)?
1173 .is_some_and(|actual_distance| actual_distance <= distance)
1174 {
1175 close_authors.push(author);
1176 }
1177 }
1178 if close_authors.is_empty() {
1179 return Ok(());
1180 }
1181 let close_authors = self
1182 .prioritize_full_text_note_history_authors(close_authors)
1183 .await?;
1184
1185 self.history_sync_distance_filtered_full_text_notes(
1186 close_authors,
1187 distance,
1188 max_relay_pages,
1189 )
1190 .await
1191 }
1192
1193 async fn history_sync_distance_filtered_full_text_notes(
1194 &self,
1195 close_authors: Vec<String>,
1196 distance: u32,
1197 max_relay_pages: usize,
1198 ) -> Result<()> {
1199 if close_authors.is_empty() {
1200 return Ok(());
1201 }
1202
1203 info!(
1204 "Nostr mirror full text content history sync starting: authors={} max_follow_distance={} max_relay_pages={}",
1205 close_authors.len(),
1206 distance,
1207 max_relay_pages
1208 );
1209 let kinds = [Kind::TextNote.as_u16(), KIND_LONG_FORM_CONTENT];
1210 self.history_sync_authors_with_kinds_and_mode(
1211 close_authors,
1212 &kinds,
1213 true,
1214 Some(max_relay_pages),
1215 )
1216 .await
1217 }
1218
1219 async fn history_sync_authors_with_kinds_and_mode(
1220 &self,
1221 authors: Vec<String>,
1222 kinds: &[u16],
1223 full_author_history: bool,
1224 max_relay_pages: Option<usize>,
1225 ) -> Result<()> {
1226 let update_profile_and_graph = Self::history_sync_kinds_affect_profile_or_graph(kinds);
1227 let chunk_size = Self::history_sync_chunk_size_for_config(
1228 &self.config,
1229 authors.len(),
1230 kinds,
1231 full_author_history,
1232 None,
1233 );
1234 self.history_sync_authors_chunked(
1235 authors,
1236 |current_root, author_chunk| async move {
1237 self.history_sync_author_chunk(
1238 current_root,
1239 author_chunk,
1240 kinds,
1241 full_author_history,
1242 max_relay_pages,
1243 )
1244 .await
1245 },
1246 update_profile_and_graph,
1247 Some(chunk_size),
1248 )
1249 .await
1250 }
1251
1252 async fn history_sync_authors_chunked<F, Fut>(
1253 &self,
1254 authors: Vec<String>,
1255 mut run_chunk: F,
1256 update_profile_and_graph: bool,
1257 chunk_size_override: Option<usize>,
1258 ) -> Result<()>
1259 where
1260 F: FnMut(Option<hashtree_core::Cid>, Vec<String>) -> Fut,
1261 Fut: std::future::Future<Output = Result<CrawlReport>>,
1262 {
1263 if authors.is_empty() {
1264 return Ok(());
1265 }
1266 let _publication_deferral = RootPublicationDeferral::new(&self.root_publication_deferrals);
1267
1268 info!(
1269 "Nostr mirror history sync starting: authors={} relays={} negentropy_only={}",
1270 authors.len(),
1271 self.config.relays.len(),
1272 self.config.require_negentropy
1273 );
1274
1275 let mut current_root = self.graph_store.public_events_root_for_write()?;
1276 let mut last_error = None;
1277 let mut applied_chunks = 0usize;
1278 let mut failed_chunks = 0usize;
1279 let mut authors_since_publish = 0usize;
1280 let publish_checkpoint_authors = self.config.history_sync_author_chunk_size.max(1);
1281 let chunk_size = chunk_size_override
1282 .unwrap_or(self.config.history_sync_author_chunk_size)
1283 .max(1);
1284 let total_chunks = authors.len().div_ceil(chunk_size);
1285
1286 for (chunk_index, author_chunk) in authors.chunks(chunk_size).enumerate() {
1287 let author_chunk = author_chunk.to_vec();
1288 let author_count = author_chunk.len();
1289 info!(
1290 "Nostr mirror history sync chunk starting: chunk={}/{} authors={}",
1291 chunk_index + 1,
1292 total_chunks,
1293 author_count
1294 );
1295 let mut report = match run_chunk(current_root.clone(), author_chunk.clone()).await {
1296 Ok(report) => report,
1297 Err(err) => {
1298 failed_chunks = failed_chunks.saturating_add(1);
1299 warn!(
1300 "Nostr mirror history sync chunk failed: chunk={}/{} authors={} error={:#}",
1301 chunk_index + 1,
1302 total_chunks,
1303 author_count,
1304 err
1305 );
1306 last_error = Some(err);
1307 trim_transient_allocations();
1308 continue;
1309 }
1310 };
1311
1312 let latest_root = self.graph_store.public_events_root_for_write()?;
1313 if latest_root != current_root {
1314 info!(
1315 "Nostr mirror history sync root advanced while chunk was fetching; merging chunk into latest root: chunk={}/{} authors={} events_applied={}",
1316 chunk_index + 1,
1317 total_chunks,
1318 author_count,
1319 report.applied_events.len()
1320 );
1321 if report.applied_events.is_empty() {
1322 report.root = latest_root.clone();
1323 } else {
1324 let event_store = NostrEventStore::new(self.store.store_arc());
1325 report.root = event_store
1326 .build(latest_root.as_ref(), report.applied_events.clone())
1327 .await
1328 .context("merge history chunk into latest mirrored event root")?;
1329 }
1330 current_root = latest_root;
1331 }
1332
1333 if report.root != current_root {
1334 self.apply_history_root_with_options(
1335 report.root.as_ref(),
1336 update_profile_and_graph,
1337 false,
1338 Some(&report.applied_events),
1339 )
1340 .await?;
1341 current_root = report.root.clone();
1342 info!(
1343 "Nostr mirror history sync updated trusted root: chunk={}/{} authors_processed={} events_selected={} events_seen={}",
1344 chunk_index + 1,
1345 total_chunks,
1346 report.authors_processed,
1347 report.events_selected,
1348 report.events_seen
1349 );
1350 }
1351 applied_chunks = applied_chunks.saturating_add(1);
1352 authors_since_publish = authors_since_publish.saturating_add(author_count);
1353 if authors_since_publish >= publish_checkpoint_authors {
1354 self.publish_history_roots(update_profile_and_graph).await;
1355 authors_since_publish = 0;
1356 }
1357 trim_transient_allocations();
1358 }
1359
1360 if applied_chunks == 0 {
1361 return Err(last_error
1362 .unwrap_or_else(|| anyhow::anyhow!("mirror history sync made no progress"))
1363 .context("run mirror history sync"));
1364 }
1365 if failed_chunks > 0 {
1366 warn!(
1367 "Nostr mirror history sync completed with skipped chunks: applied_chunks={} failed_chunks={}",
1368 applied_chunks, failed_chunks
1369 );
1370 }
1371 self.publish_history_roots(update_profile_and_graph).await;
1375 Ok(())
1376 }
1377
1378 async fn history_sync_author_chunk(
1379 &self,
1380 current_root: Option<hashtree_core::Cid>,
1381 authors: Vec<String>,
1382 kinds: &[u16],
1383 full_author_history: bool,
1384 max_relay_pages: Option<usize>,
1385 ) -> Result<CrawlReport> {
1386 let mut last_error = None;
1387 let mut report = None;
1388 let mut plan = self.history_sync_plan(authors.len(), kinds);
1389 if full_author_history {
1390 plan.relay_fetch_mode = RelayFetchMode::AuthorBatches;
1391 plan.max_relay_pages = max_relay_pages.unwrap_or(plan.max_relay_pages);
1392 }
1393 for attempt in 0..3 {
1394 let mut last_logged_authors = 0usize;
1395 let bridge = NostrBridge::new(
1396 self.store.store_arc(),
1397 CrawlConfig {
1398 relays: self.config.relays.clone(),
1399 author_allowlist: Some(authors.clone()),
1400 max_live_bytes: None,
1401 max_events_seen: None,
1402 max_authors: None,
1403 max_follow_distance: None,
1404 author_batch_size: plan.author_batch_size,
1405 per_author_event_limit: plan.per_author_event_limit,
1406 per_author_live_bytes: None,
1407 fetch_timeout: self.config.fetch_timeout,
1408 kinds: Some(kinds.to_vec()),
1409 relay_fetch_mode: plan.relay_fetch_mode,
1410 require_negentropy: self.config.require_negentropy,
1411 relay_event_max_size: self.config.relay_event_max_size,
1412 relay_page_size: plan.relay_page_size,
1413 max_relay_pages: plan.max_relay_pages,
1414 full_author_history,
1415 },
1416 );
1417
1418 let crawl = bridge.crawl_with_progress(
1419 self.graph_store.as_ref(),
1420 current_root.as_ref(),
1421 |progress| {
1422 let log_interval = self.config.author_batch_size.saturating_mul(8).max(2_048);
1423 let should_log = progress.authors_processed == progress.authors_considered
1424 || progress.authors_processed == 0
1425 || progress
1426 .authors_processed
1427 .saturating_sub(last_logged_authors)
1428 >= log_interval;
1429 if should_log {
1430 last_logged_authors = progress.authors_processed;
1431 info!(
1432 "Nostr mirror history sync progress: authors_processed={}/{} events_selected={} events_seen={}",
1433 progress.authors_processed,
1434 progress.authors_considered,
1435 progress.events_selected,
1436 progress.events_seen
1437 );
1438 }
1439 },
1440 );
1441 let crawl_result: Result<CrawlReport> = if full_author_history {
1442 let timeout = self.config.fetch_timeout.saturating_mul(4);
1443 match tokio::time::timeout(timeout, crawl).await {
1444 Ok(result) => result.map_err(Into::into),
1445 Err(_) => Err(anyhow::anyhow!(
1446 "full author history crawl timed out after {:?} for {} author(s)",
1447 timeout,
1448 authors.len()
1449 )),
1450 }
1451 } else {
1452 crawl.await.map_err(Into::into)
1453 };
1454
1455 match crawl_result {
1456 Ok(next_report) => {
1457 report = Some(next_report);
1458 break;
1459 }
1460 Err(err) => {
1461 last_error = Some(err);
1462 if full_author_history {
1463 break;
1464 }
1465 if attempt < 2 {
1466 tokio::time::sleep(Duration::from_millis(500)).await;
1467 }
1468 }
1469 }
1470 }
1471 report
1472 .ok_or_else(|| last_error.expect("history sync retry captured error"))
1473 .context("run mirror history sync")
1474 }
1475
1476 #[cfg(test)]
1477 async fn apply_history_root(&self, root: Option<&hashtree_core::Cid>) -> Result<()> {
1478 self.apply_history_root_with_options(root, true, true, None)
1479 .await
1480 }
1481
1482 async fn apply_history_root_with_options(
1483 &self,
1484 root: Option<&hashtree_core::Cid>,
1485 update_profile_and_graph: bool,
1486 publish_roots: bool,
1487 applied_events: Option<&[hashtree_nostr::StoredNostrEvent]>,
1488 ) -> Result<()> {
1489 self.graph_store.write_public_events_root(root)?;
1490 let Some(root) = root else {
1491 return Ok(());
1492 };
1493
1494 self.note_public_events_root_change()?;
1495 if update_profile_and_graph {
1496 let events = match applied_events {
1497 Some(events) => events
1498 .iter()
1499 .cloned()
1500 .map(socialgraph::stored_event_to_nostr_event)
1501 .collect::<Result<Vec<_>>>()?,
1502 None => {
1503 let event_store = NostrEventStore::new(self.store.store_arc());
1504 event_store
1505 .list_recent_lossy(Some(root), ListEventsOptions::default())
1506 .await
1507 .context("list trusted mirrored events")?
1508 .into_iter()
1509 .map(socialgraph::stored_event_to_nostr_event)
1510 .collect::<Result<Vec<_>>>()?
1511 }
1512 };
1513
1514 socialgraph::ingest_graph_parsed_events(self.graph_store.as_ref(), &events)
1515 .context("sync mirrored social graph state")?;
1516 if applied_events.is_some() {
1517 self.graph_store
1518 .sync_profile_index_for_events(&events)
1519 .context("update mirrored profile search index")?;
1520 } else {
1521 self.graph_store
1522 .rebuild_profile_index_for_events(&events)
1523 .context("rebuild mirrored profile search index")?;
1524 }
1525 self.note_profile_search_root_change()?;
1526 self.note_profiles_by_pubkey_root_change()?;
1527 }
1528 if !publish_roots {
1529 return Ok(());
1530 }
1531 self.publish_history_roots(update_profile_and_graph).await;
1532 Ok(())
1533 }
1534
1535 async fn publish_history_roots(&self, update_profile_and_graph: bool) {
1536 let (event_result, profile_search_result, profiles_by_pubkey_result) = self
1537 .publish_priority_roots(true, update_profile_and_graph, update_profile_and_graph)
1538 .await;
1539 if let Err(err) = event_result {
1540 warn!(
1541 "Nostr mirror event-root publish failed after root update: {:#}",
1542 err
1543 );
1544 }
1545 if let Err(err) = profile_search_result {
1546 warn!(
1547 "Nostr mirror profile-search publish failed after root update: {:#}",
1548 err
1549 );
1550 }
1551 if let Err(err) = profiles_by_pubkey_result {
1552 warn!(
1553 "Nostr mirror profiles-by-pubkey publish failed after root update: {:#}",
1554 err
1555 );
1556 }
1557 }
1558
1559 async fn subscribe_authors_since(
1560 &self,
1561 authors: &[String],
1562 since: Timestamp,
1563 subscribed_authors: &mut HashSet<String>,
1564 ) -> Result<()> {
1565 let new_authors = authors
1566 .iter()
1567 .filter(|author| !subscribed_authors.contains(*author))
1568 .cloned()
1569 .collect::<Vec<_>>();
1570 if new_authors.is_empty() {
1571 return Ok(());
1572 }
1573
1574 for chunk in new_authors.chunks(self.config.author_batch_size.max(1)) {
1575 let pubkeys = chunk
1576 .iter()
1577 .filter_map(|author| PublicKey::from_hex(author).ok())
1578 .collect::<Vec<_>>();
1579 if pubkeys.is_empty() {
1580 continue;
1581 }
1582
1583 let filter = Filter::new()
1584 .authors(pubkeys)
1585 .kinds(self.config.kinds.iter().copied().map(Kind::from))
1586 .since(since);
1587
1588 if let Err(err) = self.client.subscribe(filter, None).await {
1589 warn!(
1590 "Nostr mirror author subscription failed: authors={} error={:#}",
1591 chunk.len(),
1592 err
1593 );
1594 continue;
1595 }
1596 subscribed_authors.extend(chunk.iter().cloned());
1597 }
1598 Ok(())
1599 }
1600
1601 fn ingest_live_event(&self, event: &Event) -> Result<()> {
1602 self.pending_live_events
1603 .lock()
1604 .expect("pending live events")
1605 .insert(event.id.to_hex(), event.clone());
1606 Ok(())
1607 }
1608
1609 async fn flush_live_events(&self) -> Result<()> {
1610 let pending = {
1611 let mut pending = self
1612 .pending_live_events
1613 .lock()
1614 .expect("pending live events");
1615 if pending.is_empty() {
1616 return Ok(());
1617 }
1618 std::mem::take(&mut *pending)
1619 };
1620 let events = pending.into_values().collect::<Vec<_>>();
1621 let event_count = events.len();
1622 let previous_event_root = self.graph_store.public_events_root()?;
1623 let previous_profile_search_root = self.graph_store.profile_search_root()?;
1624 let previous_profiles_by_pubkey_root = self.graph_store.profiles_by_pubkey_root()?;
1625
1626 socialgraph::ingest_parsed_events_with_storage_class(
1627 self.graph_store.as_ref(),
1628 &events,
1629 socialgraph::EventStorageClass::Public,
1630 )
1631 .context("ingest live mirrored event batch")?;
1632
1633 let next_event_root = self.graph_store.public_events_root()?;
1634 let next_profile_search_root = self.graph_store.profile_search_root()?;
1635 let next_profiles_by_pubkey_root = self.graph_store.profiles_by_pubkey_root()?;
1636 let event_root_changed = next_event_root != previous_event_root;
1637 let profile_search_root_changed = next_profile_search_root != previous_profile_search_root;
1638 let profiles_by_pubkey_root_changed =
1639 next_profiles_by_pubkey_root != previous_profiles_by_pubkey_root;
1640
1641 if event_root_changed {
1642 self.note_public_events_root_change()?;
1643 }
1644 if profile_search_root_changed {
1645 self.note_profile_search_root_change()?;
1646 }
1647 if profiles_by_pubkey_root_changed {
1648 self.note_profiles_by_pubkey_root_change()?;
1649 }
1650 if profile_search_root_changed {
1651 self.maybe_publish_profile_search_root(false).await?;
1652 }
1653 if profiles_by_pubkey_root_changed {
1654 self.maybe_publish_profiles_by_pubkey_root(false).await?;
1655 }
1656 if event_root_changed {
1657 self.maybe_publish_event_root(false).await?;
1658 }
1659 info!(
1660 "Nostr mirror flushed live events: events={} event_root_changed={} profile_search_root_changed={} profiles_by_pubkey_root_changed={}",
1661 event_count,
1662 event_root_changed,
1663 profile_search_root_changed,
1664 profiles_by_pubkey_root_changed
1665 );
1666 Ok(())
1667 }
1668
1669 fn note_public_events_root_change(&self) -> Result<()> {
1670 let root = self.graph_store.public_events_root()?;
1671 if let Err(err) = Self::write_latest_event_root_state(self.store.base_path(), root.as_ref())
1672 {
1673 warn!("Nostr mirror failed to persist queryable event-root state: {err:#}");
1674 }
1675 Self::note_root_change(
1676 self.config.published_event_tree_name.as_deref(),
1677 &self.event_publish_state,
1678 root,
1679 )
1680 }
1681
1682 fn note_profile_search_root_change(&self) -> Result<()> {
1683 let root = self.graph_store.profile_search_root()?;
1684 Self::note_root_change(
1685 self.config.published_profile_search_tree_name.as_deref(),
1686 &self.profile_search_publish_state,
1687 root,
1688 )
1689 }
1690
1691 fn note_profiles_by_pubkey_root_change(&self) -> Result<()> {
1692 let root = self.graph_store.profiles_by_pubkey_root()?;
1693 Self::note_root_change(
1694 self.config
1695 .published_profiles_by_pubkey_tree_name
1696 .as_deref(),
1697 &self.profiles_by_pubkey_publish_state,
1698 root,
1699 )
1700 }
1701
1702 fn note_root_change(
1703 tree_name: Option<&str>,
1704 publish_state: &Arc<Mutex<RootPublishState>>,
1705 root: Option<hashtree_core::Cid>,
1706 ) -> Result<()> {
1707 let Some(_tree_name) = tree_name else {
1708 return Ok(());
1709 };
1710
1711 let mut state = publish_state.lock().expect("root publish state");
1712 let now = Instant::now();
1713
1714 if state.pending_root == root {
1715 return Ok(());
1716 }
1717
1718 state.pending_root = root;
1719 state.last_upload_failed_at = None;
1720 state.last_upload_error = None;
1721 state.last_changed_at = Some(now);
1722 if state.dirty_since.is_none() {
1723 state.dirty_since = Some(now);
1724 }
1725 Ok(())
1726 }
1727
1728 async fn maybe_publish_event_root(&self, force: bool) -> Result<()> {
1729 if self.take_missing_blob_event_upload_error() {
1730 return self.rebuild_event_indexes_after_missing_blobs(force).await;
1731 }
1732 let result = self
1733 .maybe_publish_root(
1734 self.config.published_event_tree_name.as_deref(),
1735 &self.event_publish_state,
1736 "event root",
1737 force,
1738 false,
1739 )
1740 .await;
1741 let Err(error) = result else {
1742 if self.take_missing_blob_event_upload_error() {
1743 return self.rebuild_event_indexes_after_missing_blobs(force).await;
1744 }
1745 return Ok(());
1746 };
1747 if !is_missing_local_blob_push_error(&error) {
1748 return Err(error);
1749 }
1750
1751 self.rebuild_event_indexes_after_missing_blobs(force).await
1752 }
1753
1754 fn take_missing_blob_event_upload_error(&self) -> bool {
1755 let mut state = self
1756 .event_publish_state
1757 .lock()
1758 .expect("event root publish state");
1759 if state.upload_in_progress_root.is_some() {
1760 return false;
1761 }
1762 if !state.missing_blob_rebuild_required {
1763 return false;
1764 }
1765 state.missing_blob_rebuild_required = false;
1766 state.last_upload_error = None;
1767 state.last_upload_failed_at = None;
1768 true
1769 }
1770
1771 async fn rebuild_event_indexes_after_missing_blobs(&self, force: bool) -> Result<()> {
1772 warn!(
1773 "Nostr mirror event root DAG references missing local blobs; rebuilding event indexes from stored events"
1774 );
1775 let (public_count, ambient_count) = self
1776 .graph_store
1777 .rebuild_event_indexes_from_stored_events_async()
1778 .await
1779 .context("rebuild event indexes after missing event blobs")?;
1780 info!(
1781 "Nostr mirror rebuilt event indexes after missing blobs: public={} ambient={}",
1782 public_count, ambient_count
1783 );
1784 self.sync_publish_roots_from_store()?;
1785
1786 self.maybe_publish_root(
1787 self.config.published_event_tree_name.as_deref(),
1788 &self.event_publish_state,
1789 "event root",
1790 force,
1791 false,
1792 )
1793 .await
1794 }
1795
1796 async fn maybe_publish_profile_search_root(&self, force: bool) -> Result<()> {
1797 self.maybe_publish_root(
1798 self.config.published_profile_search_tree_name.as_deref(),
1799 &self.profile_search_publish_state,
1800 "profile search root",
1801 force,
1802 true,
1803 )
1804 .await
1805 }
1806
1807 async fn maybe_publish_profiles_by_pubkey_root(&self, force: bool) -> Result<()> {
1808 self.maybe_publish_root(
1809 self.config
1810 .published_profiles_by_pubkey_tree_name
1811 .as_deref(),
1812 &self.profiles_by_pubkey_publish_state,
1813 "profiles-by-pubkey root",
1814 force,
1815 true,
1816 )
1817 .await
1818 }
1819
1820 async fn maybe_publish_root(
1821 &self,
1822 tree_name: Option<&str>,
1823 publish_state: &Arc<Mutex<RootPublishState>>,
1824 log_label: &str,
1825 force: bool,
1826 publish_before_upload_ready_on_force: bool,
1827 ) -> Result<()> {
1828 if !force && self.root_publication_deferrals.load(Ordering::Acquire) > 0 {
1829 return Ok(());
1830 }
1831 let Some(tree_name) = tree_name else {
1832 return Ok(());
1833 };
1834
1835 let pending_root = {
1836 let state = publish_state.lock().expect("root publish state");
1837 let Some(pending_root) = state.pending_root.clone() else {
1838 return Ok(());
1839 };
1840
1841 let now = Instant::now();
1842 let debounce_ready = state.last_changed_at.is_some_and(|changed_at| {
1843 now.duration_since(changed_at) >= MIRROR_ROOT_PUBLISH_DEBOUNCE
1844 });
1845 let stale_ready = state.dirty_since.is_some_and(|dirty_since| {
1846 now.duration_since(dirty_since) >= MIRROR_ROOT_PUBLISH_MAX_STALENESS
1847 });
1848 if !force && !debounce_ready && !stale_ready {
1849 return Ok(());
1850 }
1851
1852 pending_root
1853 };
1854
1855 let upload_started = self.maybe_start_background_root_upload(
1856 tree_name,
1857 &pending_root,
1858 publish_state,
1859 log_label,
1860 );
1861 let upload_required = !self.config.blossom_write_servers.is_empty();
1862 let (upload_ready, publish_root) = {
1863 let state = publish_state.lock().expect("root publish state");
1864 let upload_ready =
1865 !upload_required || state.last_uploaded_root.as_ref() == Some(&pending_root);
1866 let publish_root = if upload_ready {
1867 Some(pending_root.clone())
1868 } else {
1869 state.last_uploaded_root.clone().filter(|uploaded_root| {
1870 state.last_published_root.as_ref() != Some(uploaded_root)
1871 })
1872 };
1873 (upload_ready, publish_root)
1874 };
1875 let publish_before_upload_ready =
1876 force && upload_required && !upload_ready && publish_before_upload_ready_on_force;
1877 let publish_root = if let Some(publish_root) = publish_root {
1878 publish_root
1879 } else if publish_before_upload_ready {
1880 pending_root.clone()
1881 } else {
1882 if upload_started {
1883 info!(
1884 "Nostr mirror uploading {} DAG before publish: tree={} hash={}",
1885 log_label,
1886 tree_name,
1887 hex::encode(pending_root.hash),
1888 );
1889 }
1890 return Ok(());
1891 };
1892
1893 let mut successful_relays = Vec::new();
1894 let mut failed_relays = Vec::new();
1895 let mut published_now = false;
1896 let publish_required =
1897 self.publish_client.is_some() && !self.config.publish_relays.is_empty();
1898 if publish_required {
1899 let Some(publish_client) = self.publish_client.as_ref() else {
1900 unreachable!("publish_required implies publish_client");
1901 };
1902 if !self.has_connected_publish_relay().await {
1903 return Ok(());
1904 }
1905 if publish_before_upload_ready {
1906 info!(
1907 "Nostr mirror publishing {} before Blossom upload completes: tree={} hash={}",
1908 log_label,
1909 tree_name,
1910 hex::encode(pending_root.hash),
1911 );
1912 } else if !upload_ready {
1913 info!(
1914 "Nostr mirror publishing uploaded {} while newer root is still uploading: tree={} published_hash={} pending_hash={}",
1915 log_label,
1916 tree_name,
1917 hex::encode(publish_root.hash),
1918 hex::encode(pending_root.hash),
1919 );
1920 }
1921
1922 let already_published = {
1923 let state = publish_state.lock().expect("root publish state");
1924 state.last_published_root.as_ref() == Some(&publish_root)
1925 };
1926 if !already_published {
1927 let publish_relays = self.config.publish_relays.clone();
1928 let latest_known_created_at = {
1929 let state = publish_state.lock().expect("root publish state");
1930 state.last_published_created_at
1931 };
1932 let publish_created_at =
1933 next_replaceable_created_at(Timestamp::now(), latest_known_created_at);
1934 let event = publish_client
1935 .sign_event_builder(Self::build_public_root_event(
1936 tree_name,
1937 &publish_root,
1938 publish_created_at,
1939 ))
1940 .await
1941 .with_context(|| format!("sign {log_label} event"))?;
1942 let publish_result = self
1943 .publish_root_event_to_relays(publish_client, &publish_relays, &event)
1944 .await
1945 .with_context(|| format!("publish {log_label} event"))?;
1946 successful_relays = publish_result.0;
1947 failed_relays = publish_result.1;
1948 if successful_relays.is_empty() {
1949 let failure_summary = if failed_relays.is_empty() {
1950 "no publish relays accepted the event".to_string()
1951 } else {
1952 failed_relays.join("; ")
1953 };
1954 anyhow::bail!("no publish relays accepted the event ({failure_summary})");
1955 }
1956
1957 let mut state = publish_state.lock().expect("root publish state");
1958 if state.pending_root.as_ref() == Some(&pending_root)
1959 || state.last_uploaded_root.as_ref() == Some(&publish_root)
1960 || publish_before_upload_ready
1961 {
1962 state.last_published_root = Some(publish_root.clone());
1963 state.last_published_at = Some(Instant::now());
1964 state.last_published_created_at = Some(event.created_at);
1965 }
1966 published_now = true;
1967 }
1968 }
1969
1970 {
1971 let mut state = publish_state.lock().expect("root publish state");
1972 if state.pending_root.as_ref() == Some(&pending_root) {
1973 let upload_satisfied = self.config.blossom_write_servers.is_empty()
1974 || state.last_uploaded_root.as_ref() == Some(&pending_root);
1975 let publish_satisfied =
1976 !publish_required || state.last_published_root.as_ref() == Some(&pending_root);
1977 if upload_satisfied && publish_satisfied {
1978 state.dirty_since = None;
1979 }
1980 }
1981 }
1982
1983 if published_now {
1984 info!(
1985 "Nostr mirror published {}: tree={} hash={} relays={:?}",
1986 log_label,
1987 tree_name,
1988 hex::encode(publish_root.hash),
1989 successful_relays,
1990 );
1991 }
1992 if !failed_relays.is_empty() {
1993 warn!(
1994 "Nostr mirror publish had relay failures: tree={} failures={:?}",
1995 tree_name, failed_relays
1996 );
1997 }
1998 Ok(())
1999 }
2000
2001 fn maybe_start_background_root_upload(
2002 &self,
2003 tree_name: &str,
2004 pending_root: &hashtree_core::Cid,
2005 publish_state: &Arc<Mutex<RootPublishState>>,
2006 log_label: &str,
2007 ) -> bool {
2008 if self.config.blossom_write_servers.is_empty() {
2009 return false;
2010 }
2011
2012 let previous_uploaded_root = {
2013 let mut state = publish_state.lock().expect("root publish state");
2014 if state.last_uploaded_root.as_ref() == Some(pending_root)
2015 || state.upload_in_progress_root.is_some()
2016 {
2017 return false;
2018 }
2019 if state
2020 .last_upload_failed_at
2021 .is_some_and(|failed_at| failed_at.elapsed() < MIRROR_ROOT_UPLOAD_RETRY_INTERVAL)
2022 {
2023 return false;
2024 }
2025 state.upload_in_progress_root = Some(pending_root.clone());
2026 state.last_uploaded_root.clone()
2027 };
2028
2029 let store = Arc::clone(&self.store);
2030 let upload_state_path = Self::uploaded_root_state_path(store.base_path(), tree_name);
2031 let servers = self.config.blossom_write_servers.clone();
2032 let root = pending_root.clone();
2033 let publish_state = Arc::clone(publish_state);
2034 let log_label = log_label.to_string();
2035 tokio::task::spawn_blocking(move || {
2036 let runtime = tokio::runtime::Builder::new_current_thread()
2037 .enable_all()
2038 .build()
2039 .expect("build nostr mirror root upload runtime");
2040 runtime.block_on(async move {
2041 let result = background_blossom_push_incremental_with_store(
2042 store,
2043 root.clone(),
2044 previous_uploaded_root,
2045 &servers,
2046 )
2047 .await;
2048 let mut state = publish_state.lock().expect("root publish state");
2049 if state.upload_in_progress_root.as_ref() == Some(&root) {
2050 state.upload_in_progress_root = None;
2051 }
2052 match result {
2053 Ok(()) => {
2054 if let Err(err) =
2055 Self::write_uploaded_root_state(&upload_state_path, &root, &log_label)
2056 {
2057 warn!("Nostr mirror failed to persist uploaded root state: {err:#}");
2058 }
2059 state.last_uploaded_root = Some(root.clone());
2060 state.last_uploaded_at = Some(Instant::now());
2061 if state.pending_root.as_ref() == Some(&root) {
2062 state.last_upload_failed_at = None;
2063 state.last_upload_error = None;
2064 state.missing_blob_rebuild_required = false;
2065 }
2066 info!(
2067 "Nostr mirror uploaded {} DAG to Blossom: hash={}",
2068 log_label,
2069 hex::encode(root.hash)
2070 );
2071 }
2072 Err(err) => {
2073 if state.pending_root.as_ref() == Some(&root) {
2074 state.last_upload_failed_at = Some(Instant::now());
2075 state.last_upload_error = Some(format!("{err:#}"));
2076 }
2077 if is_missing_local_blob_message(&format!("{err:#}")) {
2078 state.missing_blob_rebuild_required = true;
2079 }
2080 warn!(
2081 "Nostr mirror {} DAG upload failed: hash={} error={:#}",
2082 log_label,
2083 hex::encode(root.hash),
2084 err
2085 );
2086 }
2087 }
2088 });
2089 trim_transient_allocations();
2090 });
2091
2092 true
2093 }
2094
2095 async fn publish_root_event_to_relays(
2096 &self,
2097 publish_client: &Client,
2098 relays: &[String],
2099 event: &Event,
2100 ) -> Result<(Vec<String>, Vec<String>)> {
2101 let primary_send = Self::send_root_event_to_relays(publish_client, relays, event);
2102 let (mut successful_relays, mut failed_relays) =
2103 match tokio::time::timeout(MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT, primary_send).await {
2104 Ok(result) => result,
2105 Err(_) => (
2106 Vec::new(),
2107 vec![format!(
2108 "publish relays: primary publish timed out after {:?}",
2109 MIRROR_ROOT_PUBLISH_PRIMARY_TIMEOUT
2110 )],
2111 ),
2112 };
2113
2114 if successful_relays.is_empty() {
2115 let (retry_successes, retry_failures) = self
2116 .publish_root_event_with_fresh_client(relays, event)
2117 .await;
2118 successful_relays.extend(retry_successes);
2119 failed_relays.extend(retry_failures);
2120 }
2121
2122 Ok((successful_relays, failed_relays))
2123 }
2124
2125 async fn send_root_event_to_relays(
2126 publish_client: &Client,
2127 relays: &[String],
2128 event: &Event,
2129 ) -> (Vec<String>, Vec<String>) {
2130 let mut successful_relays = Vec::new();
2131 let mut failed_relays = Vec::new();
2132
2133 match publish_client
2134 .send_event_to(relays.iter().map(|relay| relay.as_str()), event)
2135 .await
2136 {
2137 Ok(output) => {
2138 for relay in relays {
2139 let relay_url = relay.trim_end_matches('/');
2140 if output
2141 .success
2142 .iter()
2143 .any(|url| url.as_str().trim_end_matches('/') == relay_url)
2144 {
2145 successful_relays.push(relay.clone());
2146 }
2147 }
2148 failed_relays.extend(
2149 output
2150 .failed
2151 .into_iter()
2152 .map(|(url, reason)| format!("{url}: {reason}")),
2153 );
2154 }
2155 Err(err) => {
2156 failed_relays.push(format!("publish relays: {err}"));
2157 }
2158 }
2159
2160 (successful_relays, failed_relays)
2161 }
2162
2163 async fn publish_root_event_with_fresh_client(
2164 &self,
2165 relays: &[String],
2166 event: &Event,
2167 ) -> (Vec<String>, Vec<String>) {
2168 let client = Client::new(Keys::generate());
2169 let mut setup_failures = Vec::new();
2170 for relay in relays {
2171 if let Err(err) = client.add_relay(relay).await {
2172 setup_failures.push(format!("{relay}: add relay failed: {err}"));
2173 }
2174 }
2175
2176 client.connect().await;
2177 let publish = Self::send_root_event_to_relays(&client, relays, event);
2178 let retry_timeout = self
2179 .config
2180 .fetch_timeout
2181 .min(MIRROR_ROOT_PUBLISH_RETRY_TIMEOUT);
2182 let result = tokio::time::timeout(retry_timeout, publish).await;
2183 let _ = client.disconnect().await;
2184
2185 match result {
2186 Ok((successful_relays, mut failed_relays)) => {
2187 failed_relays.extend(setup_failures);
2188 (successful_relays, failed_relays)
2189 }
2190 Err(_) => {
2191 setup_failures.push(format!(
2192 "fresh publish client timed out after {:?}",
2193 retry_timeout
2194 ));
2195 (Vec::new(), setup_failures)
2196 }
2197 }
2198 }
2199
2200 fn build_public_root_event(
2201 tree_name: &str,
2202 cid: &hashtree_core::Cid,
2203 created_at: Timestamp,
2204 ) -> EventBuilder {
2205 let mut tags = vec![
2206 Tag::identifier(tree_name.to_string()),
2207 Tag::custom(
2208 TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::L)),
2209 vec!["hashtree"],
2210 ),
2211 Tag::custom(TagKind::Custom("hash".into()), vec![hex::encode(cid.hash)]),
2212 ];
2213 if let Some(key) = cid.key {
2214 tags.push(Tag::custom(
2215 TagKind::Custom("key".into()),
2216 vec![hex::encode(key)],
2217 ));
2218 }
2219
2220 EventBuilder::new(Kind::Custom(HASHTREE_ROOT_KIND as u16), "")
2221 .tags(tags)
2222 .custom_created_at(created_at)
2223 }
2224}
2225
2226fn is_missing_local_blob_push_error(error: &anyhow::Error) -> bool {
2227 error
2228 .chain()
2229 .any(|cause| cause.to_string().contains(MISSING_LOCAL_BLOB_PUSH_ERROR))
2230}
2231
2232fn is_missing_local_blob_message(message: &str) -> bool {
2233 message.contains(MISSING_LOCAL_BLOB_PUSH_ERROR)
2234}
2235
2236fn next_replaceable_created_at(now: Timestamp, latest_existing: Option<Timestamp>) -> Timestamp {
2237 match latest_existing {
2238 Some(latest) if latest >= now => Timestamp::from_secs(latest.as_secs().saturating_add(1)),
2239 _ => now,
2240 }
2241}
2242
2243#[cfg(test)]
2244mod tests;