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:
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());
+ }
+}