Skip to main content

hashtree_cli/
nostr_mirror.rs

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