From aaa8941bdbd228915a40c845748ec9d4f2868de5 Mon Sep 17 00:00:00 2001 From: Yuki Kishimoto Date: Wed, 11 Oct 2023 12:42:18 +0200 Subject: [PATCH] Add `nostr-sdk-db` crate --- Cargo.toml | 4 +- crates/nostr-sdk-db/Cargo.toml | 18 ++++++ crates/nostr-sdk-db/src/error.rs | 27 ++++++++ crates/nostr-sdk-db/src/lib.rs | 102 ++++++++++++++++++++++++++++++ crates/nostr-sdk-db/src/memory.rs | 84 ++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 crates/nostr-sdk-db/Cargo.toml create mode 100644 crates/nostr-sdk-db/src/error.rs create mode 100644 crates/nostr-sdk-db/src/lib.rs create mode 100644 crates/nostr-sdk-db/src/memory.rs diff --git a/Cargo.toml b/Cargo.toml index 67f151d1f..e73a41cba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,7 @@ members = [ "bindings/nostr-sdk-ffi", "bindings/nostr-sdk-js", "bindings/uniffi-bindgen", - "crates/nostr", - "crates/nostr-sdk", - "crates/nostr-sdk-net", + "crates/*", ] default-members = ["crates/*"] resolver = "2" diff --git a/crates/nostr-sdk-db/Cargo.toml b/crates/nostr-sdk-db/Cargo.toml new file mode 100644 index 000000000..36ebc56c1 --- /dev/null +++ b/crates/nostr-sdk-db/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "nostr-sdk-db" +version = "0.1.0" +edition = "2021" +description = "Nostr SDK Database" +authors = ["Yuki Kishimoto "] +homepage.workspace = true +repository.workspace = true +license.workspace = true +readme = "README.md" +rust-version.workspace = true +keywords = ["nostr", "sdk", "db"] + +[dependencies] +async-trait = "0.1" +nostr = { version = "0.24", path = "../nostr", default-features = false, features = ["std"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync"] } diff --git a/crates/nostr-sdk-db/src/error.rs b/crates/nostr-sdk-db/src/error.rs new file mode 100644 index 000000000..95209a2f2 --- /dev/null +++ b/crates/nostr-sdk-db/src/error.rs @@ -0,0 +1,27 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Distributed under the MIT software license + +//! Database Error + +use thiserror::Error; + +/// Database Error +#[derive(Debug, Error)] +pub enum DatabaseError { + /// An error happened in the underlying database backend. + #[error(transparent)] + Backend(Box), +} + +impl DatabaseError { + /// Create a new [`Backend`][Self::Backend] error. + /// + /// Shorthand for `Error::Backend(Box::new(error))`. + #[inline] + pub fn backend(error: E) -> Self + where + E: std::error::Error + Send + Sync + 'static, + { + Self::Backend(Box::new(error)) + } +} diff --git a/crates/nostr-sdk-db/src/lib.rs b/crates/nostr-sdk-db/src/lib.rs new file mode 100644 index 000000000..e10466dd4 --- /dev/null +++ b/crates/nostr-sdk-db/src/lib.rs @@ -0,0 +1,102 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Distributed under the MIT software license + +//! Nostr SDK Database + +#![warn(missing_docs)] +#![warn(rustdoc::bare_urls)] + +use async_trait::async_trait; +use nostr::{Event, EventId, Filter, Url}; + +mod error; +pub mod memory; + +pub use self::error::DatabaseError; + +/// Backend +pub enum Backend { + /// Memory + Memory, + /// RocksDB + RocksDB, + /// Lightning Memory-Mapped Database + LMDB, + /// SQLite + SQLite, + /// IndexedDB + IndexedDB, + /// Custom + Custom(String), +} + +/// A type-erased [`StateStore`]. +pub type DynNostrDatabase = dyn NostrDatabase; + +/// Nostr SDK Database +#[async_trait] +pub trait NostrDatabase: AsyncTraitDeps { + /// Error + type Err; + + /// Name of the backend database used (ex. rocksdb, lmdb, sqlite, indexeddb, ...) + fn backend(&self) -> Backend; + + /// Save [`Event`] into store + async fn save_event(&self, event: &Event) -> Result<(), Self::Err>; + + /// Save [`EventId`] seen by relay + /// + /// Useful for NIP65 (gossip) + async fn save_event_id_seen_by_relay( + &self, + event_id: EventId, + relay_url: Url, + ) -> Result<(), Self::Err>; + + /// Get list of relays that have seen the [`EventId`] + async fn event_recently_seen_on_relays(&self, event_id: EventId) + -> Result, Self::Err>; + + /// Query store with filters + async fn query(&self, filters: Vec) -> Result, Self::Err>; + + /// Get event IDs by filters + /// + /// Uuseful for negentropy reconciliation + async fn event_ids_by_filters(&self, filters: Vec) -> Result, Self::Err>; +} + +/// Alias for `Send` on non-wasm, empty trait (implemented by everything) on +/// wasm. +#[cfg(not(target_arch = "wasm32"))] +pub trait SendOutsideWasm: Send {} +#[cfg(not(target_arch = "wasm32"))] +impl SendOutsideWasm for T {} + +/// Alias for `Send` on non-wasm, empty trait (implemented by everything) on +/// wasm. +#[cfg(target_arch = "wasm32")] +pub trait SendOutsideWasm {} +#[cfg(target_arch = "wasm32")] +impl SendOutsideWasm for T {} + +/// Alias for `Sync` on non-wasm, empty trait (implemented by everything) on +/// wasm. +#[cfg(not(target_arch = "wasm32"))] +pub trait SyncOutsideWasm: Sync {} +#[cfg(not(target_arch = "wasm32"))] +impl SyncOutsideWasm for T {} + +/// Alias for `Sync` on non-wasm, empty trait (implemented by everything) on +/// wasm. +#[cfg(target_arch = "wasm32")] +pub trait SyncOutsideWasm {} +#[cfg(target_arch = "wasm32")] +impl SyncOutsideWasm for T {} + +/// Super trait that is used for our store traits, this trait will differ if +/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have +/// implemented, while other targets will. +pub trait AsyncTraitDeps: std::fmt::Debug + SendOutsideWasm + SyncOutsideWasm {} +impl AsyncTraitDeps for T {} diff --git a/crates/nostr-sdk-db/src/memory.rs b/crates/nostr-sdk-db/src/memory.rs new file mode 100644 index 000000000..2119f6a6a --- /dev/null +++ b/crates/nostr-sdk-db/src/memory.rs @@ -0,0 +1,84 @@ +// Copyright (c) 2022-2023 Yuki Kishimoto +// Distributed under the MIT software license + +//! Nostr SDK Database + +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use async_trait::async_trait; +use nostr::{Event, EventId, Filter, Url}; +use thiserror::Error; +use tokio::sync::RwLock; + +use crate::{Backend, DatabaseError, NostrDatabase}; + +/// Memory Database Error +#[derive(Debug, Error)] +pub enum Error {} + +impl From for DatabaseError { + fn from(e: Error) -> Self { + DatabaseError::backend(e) + } +} + +/// Memory Database (RAM) +#[derive(Debug, Default)] +pub struct MemoryDatabase { + seen_event_ids: Arc>>>, +} + +impl MemoryDatabase { + /// New Memory database + pub fn new() -> Self { + Self::default() + } +} + +#[async_trait] +impl NostrDatabase for MemoryDatabase { + type Err = DatabaseError; + + fn backend(&self) -> Backend { + Backend::Memory + } + + async fn save_event(&self, _event: &Event) -> Result<(), Self::Err> { + Ok(()) + } + + async fn save_event_id_seen_by_relay( + &self, + event_id: EventId, + relay_url: Url, + ) -> Result<(), Self::Err> { + let mut seen_event_ids = self.seen_event_ids.write().await; + seen_event_ids + .entry(event_id) + .and_modify(|set| { + set.insert(relay_url.clone()); + }) + .or_insert_with(|| { + let mut set = HashSet::with_capacity(1); + set.insert(relay_url); + set + }); + Ok(()) + } + + async fn event_recently_seen_on_relays( + &self, + _event_id: EventId, + ) -> Result, Self::Err> { + todo!() + } + + async fn query(&self, _filters: Vec) -> Result, Self::Err> { + Ok(Vec::new()) + } + + async fn event_ids_by_filters(&self, _filters: Vec) -> Result, Self::Err> { + Ok(Vec::new()) + } +}