Skip to main content

hashtree_cli/
nostr_mirror.rs

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