Skip to main content

hashtree_cli/
nostr_mirror.rs

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