Skip to content

Commit

Permalink
database: add SaveEventStatus enum
Browse files Browse the repository at this point in the history
Return `SaveEventStatus` enum instead of `bool` for `NostrEventsDatabase::save_event` method to provide more detailed feedback about event insertion. This change includes support for rejection reasons.

Closes rust-nostr#673

Signed-off-by: Yuki Kishimoto <[email protected]>
  • Loading branch information
yukibtc committed Dec 12, 2024
1 parent 231e476 commit 2f662f8
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 93 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
* nostr: refactor `MachineReadablePrefix::parse` method to use `&str` directly ([Yuki Kishimoto])
* nostr: update `RelayMessage::Notice` variant ([Yuki Kishimoto])
* database: reduce default in-memory database limit to `35_000` ([Yuki Kishimoto])
* database: update `NostrEventsDatabase::save_event` method signature ([Yuki Kishimoto])
* pool: replace `Option<String>` with `String` in `Output::failed` ([Yuki Kishimoto])
* sdk: update `fetch_*` and `stream_*` methods signature ([Yuki Kishimoto])
* bindings: remove redundant parsing methods from `EventId`, `Coordinate`, `PublicKey` and `SecretKey` ([Yuki Kishimoto])
Expand All @@ -64,6 +65,7 @@
* nostr: add `Tags::challenge` method ([Yuki Kishimoto])
* nostr: add `RelayUrl::is_local_addr` ([Yuki Kishimoto])
* database: impl PartialEq and Eq for `Events` ([Yuki Kishimoto])
* database: add `SaveEventStatus` enum ([Yuki Kishimoto])
* pool: add `ReceiverStream` ([Yuki Kishimoto])
* sdk: automatically resend event after NIP-42 authentication ([Yuki Kishimoto])
* relay-builder: add NIP42 support ([Yuki Kishimoto])
Expand Down
59 changes: 54 additions & 5 deletions bindings/nostr-sdk-ffi/src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,65 @@ use nostr_sdk::prelude::{self, IntoNostrDatabase, NostrEventsDatabaseExt};
use nostr_sdk::NdbDatabase;
#[cfg(feature = "lmdb")]
use nostr_sdk::NostrLMDB;
use uniffi::Object;
use uniffi::{Enum, Object};

pub mod events;

use self::events::Events;
use crate::error::Result;
use crate::protocol::{Event, EventId, Filter, Metadata, PublicKey};

/// Reason why event wasn't stored into the database
#[derive(Enum)]
pub enum RejectedReason {
/// Ephemeral events aren't expected to be stored
Ephemeral,
/// The event already exists
Duplicate,
/// The event was deleted
Deleted,
/// The event is expired
Expired,
/// The event was replaced
Replaced,
/// Attempt to delete a non-owned event
InvalidDelete,
/// Other reason
Other,
}

impl From<prelude::RejectedReason> for RejectedReason {
fn from(status: prelude::RejectedReason) -> Self {
match status {
prelude::RejectedReason::Ephemeral => Self::Ephemeral,
prelude::RejectedReason::Duplicate => Self::Duplicate,
prelude::RejectedReason::Deleted => Self::Deleted,
prelude::RejectedReason::Expired => Self::Expired,
prelude::RejectedReason::Replaced => Self::Replaced,
prelude::RejectedReason::InvalidDelete => Self::InvalidDelete,
prelude::RejectedReason::Other => Self::Other,
}
}
}

/// Save event status
#[derive(Enum)]
pub enum SaveEventStatus {
/// The event has been successfully saved
Success,
/// The event has been rejected
Rejected(RejectedReason),
}

impl From<prelude::SaveEventStatus> for SaveEventStatus {
fn from(status: prelude::SaveEventStatus) -> Self {
match status {
prelude::SaveEventStatus::Success => Self::Success,
prelude::SaveEventStatus::Rejected(reason) => Self::Rejected(reason.into()),
}
}
}

#[derive(Object)]
pub struct NostrDatabase {
inner: Arc<dyn prelude::NostrDatabase>,
Expand Down Expand Up @@ -69,10 +120,8 @@ impl NostrDatabase {
// TODO: re-allow to use custom database (only for events)?

/// Save [`Event`] into store
///
/// Return `true` if event was successfully saved into database.
pub async fn save_event(&self, event: &Event) -> Result<bool> {
Ok(self.inner.save_event(event.deref()).await?)
pub async fn save_event(&self, event: &Event) -> Result<SaveEventStatus> {
Ok(self.inner.save_event(event.deref()).await?.into())
}

/// Get list of relays that have seen the [`EventId`]
Expand Down
43 changes: 38 additions & 5 deletions bindings/nostr-sdk-js/src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,41 @@ use crate::protocol::key::JsPublicKey;
use crate::protocol::types::{JsFilter, JsMetadata};
use crate::JsStringArray;

#[wasm_bindgen(js_name = SaveEventStatus)]
pub enum JsSaveEventStatus {
/// The event has been successfully saved into the database
Success,
/// Ephemeral events aren't expected to be stored
Ephemeral,
/// The event already exists
Duplicate,
/// The event was deleted
Deleted,
/// The event is expired
Expired,
/// The event was replaced
Replaced,
/// Attempt to delete a non-owned event
InvalidDelete,
/// Other reason
Other,
}

impl From<SaveEventStatus> for JsSaveEventStatus {
fn from(status: SaveEventStatus) -> Self {
match status {
SaveEventStatus::Success => Self::Success,
SaveEventStatus::Rejected(RejectedReason::Ephemeral) => Self::Ephemeral,
SaveEventStatus::Rejected(RejectedReason::Duplicate) => Self::Duplicate,
SaveEventStatus::Rejected(RejectedReason::Deleted) => Self::Deleted,
SaveEventStatus::Rejected(RejectedReason::Expired) => Self::Expired,
SaveEventStatus::Rejected(RejectedReason::Replaced) => Self::Replaced,
SaveEventStatus::Rejected(RejectedReason::InvalidDelete) => Self::InvalidDelete,
SaveEventStatus::Rejected(RejectedReason::Other) => Self::Other,
}
}
}

/// Nostr Database
#[wasm_bindgen(js_name = NostrDatabase)]
pub struct JsNostrDatabase {
Expand Down Expand Up @@ -63,11 +98,9 @@ impl JsNostrDatabase {

/// Save `Event` into store
///
/// Return `true` if event was successfully saved into database.
///
/// **This method assume that `Event` was already verified**
pub async fn save_event(&self, event: &JsEvent) -> Result<bool> {
self.inner.save_event(event).await.map_err(into_err)
/// **This method assumes that `Event` was already verified**
pub async fn save_event(&self, event: &JsEvent) -> Result<JsSaveEventStatus> {
Ok(self.inner.save_event(event).await.map_err(into_err)?.into())
}
/// Get list of relays that have seen the [`EventId`]
#[wasm_bindgen(js_name = eventSeenOnRelays)]
Expand Down
4 changes: 2 additions & 2 deletions crates/nostr-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,8 @@ async fn handle_command(command: ShellCommand, client: &Client) -> Result<()> {
let now = Instant::now();

for event in iter {
if let Ok(stored) = db.save_event(&event).await {
if stored {
if let Ok(status) = db.save_event(&event).await {
if status.is_success() {
counter += 1;
}
}
Expand Down
70 changes: 38 additions & 32 deletions crates/nostr-database/src/events/helper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use nostr::{Alphabet, Event, EventId, Filter, Kind, PublicKey, SingleLetterTag,
use tokio::sync::{OwnedRwLockReadGuard, RwLock};

use crate::collections::tree::{BTreeCappedSet, Capacity, InsertResult, OverCapacityPolicy};
use crate::Events;
use crate::{Events, RejectedReason, SaveEventStatus};

type DatabaseEvent = Arc<Event>;

Expand Down Expand Up @@ -132,10 +132,10 @@ impl From<Filter> for QueryPattern {
}

/// Database Event Result
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatabaseEventResult {
/// Handled event should be stored into database?
pub to_store: bool,
/// Status
pub status: SaveEventStatus,
/// List of events that should be removed from database
pub to_discard: HashSet<EventId>,
}
Expand Down Expand Up @@ -190,21 +190,33 @@ impl InternalDatabaseHelper {
.into_iter()
.rev() // Lookup ID: EVENT_ORD_IMPL
.filter(|e| !e.is_expired() && !e.kind.is_ephemeral())
.filter(move |event| self.internal_index_event(event, &now).to_store)
.filter(move |event| self.internal_index_event(event, &now).status.is_success())
}

fn internal_index_event(&mut self, event: &Event, now: &Timestamp) -> DatabaseEventResult {
// Check if was already added
if self.ids.contains_key(&event.id) {
return DatabaseEventResult::default();
return DatabaseEventResult {
status: SaveEventStatus::Rejected(RejectedReason::Duplicate),
to_discard: HashSet::new(),
};
}

// Check if was deleted or is expired
if self.deleted_ids.contains(&event.id) || event.is_expired_at(now) {
if self.deleted_ids.contains(&event.id) {
let mut to_discard: HashSet<EventId> = HashSet::with_capacity(1);
to_discard.insert(event.id);
return DatabaseEventResult {
to_store: false,
status: SaveEventStatus::Rejected(RejectedReason::Deleted),
to_discard,
};
}

if event.is_expired_at(now) {
let mut to_discard: HashSet<EventId> = HashSet::with_capacity(1);
to_discard.insert(event.id);
return DatabaseEventResult {
status: SaveEventStatus::Rejected(RejectedReason::Expired),
to_discard,
};
}
Expand All @@ -216,13 +228,13 @@ impl InternalDatabaseHelper {
let created_at: Timestamp = event.created_at;
let kind: Kind = event.kind;

let mut should_insert: bool = true;
let mut status: SaveEventStatus = SaveEventStatus::Success;

if kind.is_replaceable() {
let params: QueryByKindAndAuthorParams = QueryByKindAndAuthorParams::new(kind, author);
for ev in self.internal_query_by_kind_and_author(params) {
if ev.created_at > created_at || ev.id == event.id {
should_insert = false;
status = SaveEventStatus::Rejected(RejectedReason::Replaced);
} else {
to_discard.insert(ev.id);
}
Expand All @@ -235,28 +247,28 @@ impl InternalDatabaseHelper {

// Check if coordinate was deleted
if self.has_coordinate_been_deleted(&coordinate, now) {
should_insert = false;
status = SaveEventStatus::Rejected(RejectedReason::Deleted);
} else {
let params: QueryByParamReplaceable =
QueryByParamReplaceable::new(kind, author, identifier.to_string());
if let Some(ev) = self.internal_query_param_replaceable(params) {
if ev.created_at > created_at || ev.id == event.id {
should_insert = false;
status = SaveEventStatus::Rejected(RejectedReason::Replaced);
} else {
to_discard.insert(ev.id);
}
}
}
}
None => should_insert = false,
None => status = SaveEventStatus::Rejected(RejectedReason::Other),
}
} else if kind == Kind::EventDeletion {
// Check `e` tags
for id in event.tags.event_ids() {
if let Some(ev) = self.ids.get(id) {
if ev.pubkey != author {
to_discard.insert(event.id);
should_insert = false;
status = SaveEventStatus::Rejected(RejectedReason::InvalidDelete);
break;
}

Expand All @@ -270,7 +282,7 @@ impl InternalDatabaseHelper {
for coordinate in event.tags.coordinates() {
if coordinate.public_key != author {
to_discard.insert(event.id);
should_insert = false;
status = SaveEventStatus::Rejected(RejectedReason::InvalidDelete);
break;
}

Expand Down Expand Up @@ -310,7 +322,7 @@ impl InternalDatabaseHelper {
self.discard_events(&to_discard);

// Insert event
if should_insert {
if status.is_success() {
let e: DatabaseEvent = Arc::new(event.clone()); // TODO: avoid clone?

let InsertResult { inserted, pop } = self.events.insert(e.clone());
Expand Down Expand Up @@ -349,10 +361,7 @@ impl InternalDatabaseHelper {
}
}

DatabaseEventResult {
to_store: should_insert,
to_discard,
}
DatabaseEventResult { status, to_discard }
}

fn discard_events(&mut self, ids: &HashSet<EventId>) {
Expand Down Expand Up @@ -405,9 +414,12 @@ impl InternalDatabaseHelper {
///
/// **This method assume that [`Event`] was already verified**
pub fn index_event(&mut self, event: &Event) -> DatabaseEventResult {
// Check if it's expired or ephemeral (in `internal_index_event` is checked only the raw event expiration)
if event.is_expired() || event.kind.is_ephemeral() {
return DatabaseEventResult::default();
// Check if it's ephemeral
if event.kind.is_ephemeral() {
return DatabaseEventResult {
status: SaveEventStatus::Rejected(RejectedReason::Ephemeral),
to_discard: HashSet::new(),
};
}
let now = Timestamp::now();
self.internal_index_event(event, &now)
Expand Down Expand Up @@ -710,14 +722,8 @@ impl DatabaseHelper {

/// Index [`Event`]
///
/// **This method assume that [`Event`] was already verified**
/// **This method assumes that [`Event`] was already verified**
pub async fn index_event(&self, event: &Event) -> DatabaseEventResult {
// Check if it's expired or ephemeral
if event.is_expired() || event.kind.is_ephemeral() {
return DatabaseEventResult::default();
}

// Acquire write lock
let mut inner = self.inner.write().await;
inner.index_event(event)
}
Expand Down Expand Up @@ -987,7 +993,7 @@ mod tests {
// Test add new replaceable event (metadata)
let first_ev_metadata = Event::from_json(REPLACEABLE_EVENT_1).unwrap();
let res = indexes.index_event(&first_ev_metadata).await;
assert!(res.to_store);
assert!(res.status.is_success());
assert!(res.to_discard.is_empty());
assert_eq!(
indexes
Expand All @@ -1002,7 +1008,7 @@ mod tests {
// Test add replace metadata
let ev = Event::from_json(REPLACEABLE_EVENT_2).unwrap();
let res = indexes.index_event(&ev).await;
assert!(res.to_store);
assert!(res.status.is_success());
assert!(res.to_discard.contains(&first_ev_metadata.id));
assert_eq!(
indexes
Expand Down
Loading

0 comments on commit 2f662f8

Please sign in to comment.