tangle_indexer


git clone https://radroots.dev/git/tangle_indexer.git
Log | Files | Refs | Submodules | LICENSE

commit 7208385dfba24ee82a20e6633b08ca4e7fe894ec
parent 753b7016cb68e3903bf950ebce489fb731224d56
Author: triesap <137732411+triesap@users.noreply.github.com>
Date:   Sat,  2 Aug 2025 21:23:42 +0000

Update `indexer` adding kind 0 metadata event indexing and writing of hashed `RadrootsMetadataEvent` under events/0

Diffstat:
MCargo.lock | 240+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/indexer/Cargo.toml | 1+
Dcrates/indexer/src/domain/event/kind.rs | 56--------------------------------------------------------
Dcrates/indexer/src/domain/event/mod.rs | 5-----
Acrates/indexer/src/domain/events/metadata.rs | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/indexer/src/domain/events/mod.rs | 21+++++++++++++++++++++
Dcrates/indexer/src/domain/indexer.rs | 27---------------------------
Rcrates/indexer/src/domain/event/key.rs -> crates/indexer/src/domain/indexer/key.rs | 0
Acrates/indexer/src/domain/indexer/kind.rs | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/indexer/src/domain/indexer/mod.rs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/indexer/src/domain/indexer/models/metadata.rs | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/indexer/src/domain/indexer/models/mod.rs | 31+++++++++++++++++++++++++++++++
Mcrates/indexer/src/lib.rs | 26+++++++++++++++++---------
Mcrates/indexer/src/relay/event.rs | 5++++-
Mcrates/indexer/src/relay/record.rs | 2+-
15 files changed, 703 insertions(+), 99 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -39,6 +39,21 @@ dependencies = [ ] [[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anstream" version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -157,6 +172,12 @@ dependencies = [ ] [[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -178,6 +199,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] name = "clap" version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -272,6 +307,12 @@ dependencies = [ ] [[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -436,6 +477,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] name = "indexer-utils" version = "0.1.0" dependencies = [ @@ -472,6 +537,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] name = "json5" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -573,6 +648,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] name = "object" version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -719,6 +803,16 @@ dependencies = [ ] [[package]] +name = "radroots-common" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", + "typeshare", +] + +[[package]] name = "radroots-market-relay-indexer" version = "0.1.0" dependencies = [ @@ -726,6 +820,7 @@ dependencies = [ "clap", "config", "indexer-utils", + "radroots-common", "serde", "serde_json", "thiserror 1.0.69", @@ -831,6 +926,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1188,6 +1289,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] +name = "typeshare" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19be0f411120091e76e13e5a0186d8e2bcc3e7e244afdb70152197f1a8486ceb" +dependencies = [ + "chrono", + "serde", + "serde_json", + "typeshare-annotation", +] + +[[package]] +name = "typeshare-annotation" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" +dependencies = [ + "quote", + "syn", +] + +[[package]] name = "ucd-trie" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1236,6 +1359,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1258,6 +1439,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/crates/indexer/Cargo.toml b/crates/indexer/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] indexer-utils = { path = "../indexer-utils" } +radroots-common = { path = "../../../../../crates/radroots-common/" } anyhow = "1.0" clap = { version = "4", features = ["derive"] } diff --git a/crates/indexer/src/domain/event/kind.rs b/crates/indexer/src/domain/event/kind.rs @@ -1,56 +0,0 @@ -use serde::ser::Serializer; -use serde::Serialize; -use std::fmt; - -use crate::domain::event::{IndexerKey, METADATA_INDEX_DIRECTORY}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum IndexerEventKind { - Metadata, -} - -impl IndexerEventKind { - pub const ALL: [IndexerEventKind; 1] = [IndexerEventKind::Metadata]; - - pub const fn as_u64(self) -> u64 { - match self { - IndexerEventKind::Metadata => 0, - } - } - - pub const fn paths(self) -> &'static [IndexerKey] { - match self { - IndexerEventKind::Metadata => &METADATA_INDEX_DIRECTORY, - } - } -} - -impl fmt::Display for IndexerEventKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_u64()) - } -} - -#[derive(thiserror::Error, Debug)] -#[error("unknown event kind: {0}")] -pub struct IndexerEventKindParseError(pub u64); - -impl TryFrom<u64> for IndexerEventKind { - type Error = IndexerEventKindParseError; - - fn try_from(val: u64) -> Result<Self, Self::Error> { - match val { - 0 => Ok(IndexerEventKind::Metadata), - other => Err(IndexerEventKindParseError(other)), - } - } -} - -impl Serialize for IndexerEventKind { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - serializer.serialize_u64(self.as_u64()) - } -} diff --git a/crates/indexer/src/domain/event/mod.rs b/crates/indexer/src/domain/event/mod.rs @@ -1,5 +0,0 @@ -mod key; -mod kind; - -pub use key::{IndexerKey, METADATA_INDEX_DIRECTORY}; -pub use kind::{IndexerEventKind, IndexerEventKindParseError}; diff --git a/crates/indexer/src/domain/events/metadata.rs b/crates/indexer/src/domain/events/metadata.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +use radroots_common::models::events::{ + RadrootsNostrEvent, RadrootsMetadataEvent, RadrootsMetadataEventData, + RadrootsMetadataEventDataMetadata, +}; +use std::collections::HashMap; +use thiserror::Error; + +use crate::domain::events::RequiredField; +use crate::{opt_required, relay::event::RelayIndexerEvent}; + +#[derive(Debug, Error)] +pub enum RadrootsMetadataEventError { + #[error("Failed to parse metadata content JSON: {0}")] + ParseError(#[from] serde_json::Error), + + #[error("Missing or empty 'name' field in profile data")] + MissingNameField, + + #[error("Missing or invalid 'published_at' tag")] + MissingPublishedAt, +} + +pub fn create_radroots_metadata_event_data( + content: String, + tags: Vec<Vec<String>>, +) -> Result<RadrootsMetadataEventData, RadrootsMetadataEventError> { + let mut tag_map: HashMap<String, Vec<Vec<String>>> = HashMap::new(); + for tag in tags { + if let Some(key) = tag.get(0).map(String::as_str) { + tag_map.entry(key.to_string()).or_default().push(tag); + } + } + + let get = |key: &str| -> Option<String> { tag_map.get(key)?.get(0)?.get(1).cloned() }; + + let published_at_str = opt_required!(get("published_at")) + .map_err(|_| RadrootsMetadataEventError::MissingPublishedAt)?; + let published_at = published_at_str + .parse::<u32>() + .map_err(|_| RadrootsMetadataEventError::MissingPublishedAt)?; + + let metadata: RadrootsMetadataEventDataMetadata = serde_json::from_str(&content)?; + if metadata.name.trim().is_empty() { + return Err(RadrootsMetadataEventError::MissingNameField); + } + + Ok(RadrootsMetadataEventData { + metadata, + published_at, + }) +} + +pub trait ToRadrootsMetadataEvent { + fn to_radroots_metadata_event(self) -> Result<RadrootsMetadataEvent, RadrootsMetadataEventError>; +} + +impl ToRadrootsMetadataEvent for RelayIndexerEvent { + fn to_radroots_metadata_event(self) -> Result<RadrootsMetadataEvent, RadrootsMetadataEventError> { + let data = create_radroots_metadata_event_data(self.content.clone(), self.tags.clone())?; + + let kind = self.kind.as_u64(); + + Ok(RadrootsMetadataEvent { + event: RadrootsNostrEvent { + id: self.id, + author: self.author, + created_at: self.created_at, + kind: kind.try_into().unwrap(), + content: self.content, + tags: self.tags, + sig: self.sig, + }, + data, + }) + } +} diff --git a/crates/indexer/src/domain/events/mod.rs b/crates/indexer/src/domain/events/mod.rs @@ -0,0 +1,21 @@ +pub mod metadata; + +#[macro_export] +macro_rules! opt_required { + ($opt:expr) => { + $opt.required(stringify!($opt)) + }; +} + +pub trait RequiredField { + type Output; + fn required(self, field_name: &str) -> Result<Self::Output, String>; +} + +impl<T> RequiredField for Option<T> { + type Output = T; + + fn required(self, field_name: &str) -> Result<T, String> { + self.ok_or_else(|| format!("Missing {}", field_name)) + } +} diff --git a/crates/indexer/src/domain/indexer.rs b/crates/indexer/src/domain/indexer.rs @@ -1,27 +0,0 @@ -use anyhow::{Context, Result}; -use indexer_utils::file::fs_mkdir; - -use crate::{config::Settings, IndexerEventKind}; - -pub fn create_index_dirs(settings: &Settings) -> Result<()> { - for kind in IndexerEventKind::ALL { - let kind_str = kind.as_u64().to_string(); - - for subdir in kind.paths() { - fs_mkdir(&[ - settings.service.output_dir.as_str(), - "events", - &kind_str, - subdir.as_str(), - ]) - .with_context(|| { - format!( - "Failed to create directory for kind {} / {}", - kind_str, - subdir.as_str() - ) - })?; - } - } - Ok(()) -} diff --git a/crates/indexer/src/domain/event/key.rs b/crates/indexer/src/domain/indexer/key.rs diff --git a/crates/indexer/src/domain/indexer/kind.rs b/crates/indexer/src/domain/indexer/kind.rs @@ -0,0 +1,56 @@ +use serde::ser::Serializer; +use serde::Serialize; +use std::fmt; + +use crate::domain::indexer::{IndexerKey, METADATA_INDEX_DIRECTORY}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum IndexerEventKind { + Metadata, +} + +impl IndexerEventKind { + pub const ALL: [IndexerEventKind; 1] = [IndexerEventKind::Metadata]; + + pub const fn as_u64(self) -> u64 { + match self { + IndexerEventKind::Metadata => 0, + } + } + + pub const fn paths(self) -> &'static [IndexerKey] { + match self { + IndexerEventKind::Metadata => &METADATA_INDEX_DIRECTORY, + } + } +} + +impl fmt::Display for IndexerEventKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_u64()) + } +} + +#[derive(thiserror::Error, Debug)] +#[error("unknown event kind: {0}")] +pub struct IndexerEventKindParseError(pub u64); + +impl TryFrom<u64> for IndexerEventKind { + type Error = IndexerEventKindParseError; + + fn try_from(val: u64) -> Result<Self, Self::Error> { + match val { + 0 => Ok(IndexerEventKind::Metadata), + other => Err(IndexerEventKindParseError(other)), + } + } +} + +impl Serialize for IndexerEventKind { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_u64(self.as_u64()) + } +} diff --git a/crates/indexer/src/domain/indexer/mod.rs b/crates/indexer/src/domain/indexer/mod.rs @@ -0,0 +1,62 @@ +use std::{collections::HashMap, path::PathBuf}; + +use anyhow::{Context, Result}; +use indexer_utils::file::fs_mkdir; + +use crate::{ + config::Settings, + domain::indexer::{ + kind::IndexerEventKind, + models::{Event0StaticIndexes, EventIndexes, WriteEventIndexes}, + }, + relay::event::RelayIndexerEvent, +}; + +pub mod key; +pub mod kind; +pub mod models; + +pub use key::{IndexerKey, METADATA_INDEX_DIRECTORY}; + +pub fn create_index_dirs(settings: &Settings) -> Result<()> { + for kind in IndexerEventKind::ALL { + let kind_str = kind.as_u64().to_string(); + + for subdir in kind.paths() { + fs_mkdir(&[ + settings.service.output_dir.as_str(), + "events", + &kind_str, + subdir.as_str(), + ]) + .with_context(|| { + format!( + "Failed to create directory for kind {} / {}", + kind_str, + subdir.as_str() + ) + })?; + } + } + Ok(()) +} + +pub fn write_index_events( + settings: &Settings, + events_by_kind: &HashMap<IndexerEventKind, Vec<RelayIndexerEvent>>, +) -> Result<Vec<PathBuf>> { + let mut updated = Vec::new(); + + for &kind in &IndexerEventKind::ALL { + let events = events_by_kind.get(&kind).cloned().unwrap_or_default(); + match kind { + IndexerEventKind::Metadata => { + let idx = + Event0StaticIndexes::build(&events).context("building indexes for Metadata")?; + idx.write(settings, &mut updated)?; + } + } + } + + Ok(updated) +} diff --git a/crates/indexer/src/domain/indexer/models/metadata.rs b/crates/indexer/src/domain/indexer/models/metadata.rs @@ -0,0 +1,193 @@ +use indexer_utils::{ + file::{fs_mkdir, fs_write_with_hash_check}, + logs::truncate_log, + paths::paths_join, +}; +use radroots_common::models::events::RadrootsMetadataEvent; +use serde_json::Value; +use std::collections::BTreeMap; +use std::path::PathBuf; +use tracing::{instrument, warn}; + +use crate::{ + domain::{ + events::metadata::ToRadrootsMetadataEvent, + indexer::{ + kind::IndexerEventKind, + models::{EventIndexes, NostrEventsStaticError, WriteEventIndexes}, + IndexerKey, METADATA_INDEX_DIRECTORY, + }, + }, + relay::event::RelayIndexerEvent, + Settings, +}; + +#[derive(Debug)] +pub struct Event0StaticIndexes { + events: Vec<RadrootsMetadataEvent>, + events_id: BTreeMap<String, RadrootsMetadataEvent>, + events_nip05: BTreeMap<String, RadrootsMetadataEvent>, + events_author: BTreeMap<String, Vec<RadrootsMetadataEvent>>, +} + +impl EventIndexes for Event0StaticIndexes { + type Event = RelayIndexerEvent; + + fn subdirs() -> &'static [IndexerKey] { + &METADATA_INDEX_DIRECTORY + } + + #[instrument(skip(raw_events), fields(event_count = raw_events.len()))] + fn build(raw_events: &[Self::Event]) -> Result<Self, NostrEventsStaticError> { + let mut events = Vec::with_capacity(raw_events.len()); + let mut events_id = BTreeMap::new(); + let mut events_nip05 = BTreeMap::new(); + let mut events_author: BTreeMap<String, Vec<RadrootsMetadataEvent>> = BTreeMap::new(); + + for raw in raw_events { + match raw.clone().to_radroots_metadata_event() { + Ok(parsed) => { + let id = parsed.event.id.clone(); + let author = parsed.event.author.clone(); + + events.push(parsed.clone()); + events_id.insert(id.clone(), parsed.clone()); + events_author + .entry(author.clone()) + .or_default() + .push(parsed.clone()); + + if let Some(nip05) = &parsed.data.metadata.nip05 { + let normalized = nip05.replace("@radroots.market", ""); + events_nip05.insert(normalized, parsed); + } + } + Err(err) => { + warn!( + kind = raw.kind.as_u64(), + id = %raw.id, + author = %raw.author, + content = %truncate_log(&raw.content, 1000), + tags = ?raw.tags, + error = %err, + "Skipping malformed metadata event" + ); + } + } + } + + Ok(Event0StaticIndexes { + events, + events_id, + events_nip05, + events_author, + }) + } + + fn index_json(&self, subdir: IndexerKey) -> Option<Value> { + match subdir { + IndexerKey::Id => serde_json::to_value(&self.events_id).ok(), + IndexerKey::Author => { + // Map author -> [event IDs] + let map: BTreeMap<&String, Vec<String>> = self + .events_author + .iter() + .map(|(author, evts)| { + let ids = evts.iter().map(|e| e.event.id.clone()).collect(); + (author, ids) + }) + .collect(); + serde_json::to_value(&map).ok() + } + IndexerKey::Nip05 => serde_json::to_value(&self.events_nip05).ok(), + _ => None, + } + } +} + +impl WriteEventIndexes for Event0StaticIndexes { + fn write(&self, settings: &Settings, updated: &mut Vec<PathBuf>) -> anyhow::Result<()> { + let base = paths_join(&[ + settings.service.output_dir.as_str(), + "events", + &IndexerEventKind::Metadata.as_u64().to_string(), + ])?; + fs_mkdir(&[&base])?; + + // Write top-level events.json with all event IDs + let all_ids: Vec<&String> = self.events.iter().map(|e| &e.event.id).collect(); + let top_events = base.join("events.json"); + if fs_write_with_hash_check(&top_events, &all_ids)? { + updated.push(top_events.clone()); + } + + // Per-subdir indices + for &subdir in Event0StaticIndexes::subdirs().iter() { + let sub_base = base.join(subdir.as_str()); + fs_mkdir(&[sub_base.to_str().unwrap()])?; + + // Write indexes.json (list of keys) + let keys_lower: Vec<String> = match subdir { + IndexerKey::Id => self.events_id.keys().map(|k| k.to_lowercase()).collect(), + IndexerKey::Author => self + .events_author + .keys() + .map(|k| k.to_lowercase()) + .collect(), + IndexerKey::Nip05 => self.events_nip05.keys().map(|k| k.to_lowercase()).collect(), + other => { + warn!("No index keys for subdir {:?}", other); + Vec::new() + } + }; + let idxs = sub_base.join("indexes.json"); + if fs_write_with_hash_check(&idxs, &keys_lower)? { + updated.push(idxs.clone()); + } + + // Write events.json according to subdir variant + match subdir { + IndexerKey::Author => { + // One events.json per-author, mapping event_id -> full RadrootsMetadataEvent + for (author, evts_list) in &self.events_author { + let author_dir = sub_base.join(author.to_lowercase()); + fs_mkdir(&[author_dir.to_str().unwrap()])?; + + let mut map: BTreeMap<String, &RadrootsMetadataEvent> = BTreeMap::new(); + for ev in evts_list { + map.insert(ev.event.id.clone(), ev); + } + + let evts_path = author_dir.join("events.json"); + if fs_write_with_hash_check(&evts_path, &map)? { + updated.push(evts_path.clone()); + } + } + } + IndexerKey::Id => { + // Flat events.json at subdir root + let ids: Vec<&String> = self.events_id.values().map(|e| &e.event.id).collect(); + let evts = sub_base.join("events.json"); + if fs_write_with_hash_check(&evts, &ids)? { + updated.push(evts.clone()); + } + } + IndexerKey::Nip05 => { + // Flat events.json at subdir root + let ids: Vec<&String> = + self.events_nip05.values().map(|e| &e.event.id).collect(); + let evts = sub_base.join("events.json"); + if fs_write_with_hash_check(&evts, &ids)? { + updated.push(evts.clone()); + } + } + other => { + // Default fallback: no writer implemented + warn!("No static writer implemented for subdir {:?}", other); + } + } + } + + Ok(()) + } +} diff --git a/crates/indexer/src/domain/indexer/models/mod.rs b/crates/indexer/src/domain/indexer/models/mod.rs @@ -0,0 +1,31 @@ +pub use metadata::Event0StaticIndexes; + +use crate::{config::Settings, domain::indexer::IndexerKey}; +use anyhow::Result; +use serde_json::Value; +use std::path::PathBuf; +use thiserror::Error; + +pub mod metadata; + +#[derive(Debug, Error)] +pub enum NostrEventsStaticError { + #[error("Failed to build static indexes: {0}")] + BuildError(#[from] anyhow::Error), +} + +pub trait EventIndexes { + type Event; + + fn subdirs() -> &'static [IndexerKey]; + + fn build(events: &[Self::Event]) -> Result<Self, NostrEventsStaticError> + where + Self: Sized; + + fn index_json(&self, subdir: IndexerKey) -> Option<Value>; +} + +pub trait WriteEventIndexes { + fn write(&self, settings: &Settings, updated: &mut Vec<PathBuf>) -> Result<()>; +} diff --git a/crates/indexer/src/lib.rs b/crates/indexer/src/lib.rs @@ -11,7 +11,7 @@ pub mod config; pub mod telemetry; pub mod domain { - pub mod event; + pub mod events; pub mod indexer; } @@ -21,10 +21,12 @@ pub mod relay { } pub use config::Settings; -pub use domain::event::{IndexerEventKind, IndexerKey}; pub use relay::record::RelayEventRecord; -use crate::relay::event::RelayIndexerEvent; +use crate::{ + domain::indexer::{kind::IndexerEventKind, write_index_events}, + relay::event::RelayIndexerEvent, +}; pub async fn run(settings: Settings) -> Result<()> { let select_event_kinds = IndexerEventKind::ALL @@ -56,7 +58,7 @@ pub async fn run(settings: Settings) -> Result<()> { .collect::<Result<_, _>>() .context("collecting RelayEventRecord rows")?; - info!(record_count = records.len(), "Loaded RelayEventRecords"); + info!(record_count = records.len(), "Loaded relay records"); let records_by_kind: HashMap<IndexerEventKind, Vec<RelayIndexerEvent>> = records .into_iter() @@ -71,15 +73,21 @@ pub async fn run(settings: Settings) -> Result<()> { }, ); - info!( - records_count_by_kind = records_by_kind.len(), - "Loaded RelayIndexerEvents" - ); + let updated = write_index_events(&settings, &records_by_kind)?; + + info!(updated_files = updated.len(), "Updated index events"); // sleep let elapsed = iteration_start.elapsed(); let interval = Duration::from_secs(settings.service.flush_interval); - tokio::time::sleep(interval.saturating_sub(elapsed)).await; + let delay = interval.saturating_sub(elapsed); + + info!( + elapsed_ms = elapsed.as_millis(), + sleeping_ms = delay.as_millis(), + "Iteration complete" + ); + tokio::time::sleep(delay).await; } #[allow(unreachable_code)] diff --git a/crates/indexer/src/relay/event.rs b/crates/indexer/src/relay/event.rs @@ -1,7 +1,10 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use crate::{domain::event::IndexerEventKindParseError, IndexerEventKind, RelayEventRecord}; +use crate::{ + domain::indexer::kind::{IndexerEventKind, IndexerEventKindParseError}, + RelayEventRecord, +}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct RelayRawEvent { diff --git a/crates/indexer/src/relay/record.rs b/crates/indexer/src/relay/record.rs @@ -1,7 +1,7 @@ use indexer_utils::sqlite::{RustqliteError, SqliteResult, SqliteRow, SqliteType}; use serde::Serialize; -use crate::domain::event::{IndexerEventKind, IndexerEventKindParseError}; +use crate::domain::indexer::kind::{IndexerEventKind, IndexerEventKindParseError}; #[derive(Clone, Debug, Serialize)] pub struct RelayEventRecord {