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