lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 3de23850c76e522ee039353470f17ec31ea4359a
parent 7b7d6014a5ff2d77a179ab1637df9fc6a85972f5
Author: triesap <tyson@radroots.org>
Date:   Thu, 19 Feb 2026 18:06:45 +0000

nostr-ndb: add config and open lifecycle api


- add a dedicated config type for ndb open settings
- add the radroots ndb handle with deterministic open flow
- map core open-path failures into crate error variants
- add unit tests for config builders and database creation

Diffstat:
MCargo.lock | 1+
Mnostr-ndb/Cargo.toml | 1+
Anostr-ndb/src/config.rs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mnostr-ndb/src/error.rs | 14++++++++++++--
Mnostr-ndb/src/lib.rs | 8++++++++
Anostr-ndb/src/ndb.rs | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 139 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2240,6 +2240,7 @@ dependencies = [ "nostrdb", "radroots-nostr", "radroots-nostr-runtime", + "tempfile", "thiserror 1.0.69", "tokio", ] diff --git a/nostr-ndb/Cargo.toml b/nostr-ndb/Cargo.toml @@ -24,3 +24,4 @@ tokio = { workspace = true, optional = true, features = ["rt", "sync", "time"] } [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] } +tempfile = { workspace = true } diff --git a/nostr-ndb/src/config.rs b/nostr-ndb/src/config.rs @@ -0,0 +1,51 @@ +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RadrootsNostrNdbConfig { + db_dir: PathBuf, + mapsize_bytes: Option<usize>, + ingester_threads: Option<i32>, + skip_validation: bool, +} + +impl RadrootsNostrNdbConfig { + pub fn new(db_dir: impl Into<PathBuf>) -> Self { + Self { + db_dir: db_dir.into(), + mapsize_bytes: None, + ingester_threads: None, + skip_validation: false, + } + } + + pub fn db_dir(&self) -> &Path { + &self.db_dir + } + + pub fn mapsize_bytes(&self) -> Option<usize> { + self.mapsize_bytes + } + + pub fn ingester_threads(&self) -> Option<i32> { + self.ingester_threads + } + + pub fn skip_validation(&self) -> bool { + self.skip_validation + } + + pub fn with_mapsize_bytes(mut self, bytes: usize) -> Self { + self.mapsize_bytes = Some(bytes); + self + } + + pub fn with_ingester_threads(mut self, threads: i32) -> Self { + self.ingester_threads = Some(threads); + self + } + + pub fn with_skip_validation(mut self, skip_validation: bool) -> Self { + self.skip_validation = skip_validation; + self + } +} diff --git a/nostr-ndb/src/error.rs b/nostr-ndb/src/error.rs @@ -2,6 +2,16 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum RadrootsNostrNdbError { - #[error("not implemented")] - NotImplemented, + #[error("database path must be utf-8")] + NonUtf8Path, + + #[error("nostrdb error: {0}")] + Ndb(String), +} + +#[cfg(feature = "ndb")] +impl From<nostrdb::Error> for RadrootsNostrNdbError { + fn from(value: nostrdb::Error) -> Self { + Self::Ndb(value.to_string()) + } } diff --git a/nostr-ndb/src/lib.rs b/nostr-ndb/src/lib.rs @@ -6,8 +6,16 @@ compile_error!("radroots-nostr-ndb requires the std feature"); extern crate alloc; +#[cfg(feature = "ndb")] +pub mod config; pub mod error; +#[cfg(feature = "ndb")] +pub mod ndb; pub mod prelude { + #[cfg(feature = "ndb")] + pub use crate::config::RadrootsNostrNdbConfig; pub use crate::error::RadrootsNostrNdbError; + #[cfg(feature = "ndb")] + pub use crate::ndb::RadrootsNostrNdb; } diff --git a/nostr-ndb/src/ndb.rs b/nostr-ndb/src/ndb.rs @@ -0,0 +1,66 @@ +use crate::config::RadrootsNostrNdbConfig; +use crate::error::RadrootsNostrNdbError; +use std::path::Path; + +#[derive(Debug, Clone)] +pub struct RadrootsNostrNdb { + db_dir: std::path::PathBuf, + pub(crate) inner: nostrdb::Ndb, +} + +impl RadrootsNostrNdb { + pub fn open(config: RadrootsNostrNdbConfig) -> Result<Self, RadrootsNostrNdbError> { + let mut inner_config = nostrdb::Config::new().skip_validation(config.skip_validation()); + if let Some(mapsize_bytes) = config.mapsize_bytes() { + inner_config = inner_config.set_mapsize(mapsize_bytes); + } + if let Some(ingester_threads) = config.ingester_threads() { + inner_config = inner_config.set_ingester_threads(ingester_threads); + } + + let db_dir = config.db_dir().to_path_buf(); + let db_dir_str = db_dir.to_str().ok_or(RadrootsNostrNdbError::NonUtf8Path)?; + let inner = nostrdb::Ndb::new(db_dir_str, &inner_config)?; + + Ok(Self { db_dir, inner }) + } + + pub fn db_dir(&self) -> &Path { + &self.db_dir + } + + pub(crate) fn inner(&self) -> &nostrdb::Ndb { + &self.inner + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn config_builder_tracks_values() { + let config = RadrootsNostrNdbConfig::new("target/testdbs/nostr_ndb_config") + .with_mapsize_bytes(1024 * 1024) + .with_ingester_threads(2) + .with_skip_validation(true); + + assert_eq!(config.mapsize_bytes(), Some(1024 * 1024)); + assert_eq!(config.ingester_threads(), Some(2)); + assert!(config.skip_validation()); + } + + #[test] + fn open_creates_database() { + let tmp_dir = TempDir::new().expect("tempdir should open"); + let db_dir = tmp_dir.path().join("ndb"); + let config = RadrootsNostrNdbConfig::new(&db_dir) + .with_mapsize_bytes(64 * 1024 * 1024) + .with_ingester_threads(1); + + let ndb = RadrootsNostrNdb::open(config).expect("database should open"); + assert_eq!(ndb.db_dir(), db_dir.as_path()); + assert!(db_dir.exists()); + } +}