Skip to main content

hashtree_cli/
nostr_mirror.rs

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        // SAFETY: malloc_trim asks glibc to return free heap pages to the OS.
105        // It does not touch live allocations and is safe to call after the
106        // large, synchronous mirror upload job has dropped its temporary data.
107        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        // A remainder smaller than the checkpoint cadence still owns the newest
1372        // durable local root. Force one final publication attempt before the
1373        // deferral guard lets the periodic publisher resume.
1374        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;