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 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 {
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 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 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 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 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 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 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;