lib

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

commit e1e9e41cd85d3b9329a5947bdeb1c999bb317b79
parent 639888d90fc00d1edc8e92e999b67c12cf51e932
Author: triesap <tyson@radroots.org>
Date:   Thu, 19 Feb 2026 16:29:11 +0000

nostr-runtime: add pluggable event store api


- add a store module with a runtime event-store trait and in-memory implementation
- extend runtime builder configuration to accept injected event stores
- ingest streamed events into the configured store and surface store failures as runtime errors
- add tests for in-memory ingestion and store-enabled runtime construction

Diffstat:
Mnostr-runtime/src/lib.rs | 4++++
Mnostr-runtime/src/runtime.rs | 37++++++++++++++++++++++++++++++++++++-
Anostr-runtime/src/store.rs | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 106 insertions(+), 1 deletion(-)

diff --git a/nostr-runtime/src/lib.rs b/nostr-runtime/src/lib.rs @@ -8,11 +8,15 @@ pub mod types; #[cfg(all(feature = "nostr-client", feature = "rt"))] pub mod runtime; +#[cfg(all(feature = "nostr-client", feature = "rt"))] +pub mod store; pub mod prelude { pub use crate::error::RadrootsNostrRuntimeError; #[cfg(all(feature = "nostr-client", feature = "rt"))] pub use crate::runtime::{RadrootsNostrRuntime, RadrootsNostrRuntimeBuilder}; + #[cfg(all(feature = "nostr-client", feature = "rt"))] + pub use crate::store::{RadrootsNostrEventStore, RadrootsNostrInMemoryEventStore}; pub use crate::types::{ RadrootsNostrConnectionSnapshot, RadrootsNostrRuntimeEvent, RadrootsNostrSubscriptionHandle, RadrootsNostrSubscriptionPolicy, diff --git a/nostr-runtime/src/runtime.rs b/nostr-runtime/src/runtime.rs @@ -1,4 +1,5 @@ use crate::error::RadrootsNostrRuntimeError; +use crate::store::RadrootsNostrEventStore; use crate::types::{ RadrootsNostrConnectionSnapshot, RadrootsNostrRuntimeEvent, RadrootsNostrSubscriptionHandle, RadrootsNostrSubscriptionPolicy, RadrootsNostrSubscriptionSpec, RadrootsNostrTrafficLight, @@ -18,12 +19,13 @@ use std::sync::Mutex; use tokio::sync::mpsc; use tokio::task::JoinHandle; -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct RadrootsNostrRuntimeBuilder { keys: Option<RadrootsNostrKeys>, relays: Vec<String>, queue_capacity: usize, monitor_capacity: usize, + event_store: Option<Arc<dyn RadrootsNostrEventStore>>, } impl RadrootsNostrRuntimeBuilder { @@ -36,6 +38,7 @@ impl RadrootsNostrRuntimeBuilder { relays: Vec::new(), queue_capacity: Self::DEFAULT_QUEUE_CAPACITY, monitor_capacity: Self::DEFAULT_MONITOR_CAPACITY, + event_store: None, } } @@ -64,6 +67,11 @@ impl RadrootsNostrRuntimeBuilder { self } + pub fn event_store(mut self, store: Arc<dyn RadrootsNostrEventStore>) -> Self { + self.event_store = Some(store); + self + } + pub fn build(self) -> Result<RadrootsNostrRuntime, RadrootsNostrRuntimeError> { let keys = self .keys @@ -94,6 +102,7 @@ impl RadrootsNostrRuntimeBuilder { started: AtomicBool::new(false), shutting_down: AtomicBool::new(false), next_subscription_id: AtomicU64::new(1), + event_store: self.event_store, }); Ok(RadrootsNostrRuntime { inner }) @@ -123,6 +132,7 @@ struct RadrootsNostrRuntimeInner { started: AtomicBool, shutting_down: AtomicBool, next_subscription_id: AtomicU64, + event_store: Option<Arc<dyn RadrootsNostrEventStore>>, } impl RadrootsNostrRuntime { @@ -403,6 +413,18 @@ fn spawn_subscription_worker( let kind = event.kind.as_u16(); since_unix = Some(event.created_at.as_secs().saturating_add(1)); + if let Some(store) = inner.event_store.as_ref() { + if let Err(message) = store.ingest_event(&event) { + if let Ok(mut guard) = inner.last_error.lock() { + *guard = Some(message.clone()); + } + let _ = inner + .queue_tx + .send(RadrootsNostrRuntimeEvent::Error { message }) + .await; + } + } + let _ = inner .queue_tx .send(RadrootsNostrRuntimeEvent::Note { @@ -436,6 +458,8 @@ fn spawn_subscription_worker( #[cfg(test)] mod tests { use super::*; + use crate::store::RadrootsNostrInMemoryEventStore; + use alloc::sync::Arc; use radroots_nostr::prelude::RadrootsNostrFilter; fn sample_runtime() -> RadrootsNostrRuntime { @@ -495,6 +519,17 @@ mod tests { } #[test] + fn build_accepts_event_store() { + let store = Arc::new(RadrootsNostrInMemoryEventStore::new()); + let result = RadrootsNostrRuntimeBuilder::new() + .keys(RadrootsNostrKeys::generate()) + .add_relay("wss://relay.example.com") + .event_store(store) + .build(); + assert!(result.is_ok()); + } + + #[test] fn set_relays_rejects_empty_input() { let runtime = sample_runtime(); let result = runtime.set_relays(Vec::new()); diff --git a/nostr-runtime/src/store.rs b/nostr-runtime/src/store.rs @@ -0,0 +1,66 @@ +use alloc::string::ToString; +use alloc::vec::Vec; +use radroots_nostr::prelude::RadrootsNostrEvent; +use std::sync::Mutex; + +pub trait RadrootsNostrEventStore: Send + Sync { + fn ingest_event(&self, event: &RadrootsNostrEvent) -> Result<(), String>; +} + +#[derive(Default)] +pub struct RadrootsNostrInMemoryEventStore { + events: Mutex<Vec<RadrootsNostrEvent>>, +} + +impl RadrootsNostrInMemoryEventStore { + pub fn new() -> Self { + Self::default() + } + + pub fn events(&self) -> Vec<RadrootsNostrEvent> { + self.events + .lock() + .map(|guard| guard.clone()) + .unwrap_or_default() + } + + pub fn len(&self) -> usize { + self.events.lock().map(|guard| guard.len()).unwrap_or(0) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl RadrootsNostrEventStore for RadrootsNostrInMemoryEventStore { + fn ingest_event(&self, event: &RadrootsNostrEvent) -> Result<(), String> { + self.events + .lock() + .map_err(|_| "in-memory store lock poisoned".to_string()) + .map(|mut guard| { + guard.push(event.clone()); + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_nostr::prelude::{RadrootsNostrEventBuilder, RadrootsNostrKeys}; + + #[test] + fn in_memory_store_tracks_events() { + let store = RadrootsNostrInMemoryEventStore::new(); + let keys = RadrootsNostrKeys::generate(); + let event = RadrootsNostrEventBuilder::text_note("hello") + .sign_with_keys(&keys) + .expect("event should sign"); + + store + .ingest_event(&event) + .expect("event should be accepted"); + assert_eq!(store.len(), 1); + assert!(!store.is_empty()); + } +}